npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@meilynx/sdk

v0.5.0

Published

Meilynx SDK for telemetry and outcomes ingestion.

Readme

Meilynx JS SDK

CI npm License

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/sdk

Quickstart

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 URL
  • MX_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)
  • isStreaming is set to true automatically
  • latencyMs measures 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.mjs

Compatibility

  • Node.js 18+ (recommended)
  • Works in serverless runtimes that support node:crypto and fetch.
  • Server-side SDK is not intended for browsers. Use @meilynx/sdk/browser for 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 attributes before sending.

Docs

Roadmap

  • OpenTelemetry bridge for trace export
  • Edge runtime optimizations
  • Built-in redaction helpers

License

MIT