Skip to content

TypeScript SDK

Package metergraph on npm. Apache-2.0. Requires Node 18 or later and has no runtime dependencies. It ships ESM and CJS builds plus type declarations.

npm install metergraph

Every provider package is an optional peer dependency, so you install only the ones you already use: openai@>=4 <8, @anthropic-ai/sdk@>=0.30, @google/genai@>=1, ai@>=5 <8.

Source: github.com/PioneerSquareLabs/metergraphsdk.

SymbolKindOne line
initfunctionConfigure capture. Idempotent, never throws.
wrapfunctionPatch a provider client so its calls are captured.
wrapClientaliasSame function as wrap.
vercelAISDKMiddlewarefunctionCapture through the Vercel AI SDK middleware API.
trackfunctionName the calling function explicitly.
routefunctionName the unit of work, and optionally its business unit.
tracefunctionGroup calls under one logical trace.
withContextfunctionSet session ID and tags for a scope.
withSessionfunctionwithContext({ sessionId }) in one word.
withTagsfunctionwithContext({ tags }) in one word.
setSessionfunctionSet the session ID on the scope you are already in.
setTagsfunctionMerge tags into the scope you are already in.
setDefaultTagsfunctionProcess-wide tags inherited by new scopes.
modelForfunctionSticky per-session model selection.
recordOutcomefunctionSend a content-free outcome event.
flushfunctionAwait the queue draining, or the timeout.
shutdownfunctionFlush and stop every timer.
bindWaitUntilfunctionHand delivery to a serverless runtime’s waitUntil.
wrapHandlerfunctionFlush after a handler returns.
batchFirstfunctionOpt-in Batch API execution with a direct fallback.
BatchFirstIneligibleErrorclassThrown before any provider call.
DEFAULT_INGEST_URLconstantThe hosted ingest base URL.

Exported types: MetergraphOptions, ModelForOptions, OutcomeOptions, ContextOptions, RouteOptions, TraceOptions, TransportMode, WrapOptions, VercelAISDKMiddleware, VercelAISDKMiddlewareOptions, VercelAISDKSpecificationVersion, BatchFirstMetadata, BatchFirstPolicy, BatchFirstProvider, BatchFirstRequest, BatchFirstResult, BatchFirstSource, LateBatchInfo.

function init(options?: MetergraphOptions): void
interface MetergraphOptions {
token?: string;
ingestUrl?: string;
captureText?: boolean;
redact?: (text: string, kind: "request" | "response") => string;
appRoot?: string;
repository?: string;
skipFrames?: string[];
environment?: string;
disabled?: boolean;
transport?: "auto" | "background" | "buffered";
queueSize?: number;
batchSize?: number;
flushMs?: number;
configPollMs?: number;
configHardTtlMs?: number;
textMaxBytes?: number;
}

See Configuration for every default, range and clamp.

Returns void. Throws nothing. Failure is a console.warn and an uninstrumented process.

  • Idempotent. Later calls warn once with Metergraph init() was called more than once; the first configuration remains active. and return.
  • No token means not initialized. It warns Metergraph capture disabled: token and ingest URL are required once and returns without setting the initialized flag, so a later init() with a token still works.
  • disabled is terminal. disabled: true or METERGRAPH_DISABLED=1 sets the flag and returns.
  • No atexit equivalent. Node does not give the SDK a reliable exit hook, so call shutdown() or flush() yourself in a script or a short-lived job.
  • Repository identity. repository, then METERGRAPH_REPOSITORY, then an upward walk for .metergraph/config.json.
function wrap<T>(client: T, provider?: "openai" | "anthropic" | "google"): T
function wrap<T>(client: T, options: WrapOptions): T
type WrapOptions =
| { provider?: "openai" | "anthropic" | "google"; gateway?: never }
| { provider?: "openai"; gateway: "openrouter" };

Patches the supported methods in place and returns the same object. Calls init() first if you have not. wrapClient is an alias.

Returns the client. Always assign it: const client = mg.wrap(new OpenAI()).

Throws Error for an unsupported or contradictory gateway/provider pair, before any provider call and without echoing your URL or credential. Every other failure is fail-open and the client comes back unmodified.

Detection, when you pass nothing:

ConditionResult
client.models?.generateContent existsgoogle
client.chat or client.responses existsopenai
otherwiseanthropic
baseURL host is exactly openrouter.ai over HTTPS, on an OpenAI clientopenrouter gateway evidence, provider stays openai

The methods it patches:

ProviderPatchedEndpoint recorded
OpenAIchat.completions.createchat.completions
OpenAIchat.completions.parsechat.completions.parse
OpenAIbeta.chat.completions.parse (openai v4 only)chat.completions.parse
OpenAIresponses.create, beta.responses.createresponses
OpenAIresponses.streamresponses.stream
OpenAIresponses.parseresponses.parse
Anthropicmessages.createmessages
Anthropicmessages.streammessages.stream
Googlemodels.generateContentmodels.generate_content
Googlemodels.generateContentStreammodels.generate_content.stream

A seam that does not exist on your installed client version is skipped silently. The @google/genai JS client has no .aio namespace, unlike the Python client, so there is nothing extra to patch there.

function vercelAISDKMiddleware(
options: MetergraphOptions & { aiSdkVersion: 5 },
): VercelAISDKMiddleware<"v2">;
function vercelAISDKMiddleware<TVersion extends "v2" | "v3" | "v4" = "v3">(
options?: MetergraphOptions & { specificationVersion?: TVersion },
): VercelAISDKMiddleware<TVersion>;

Returns a LanguageModelMiddleware you pass to wrapLanguageModel. Calls init(options) when no runtime exists yet, so the same object carries both the Metergraph options and the version selector. The returned middleware has no runtime dependency on the ai package.

OptionUse it when
aiSdkVersion: 5You are on AI SDK 5. Maps to middleware protocol v2.
specificationVersionAdvanced. AI SDK 6 uses v3, and AI SDK 7 accepts v3 for compatibility. Defaults to "v3".

The two are mutually exclusive at the type level.

This is also the Vercel AI Gateway path. The middleware resolves a gateway/vercel/vercel-ai-gateway provider to the creator in the creator/model identifier, so a gateway call is attributed to the real publisher. See Vercel AI SDK and Vercel AI Gateway.

Context lives in an AsyncLocalStorage, so it follows await naturally and needs no executor wrapper.

function track<T extends Function>(fn: T): T
function track<T extends Function>(name: string, fn: T): T

Returns a wrapped function that runs its body with funcName set. Without a name it uses fn.name, or "<anonymous>".

Throws TypeError("track requires a function") if the second argument is not a function.

function route<T>(
name: string,
fn: () => T | Promise<T>,
options?: RouteOptions,
): Promise<T>
interface RouteOptions {
unit?: string;
unitCount?: number;
tags?: Record<string, unknown>;
captureText?: boolean;
}

Sets route on every call inside fn. When unit is given and unitCount is not, unitCount defaults to 1. Always returns a promise, even for a synchronous body.

captureText: false narrows content capture for the scope. It cannot widen it.

function trace<T>(
name: string,
fn: () => T | Promise<T>,
options?: TraceOptions,
): T | Promise<T>
interface TraceOptions {
traceId?: string;
parentSpanId?: string;
captureText?: boolean;
}

Entering a trace() inside an existing trace reuses the outer traceId and trace name unless you pass a different traceId. With no active trace and no traceId, one is generated as 16 random bytes in hex. Returns whatever fn returns, without forcing a promise.

function withContext<T>(options: ContextOptions, fn: () => T): T
interface ContextOptions {
sessionId?: string;
tags?: Record<string, unknown>;
}

Tag values are coerced with String(). Tags merge with the parent scope, inner wins.

function withSession<T>(sessionId: string, fn: () => T): T
function withTags<T>(tags: Record<string, unknown>, fn: () => T): T
function setSession(sessionId?: string): void

Sets the session ID on the scope you are already inside, using AsyncLocalStorage.enterWith. Outside a Metergraph scope it warns once with Metergraph setSession() requires an active Metergraph context; use withSession() or withContext(). and does nothing.

function setTags(tags: Record<string, unknown>): void

Merges tags into the scope you are already inside. Same rule and an equivalent warning naming setTags(), withTags() and withContext().

function setDefaultTags(tags: Record<string, unknown>): void

Replaces the process-wide default tag set, inherited by scopes entered afterwards and overridden key by key by scope-level tags.

function modelFor(routeName: string, options: ModelForOptions): string
interface ModelForOptions {
default: string;
sessionKey?: string;
}

Returns options.default on every failure path. Throws TypeError("modelFor requires options.default to be a non-empty string") when default is missing or empty. This is the one attribution-adjacent function in the SDK that throws on bad input rather than degrading.

function recordOutcome(routeName: string, options: OutcomeOptions): boolean
interface OutcomeOptions {
model: string;
taskCompleted: boolean;
sessionKey?: string;
feedbackScore?: number;
turnsToResolution?: number;
escalated?: boolean;
abandoned?: boolean;
editDistanceRatio?: number;
regenerationCount?: number;
eventId?: string;
}

Returns true if queued, false if rejected or the transport is not running. Never throws, and every rejection is silent, so check the return value while you wire it up.

FieldValidation
routeNameTrimmed, sliced to 512 characters, must be non-empty
modelTrimmed, sliced to 512 characters, must be non-empty
taskCompletedtypeof === "boolean"
sessionKeyFalls back to the scope’s session ID. Trimmed to 512 characters, must be non-empty
feedbackScoreFinite, -1 <= x <= 1
turnsToResolutionNumber.isInteger, 1 <= x <= 1000000
escalatedtypeof === "boolean"
abandonedtypeof === "boolean"
editDistanceRatioFinite, 0 <= x <= 1
regenerationCountNumber.isInteger, 0 <= x <= 1000000
eventIdDefaults to crypto.randomUUID(), with a timestamp-plus-random fallback. Sliced to 128 characters

Delivery mode is the thing that most often explains missing rows in serverless code.

ModeWhen it is chosenBehavior
backgroundDefault on a long-lived processA setInterval timer, unref’d so it does not hold the event loop open
bufferedAuto-selected when AWS_LAMBDA_FUNCTION_NAME or VERCEL is set, or navigator.userAgent is exactly Cloudflare-WorkersNo timer. Every enqueue schedules a delivery, handed to waitUntil when one is bound
autoThe default value of transportPicks one of the two above
function flush(timeoutMs?: number): Promise<boolean> // default 3000

Returns true when the queue is empty afterwards, false on timeout. Resolves true immediately when the SDK is not initialized.

function shutdown(): Promise<void>

Stops the config poller, clears the flush timer, flushes, and clears the runtime. Safe to call twice.

function bindWaitUntil(
value: { waitUntil(promise: Promise<unknown>): void } | ((p: Promise<unknown>) => void),
): void

Hands each scheduled delivery to the platform’s waitUntil, so the runtime keeps the invocation alive until the request completes. Accepts either the context object or the bare function. A no-op when the SDK is not initialized.

function wrapHandler<TArgs extends unknown[], TResult>(
handler: (...args: TArgs) => TResult | Promise<TResult>,
): (...args: TArgs) => Promise<TResult>

Returns a handler that awaits flush() in a finally block, so the queue drains whether the handler resolved or threw, and the original outcome is preserved. Use it where you have no waitUntil. See Serverless and short-lived jobs.

function batchFirst<TResult = unknown>(
client: unknown,
provider: "openai" | "anthropic" | "google",
request: BatchFirstRequest,
policy: BatchFirstPolicy,
): Promise<BatchFirstResult<TResult>>
interface BatchFirstPolicy {
deadlineMs: number;
acceptDuplicateProviderExecution: true;
allowDuplicateToolCallPlans?: boolean;
onLateBatchSettled?: (info: LateBatchInfo) => void;
}

Submits one request through the provider’s Batch API, waits up to deadlineMs, and falls back to a single direct call if the batch has not finished. Never enabled by wrap(), by capture defaults, or by an environment variable.

Throws BatchFirstIneligibleError before any provider call when:

ConditionMessage fragment
acceptDuplicateProviderExecution is not exactly truerequires policy.acceptDuplicateProviderExecution: true
deadlineMs is not a positive finite numberrequires a positive policy.deadlineMs
request.stream === truedoes not support streaming requests
The request has tools and allowDuplicateToolCallPlans is not truerequires policy.allowDuplicateToolCallPlans: true for requests with tools
No adapter for providerhas no adapter for provider "..."
The adapter rejects the requestrequest is not eligible for batch-first execution:

A submission failure rejects directly. It is not a fallback trigger, because no batch was created.

type BatchFirstSource = "batch" | "direct";
interface BatchFirstResult<TResult = unknown> {
source: BatchFirstSource;
result: TResult;
metadata: BatchFirstMetadata;
}
interface BatchFirstMetadata {
execution_mode: "batch_first";
deadline_ms: number;
batch_wait_ms: number;
batch_outcome: "completed" | "failed" | "expired" | "pending_at_deadline";
canonical_result: BatchFirstSource;
duplicate_provider_execution: boolean;
late_batch_completed: boolean; // always false at return time
late_batch_contained_tool_call_plan: boolean; // always false at return time
}
interface LateBatchInfo {
outcome: "completed" | "failed" | "expired";
contained_tool_call_plan: boolean;
}

The metadata field names are snake_case, and the units are milliseconds, which is where this differs from the Python dataclasses. A batch that settles after the fallback reaches you only through onLateBatchSettled, whose exceptions are swallowed. A batch that reports completed but whose result cannot be read is treated exactly like failed.

mg.DEFAULT_INGEST_URL // "https://d2xus7mp8zdv6t.cloudfront.net"
PropertyValue
QueueAn array bounded at queueSize. Full means drop, never block
Compressiongzip via CompressionStream once the JSON body passes 32 KiB. Where CompressionStream is absent the body is sent uncompressed
Max request body4 MiB. A larger batch is split in half and retried, down to a single row, which is dropped
keepaliveSet on buffered-mode requests up to 64 KiB
Retry401 and 403 invalidate a session token, or disable capture for the process when there is no session. 413 splits the batch. 400, 404, 413 and 422 drop the batch. Everything else backs off from 1 second, doubling to 60
Self-reportingEvery request carries meta.dropped and meta.transport_errors