@meilynx/sdk
v0.5.0
Published
Meilynx SDK for telemetry and outcomes ingestion.
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. This SDK lets you send structured telemetry and outcome events from your Node.js applications.
Install
npm install @meilynx/sdk
# or
yarn add @meilynx/sdk
# or
pnpm add @meilynx/sdkQuickstart
Option 1: Auto-instrumentation (recommended)
Add two lines at startup — existing OpenAI and Anthropic calls are tracked automatically:
import { MeilynxClient, instrument } from "@meilynx/sdk";
const mx = new MeilynxClient({
apiKey: process.env.MX_API_KEY, // baseUrl defaults to https://api.meilynx.com
});
instrument({ client: mx }); // covers OpenAI and Anthropic automatically
// use your AI clients as normal — calls are tracked
import OpenAI from "openai";
const openai = new OpenAI();
await openai.chat.completions.create({ model: "gpt-4o", messages: [...] });
await mx.shutdown();Option 2: Explicit tracking
Full control over what you send:
import { MeilynxClient } from "@meilynx/sdk";
const mx = new MeilynxClient({
apiKey: process.env.MX_API_KEY, // baseUrl defaults to https://api.meilynx.com
});
mx.track({
eventType: "llm.response",
correlationId: "run-123",
customerId: "cust-acme",
featureKey: "ask_docs",
model: "gpt-4o",
provider: "openai",
promptTokens: 1200,
completionTokens: 220,
});
await mx.flush();Which to choose? Use auto-instrumentation (Option 1) for most cases — it automatically captures model, latency, and agentic context with zero manual work. Use explicit tracking (Option 2) when you need custom event types, non-LLM operations, or providers without built-in integration.
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. |
| 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 instrumented AI calls inside inherit
this context automatically:
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 event in one turn shares a single correlationId and is
attributed to the named agent. Without an outer wrapper, a bare
observe({ stepIndex: i }) inside a for loop generates a fresh random
correlationId per iteration — the events scatter across distinct
correlation groups and the agentic dashboard shows nothing.
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 () => {
const stream = openai.chat.completions.stream({
model: process.env.AZURE_OPENAI_DEPLOYMENT,
messages,
tools,
stream_options: { include_usage: true },
});
return stream.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 first-class fields the agentic dashboard groups on:
correlationId, agentName, stepIndex, toolName. The auto-instrumentation
also captures responseToolCalls (tool names the model invoked) automatically
from streaming and non-streaming responses.
See the full guide at docs.meilynx.com / Instrumenting Agentic Loops.
Azure OpenAI
The SDK auto-detects Azure clients — both the new AzureOpenAI({ deployment })
subclass and the new OpenAI({ baseURL: "https://...openai.azure.com/openai/deployments/{dep}/" })
pattern. Detected requests are tagged with provider: "azure-openai", and
the deployment name is used as a fallback for model when the caller doesn't
include model in the request body (the typical Azure pattern). No code
change needed — instrumentation handles it.
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(),
});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. All instrumentation, context injection, telemetry emission, and governance extraction are wrapped in defensive error handling:
- If context building or injection fails, the original LLM call proceeds unmodified.
- If telemetry emission fails, the error is swallowed silently.
- If governance response parsing fails, the result is returned as-is.
- 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.
Streaming
Auto-instrumentation handles streaming transparently. For both OpenAI and Anthropic:
- One telemetry event is emitted per completion (not per chunk)
isStreamingis set totrueautomaticallylatencyMsmeasures time from request start to last token received- Tool calls in the response are accumulated into
responseToolCalls
Important: OpenAI streaming requires stream_options.include_usage
OpenAI does not include token counts in streaming responses by default. Without
the option below, promptTokens and completionTokens are undefined, and your
cost will appear as $0 in the dashboard until you redeploy with the option set.
const stream = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...],
stream: true,
stream_options: { include_usage: true }, // ← required for accurate cost
});The SDK does not silently set this for you — that would conflict with our transparent-proxy positioning. Instead, a one-time warning is logged when the SDK observes a streaming completion with no usage data. Add the option once in your codebase and the warning goes away.
Anthropic streaming is unaffected — Anthropic includes usage in the streaming protocol by default.
Prompt caching
Both providers support prompt caching, but they expose it differently. Meilynx captures and prices both shapes correctly when the SDK is at v0.4.0+ and the server is at the matching deployment.
OpenAI: automatic
OpenAI auto-caches prompts ≥ 1024 tokens — no markers required. To maximize
cache hits, structure prompts with the stable prefix first (system prompt,
fixed instructions, large reference docs) and the variable content last
(user input, current turn). The SDK reports the cached token count as
cachedInputTokens, which is a subset of promptTokens (not additive).
The server bills the cached portion at the model-specific cached rate (50%
for GPT-4o / o-series, 25% for GPT-4.1 / o4-mini, 10% for GPT-5 family).
Anthropic: explicit cache_control
Anthropic requires you to mark cache breakpoints in the request:
await anthropic.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
system: [
{
type: "text",
text: largeSystemPrompt,
cache_control: { type: "ephemeral" }, // ← cache the system prompt
},
],
messages: [{ role: "user", content: "..." }],
});The SDK reports the cache write/read counts as cacheCreationInputTokens and
cacheReadInputTokens (these are first-class buckets, not subsets of
promptTokens). The server applies the documented Anthropic multipliers
(1.25× base input for 5-minute writes, 0.10× for cache reads).
Wire format
All telemetry fields use camelCase on the wire. If you write custom
mx.track() calls instead of relying on auto-instrumentation, match these
exact keys — the server's first-class fields look them up by name and
silently fall through to attributes JSON if the key shape doesn't match:
| First-class field | Use for |
|---|---|
| promptTokens | Total input tokens (includes cached subset for OpenAI). |
| completionTokens | Total output tokens (includes reasoning subset for o-series / GPT-5). |
| cachedInputTokens | OpenAI cached subset. Not additive — already inside promptTokens. |
| reasoningTokens | OpenAI reasoning sub-count. Not additive — already inside completionTokens. |
| cacheCreationInputTokens | Anthropic cache write bucket. Separate from promptTokens. |
| cacheReadInputTokens | Anthropic cache read bucket. Separate from promptTokens. |
Usage patterns
Event + trace basics
Use correlationId, requestId, or sessionId to connect telemetry and outcomes.
client.track({
eventType: "rag.search.completed",
correlationId: "run-456",
sessionId: "sess-abc",
attributes: { latencyMs: 420 }
});Batching and flushing
The SDK buffers events and flushes automatically. Use flush() for deterministic delivery (e.g., request end) and
shutdown() to drain queues before process exit.
Error handling and retries
- Retries occur for HTTP 429 and 5xx responses with exponential backoff.
- 401/403 errors throw immediately with an auth hint.
Serverless and edge runtimes
In short-lived environments (Lambda, Cloudflare Workers, Vercel Edge), flush before the handler returns to avoid losing events:
// 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.
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.
Example script
npm run build
MX_BASE_URL=http://localhost:5000 MX_API_KEY=mx_live_... node examples/send-telemetry.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
