@meilynx/sdk
v0.7.0
Published
Meilynx SDK for business-outcome and span ingestion, plus proxy correlation context.
Maintainers
Readme
Meilynx JS SDK
Meilynx is an AI governance and FinOps platform that gives enterprises visibility and control
over LLM usage — from cost and compliance to business outcomes. Route your LLM traffic through the
Local Proxy for cost, tokens, model, latency, and governance; use this SDK to record
business-outcome events (captureOutcome), emit non-LLM spans, and propagate correlation context from your
Node.js applications.
[!IMPORTANT] The SDK does not ingest LLM telemetry. LLM cost, tokens, model, latency, governance, and budgets are captured by the Local Proxy — the sole supported LLM-telemetry path — with business dimensions attached via the
x-meilynx-contextheader. The SDK's roles are business-outcome events, non-LLM spans, and the correlation context that ties them back to proxy-captured LLM activity. (Thetrack()LLM-ingestion helpers were removed in v0.7.0 — see the CHANGELOG.)
Install
npm install @meilynx/sdk
# or
yarn add @meilynx/sdk
# or
pnpm add @meilynx/sdkQuickstart
Correlation context on proxy-routed calls
Point your OpenAI / Anthropic client at the Meilynx proxy, then call instrument() once at startup. Calls made inside
an observe() / withAgent() scope automatically carry the active correlation context as the x-meilynx-context
header (and, when a trace is active, a W3C traceparent), so the proxy attributes the captured LLM cost to the right
customer, feature, agent, and step — no request-body changes.
import { MeilynxClient, instrument, observe } from "@meilynx/sdk";
import OpenAI from "openai";
const mx = new MeilynxClient({
apiKey: process.env.MX_API_KEY, // baseUrl defaults to https://api.meilynx.com
});
instrument({ client: mx }); // OpenAI + Anthropic calls carry Meilynx context on the proxy path
// Route the provider client through your Meilynx proxy.
const openai = new OpenAI({ baseURL: process.env.MEILYNX_PROXY_URL });
await observe({ featureKey: "ask_docs", customerId: "cust-acme" }, async () => {
await openai.chat.completions.create({ model: "gpt-4o", messages: [/* ... */] });
});
await mx.shutdown();The proxy records model, tokens, cost, latency, and governance for that call; the SDK's job is to tell it who and what the call was for.
Configuration
Environment variables (convention)
The SDK does not read environment variables directly. These are recommended names for your app configuration:
MX_BASE_URL(optional) — Meilynx API URLMX_API_KEY— Project-scoped API key (mx_live_...)
Constructor options
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| apiKey | string | — | Required. API key (mx_live_...) for /v1/ingest/*. |
| baseUrl | string | "https://api.meilynx.com" | Base URL for the Meilynx API. |
| sourceSystem | string | optional | Defaults to sdk. |
| outcomesEndpointPath | string | /v1/ingest/outcomes/events/batch | Outcome-event ingestion path. |
| spansEndpointPath | string | /v1/ingest/spans | Non-LLM span ingestion path. |
| flushAt | number | 25 | Batch size before flush. |
| flushIntervalMs | number | 5000 | Auto-flush interval (ms). Set to 0 to disable. |
| maxRetries | number | 3 | Retry attempts on 429/5xx. |
| retryDelayMs | number | 250 | Base delay for backoff (ms). |
| disableValidation | boolean | false | Disable JSON schema validation. |
Context propagation with observe()
The observe() function propagates business context (correlation IDs, feature keys, customer IDs)
through the async call stack using AsyncLocalStorage. All AI calls made inside inherit
this context automatically and carry it on the x-meilynx-context header when routed through the proxy:
import { observe } from "@meilynx/sdk";
async function handleRequest(customerId: string) {
return observe({ featureKey: "ask_docs", customerId }, async () => {
// all AI calls here are tagged with featureKey="ask_docs"
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "..." }],
});
return response;
});
}Nested observe() calls inherit the parent context and can override specific fields.
Agentic loops — withAgent, withStep, withTool
For agentic loops with multiple tool-call hops, use the dedicated helpers so every LLM call in one turn shares a
single correlationId, is attributed to the named agent, and nests correctly in the trace tree. withAgent()
establishes a trace root; withStep() / withTool() mint child spans and emit non-LLM span events to
/v1/ingest/spans. Without an outer wrapper, a bare observe({ stepIndex: i }) inside a for loop generates a fresh
random correlationId per iteration — the work scatters across distinct correlation groups.
import { withAgent, withStep, withTool } from "@meilynx/sdk";
await withAgent({ agentName: "workspace-assistant", featureKey: "assistant" }, async () => {
for (let i = 0; i < maxSteps; i++) {
const completion = await withStep(i, async () => {
return openai.chat.completions
.stream({ model: process.env.AZURE_OPENAI_DEPLOYMENT, messages, tools })
.finalChatCompletion();
});
if (completion.choices[0].finish_reason === "stop") break;
for (const call of completion.choices[0].message.tool_calls ?? []) {
const result = await withTool(call.function.name, async () => runTool(call));
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
}
});This populates the four dimensions the agentic dashboard groups on — correlationId, agentName, stepIndex,
toolName — and, via the traceparent header, nests each proxied LLM call under the active step to form a
session → turn → call tree in the Explorer.
See the full guide at docs.meilynx.com / Instrumenting Agentic Loops.
Azure OpenAI
Azure OpenAI is supported the same way: point your AzureOpenAI client (or an OpenAI client configured with an
Azure baseURL) at the Meilynx proxy. The wrapper attaches the x-meilynx-context / traceparent headers on those
calls too; the proxy tags the provider and resolves the deployment to the underlying model for cost.
Capturing outcomes
Outcomes are the business results your AI features produce:
import { mintIdempotencyKey } from "@meilynx/sdk";
mx.captureOutcome({
outcomeType: "feature.result.accepted",
idempotencyKey: mintIdempotencyKey("accepted", correlationId),
correlationId,
customerId: "cust-acme",
featureKey: "ask_docs",
occurredAtUtc: new Date(),
});Set traceId / spanId / parentSpanId on an outcome to attach it to the trace tree built from proxy LLM-call
spans, so a business result appears as a leaf under the turn that produced it.
Idempotency keys
Every outcome requires an idempotencyKey to prevent duplicate processing. Use
mintIdempotencyKey() to generate a deterministic SHA-256 key from one or more fields:
import { mintIdempotencyKey } from "@meilynx/sdk";
// Same inputs always produce the same key
mintIdempotencyKey("accepted", "run-123"); // → "a1b2c3..."
mintIdempotencyKey("accepted", "run-123"); // → "a1b2c3..." (same)
mintIdempotencyKey("accepted", "run-456"); // → "d4e5f6..." (different)Budget status
Check current budget utilization from your application. Results are cached for 60 seconds per query-parameter combination.
const status = await mx.getBudgetStatus({ customerId: "acme" });
for (const budget of status.budgets) {
if (budget.action === "block") {
console.warn(`Budget ${budget.name} exceeded: ${budget.utilizationPct}%`);
}
}Failsafe behavior
The SDK is designed to never break your application. Context injection on the provider wrappers is wrapped in defensive error handling:
- If context building or header injection fails, the original LLM call proceeds unmodified.
- If a provider rejects an instrumented request with a Meilynx-caused 400, the SDK retries once with the original args.
- Real LLM provider errors (rate limits, auth failures, invalid requests) always propagate normally.
In other words: a bug in the Meilynx SDK will log a warning but never cause your AI calls to fail.
Browser / client-side outcomes
The main SDK is server-only, but you can capture outcomes from browser code using the
lightweight @meilynx/sdk/browser export. It sends events to a server-side proxy endpoint
you control — no API key is exposed to the browser.
Browser client:
import { MeilynxBrowser } from "@meilynx/sdk/browser";
const mx = new MeilynxBrowser({ proxyUrl: "/api/meilynx/outcome" });
await mx.captureOutcome({
outcomeType: "feature.result.accepted",
idempotencyKey: "accepted:run-123",
correlationId: "run-123",
occurredAtUtc: new Date().toISOString(),
});Server-side proxy (Next.js example):
// app/api/meilynx/outcome/route.ts
import { MeilynxClient } from "@meilynx/sdk";
const mx = new MeilynxClient({ apiKey: process.env.MX_API_KEY! });
export async function POST(req: Request) {
const body = await req.json();
if (Array.isArray(body.events)) {
mx.captureOutcomeBatch(body.events);
} else {
mx.captureOutcome(body);
}
await mx.flush();
return Response.json({ ok: true });
}See the browser outcomes guide for more details.
Batching and flushing
The SDK buffers outcome and span events and flushes automatically. Use flush() for deterministic delivery (e.g.,
request end) and shutdown() to drain queues before process exit. Retries occur for HTTP 429 and 5xx responses with
exponential backoff; 401/403 errors throw immediately with an auth hint.
In short-lived environments (Lambda, Cloudflare Workers, Vercel Edge), flush before the handler returns:
// Vercel Edge / Cloudflare Workers
export default {
async fetch(req, env, ctx) {
const result = await handleRequest(req);
ctx.waitUntil(mx.flush()); // flush without blocking the response
return result;
},
};
// AWS Lambda / Next.js API routes
export async function handler(event) {
const result = await handleRequest(event);
await mx.flush(); // flush before returning
return result;
}Set flushIntervalMs: 0 to disable the background flush timer if the runtime does not support long-lived timers.
Example script
npm run build
MX_BASE_URL=http://localhost:5000 MX_API_KEY=mx_live_... node examples/send-outcome.mjsCompatibility
- Node.js 18+ (recommended)
- Works in serverless runtimes that support
node:cryptoandfetch. - Server-side SDK is not intended for browsers. Use
@meilynx/sdk/browserfor client-side outcome capture.
Security and data handling
- Avoid sending sensitive PII unless required for analytics.
- Prefer hashed or pseudonymous IDs (e.g.,
customerId,endUserId). - Redact secrets from
attributesbefore sending.
Docs
- Documentation: docs.meilynx.com
- Python SDK: github.com/meilynx/meilynx-python
- .NET SDK: github.com/meilynx/meilynx-dotnet
Roadmap
- OpenTelemetry bridge for trace export
- Edge runtime optimizations
- Built-in redaction helpers
License
MIT
