Dashboard →

Reference

API reference

Every public export, with its signature, parameters, and a short example.

init(ingest_key, endpoint=DEFAULT_ENDPOINT, *, router_url=None, governance_mode='enforce', governance_fail_mode='closed', optimization=None, **kwargs) · new TokenMeterClient(config)

Python: a module-level singleton. Call once at process start. TypeScript: instantiate a TokenMeterClient and pass it explicitly into every wrap* call. There is no global singleton. Every option is listed under Config options.

TokenMeterConfig
interface TokenMeterConfig {
  apiKey: string;
  endpoint?: string;                // full batch URL, e.g. https://ingest.example.com/v1/events:batch
  flushIntervalMs?: number;         // default 2000
  flushBatchSize?: number;          // default 500
  bufferCapacity?: number;          // default 10_000
  fetchImpl?: typeof fetch;         // injectable for tests
  disableBackgroundFlush?: boolean; // see Frameworks → Serverless
  onError?: (err: unknown) => void;
  optimization?: OptimizationOptions; // see Optimization & topics
}

const tl = new TokenMeterClient({ apiKey: process.env.TOKEN_METER_KEY! });
wrap(client, *, model_map=None) · wrapOpenAI/wrapAnthropic/wrapGemini/wrapBedrock/wrapAzureOpenAI(client, tl, defaults?)

Python has one wrap() that duck-types the client shape and dispatches to the right provider internally. TypeScript exports one function per provider, each taking the TokenMeterClient instance explicitly plus optional default attribution tags applied to every call from that client.

wrapOpenAI
const openai = wrapOpenAI(new OpenAI(), tl);
const openai2 = wrapOpenAI(new OpenAI(), tl, { team: "growth" }); // defaults applied to every call

wrap() mutates and returns the same client instance. It is not a new object, and wrapping an already-wrapped client or method is a no-op.

OpenAI-compatible providers. wrapOpenAICompatible(client, tl, provider, defaults?) · tokenmeter.wrap_openai_compatible(client, provider) records an OpenAI client pointed at another provider's base URL under that provider: openrouter, xai, mistral, fireworks, together, deepinfra or vercel_gateway.

Anthropic on Bedrock or Vertex. wrapAnthropic(client, tl, defaults?, { provider }) records AnthropicBedrock calls as bedrock and AnthropicVertex calls as google automatically. Pass provider only when detection can't see the class (a subclass or a minified build).

Vercel AI SDK. tokenmeterMiddleware(tl, defaults?, options?), used with wrapLanguageModel. See Frameworks.

action(name, attrs, fn) · with tokenmeter.action(name, *, customer_id=None, feature=None, team=None, environment=None, project=None, member_ref=None, optimization=None):

Opens a named attribution scope. See Attribution for the full field table and propagation semantics. TypeScript's attrs takes customerRef, feature, team, environment, project, memberRef, tags and optimization. Python accepts customer_ref as an alias of customer_id.

action
await action("generate-report", { customerRef: "acme", feature: "reports" }, async () => {
  await openai.chat.completions.create({ ... });
});
current() -> Optional[TraceContext] · current(): ActionContext | undefined

The active context, or nothing if called outside any open action() scope.

current
const ctx = current(); // undefined if no action() scope is open
attach(ctx) -> Token · attach(ctx, fn): T

Restores a captured context on the far side of a hop context can't see across on its own (thread pool, queue). See the escape hatch example.

attach
attach(restoredCtx, () => {
  // fn body runs with restoredCtx as the active context
});
flush(deadline_s=2.0) -> None · client.flush(): Promise<FlushResult>

Best-effort flush of whatever is currently buffered. Never raises or rejects into host code. In Node, prefer the serverless helpers (flushOnWaitUntil, flushBeforeReturn, createServerlessClient) over calling flush() directly. See Frameworks → Serverless.

flush
const { sent, ok } = await tl.flush();
diagnose(ingest_key=None, endpoint=None) -> dict

The one deliberate exception to "never throws, never surprises": called on purpose, synchronously, specifically to make failures loud. Pings the Ingest API and prints (and returns) a clear verdict: { ok, status, message }. Python-only today; it exists to answer "why do I see zero data in my dashboard."

// Not yet exported from the TypeScript SDK. Check the Ingest API response
// from your own request logs, or call the Python SDK's diagnose() against
// the same key if you have both runtimes available.
preflight(provider, model, **kw) / with governed(provider, model, **kw): · new GovernanceClient(config).preflight(req) / .guard(req, call)

For calls you make to a provider directly (not through the Router), preflight asks the Router's control plane first whether the call is allowed, under the same model, provider, data-classification, token and cost policies a routed call gets. Usage captured inside governed() / guard() carries the decision id as tags.preflight_id, which is what lets the dashboard report the call as preflight-protected.

Preflight is on the request path

Unlike metering, preflight adds one HTTP round trip to the Router before your call (2 s timeout by default), and it can stop the call. In the default enforce mode a denial raises GovernanceDenied / GovernanceDeniedError; with the default fail mode closed, an unreachable Router raises GovernanceUnavailable / GovernanceUnavailableError. Use mode: "advisory" to only record decisions, or failMode: "open" to proceed unprotected when the Router can't be reached.
GovernanceClient
const gov = new GovernanceClient({
  apiKey: process.env.TOKEN_METER_KEY!,
  routerUrl: "https://router.example.com",
  mode: "enforce",   // or "advisory"
  failMode: "closed", // or "open"
  timeoutMs: 2000,
});

const reply = await gov.guard(
  { provider: "openai", model: "gpt-4o", content: prompt, maxOutputTokens: 800 },
  () => openai.chat.completions.create({ model: "gpt-4o", messages, max_tokens: 800 }),
);

The request takes provider, model and optionally team, environment (both default to the active action() scope), dataClassification, content (classified in memory by the Router; only the class labels are stored), estimatedInputTokens and maxOutputTokens. The decision returns allowed, preflightId, violations, dataClasses, token and cost estimates, and failedOpen. In TypeScript, withPreflight(id, fn) and activePreflightId() attach or read a decision id yourself.

optimization: { promptCaching?, maxCompletionDelayMs?, topicClassifier? } · optimization={...}

Opt-in measurements that feed the dashboard's Improvements. They never change the request you send. Set them on the client config or on any action() scope (inner scopes override outer ones; null turns an inherited setting off). Python uses the snake_case keys prompt_caching, max_completion_delay_ms and topic_classifier.

  • promptCaching: true fingerprints the leading system and developer instructions locally. Only an HMAC (keyed with your ingest key) and size buckets are sent, never the text.
  • maxCompletionDelayMs (1,000 to 86,400,000) declares how long this work could wait, so batch-pricing savings can be estimated.
  • topicClassifier is a synchronous function you supply. It receives the latest user message (first 2,000 characters) in-process and returns a topic key matching ^[a-z][a-z0-9_]{1,39}$, sent as tags.prompt_topic. The text never leaves your process. A classifier that throws, returns a Promise, or returns anything else is ignored. keywordTopicClassifier / keyword_topic_classifier is a built-in keyword-only one, and latestUserText / latest_user_text extracts the same text the SDK hands it.
optimization
const tl = new TokenMeterClient({
  apiKey: process.env.TOKEN_METER_KEY!,
  optimization: { promptCaching: true, topicClassifier: keywordTopicClassifier },
});
TokenMeterReadClient(api_key, base_url).list_daily_usage(start, end, *, limit=100, cursor=None) · new TokenMeterReadClient({ apiKey, baseUrl }).listDailyUsage(params)

Reads your workspace's daily metered usage back out, one row per day, provider, model and attribution combination, with token counters, request and error counts, and pricedCostUsd as an exact decimal string (check unpricedEventCount before treating it as complete). It needs a read key (tl_admin_…), not an ingest key, and baseUrl is your dashboard's address. Pages are cursor-based; errors raise ReadApiError with the HTTP status and code. Python also exports the class as ReadApiClient.

read API
const reader = new TokenMeterReadClient({ apiKey: process.env.TOKEN_METER_READ_KEY!, baseUrl: "https://app.example.com" });
let cursor: string | undefined;
do {
  const page = await reader.listDailyUsage({ start: "2026-10-01", end: "2026-10-31", cursor });
  for (const row of page.data) console.log(row.date, row.model, row.pricedCostUsd);
  cursor = page.pagination.nextCursor ?? undefined;
} while (cursor);
new RecallClient({ routerUrl, apiKey, sessionId, minToolResultBytes?, retentionDays? }) · TypeScript only

For agent loops that run through the Router. With Agent Recall on, the Router can archive large earlier tool results out of the conversation and leave a recall_id in their place. The model gets a tokenmeter_recall tool to fetch one back when it needs the exact contents; your loop runs that tool with execute(). sessionId must be stable for one agent run and not reused across runs; minToolResultBytes is 1,024 to 1,000,000 and retentionDays 1 to 30.

RecallClient
const recall = createRecallClient({ routerUrl: "https://router.example.com", apiKey: process.env.TOKEN_METER_KEY!, sessionId: runId });

const res = await openai.chat.completions.create({
  model: "gpt-4o",
  messages,
  tools: [...myTools, recall.tool],
  ...recall.requestMetadata(), // { recall: { enabled: true, session_id, ... } }
} as any);

for (const call of res.choices[0].message.tool_calls ?? []) {
  if (call.function.name === "tokenmeter_recall") {
    const item = await recall.execute(JSON.parse(call.function.arguments)); // { recallId, toolName, content, contentSha256 }
    messages.push({ role: "tool", tool_call_id: call.id, content: item.content });
  }
}
Config options

Both SDKs share the same defaults; names follow each language's convention, and a few options exist in only one of them.

TypeScriptPythonDefaultMeaning
apiKeyingest_keyrequiredYour tl_live_… ingest key.
endpointendpointIngest API defaultTypeScript takes the full batch URL (…/v1/events:batch); Python takes the ingest origin.
flushIntervalMsflush_interval_s2000 ms / 2.0 sBackground flush cadence. Note the unit differs by language.
flushBatchSizeflush_batch_size500Events per flush, below the ingest API's 1,000-per-batch limit.
bufferCapacitybuffer_size10,000Ring buffer size; past it the oldest events are dropped and counted.
optimizationoptimizationoffOpt-in measurements. See Optimization & topics.
disableBackgroundFlushn/afalseRequired on runtimes frozen between invocations. See Frameworks.
onErrorn/ano-opSwallow-and-report hook. Python logs to the tokenmeter logger instead.
fetchImpln/aglobal fetchInjectable fetch, mainly for tests.
n/arouter_urlunsetRouter origin. Enables tokenmeter.preflight() and governed().
n/agovernance_mode"enforce"Python preflight mode: "enforce" raises on a denial, "advisory" only records it.
n/agovernance_fail_mode"closed"When the router is unreachable: "closed" raises, "open" proceeds unprotected.
WireEvent: one row of a batch's events[] array

The disjoint usage counters plus the attribution and delivery metadata every wrapper produces, posted to POST /v1/events:batch with Authorization: Bearer tl_live_…. Nullable fields are never guessed. A null means the SDK genuinely doesn't know, not zero.

WireEvent
{
  "event_id": "01930b...",        // uuidv7, client-generated
  "occurred_at": "2026-08-22T10:03:11.482Z",
  "provider": "openai",            // openai | anthropic | google | bedrock | azure_openai | openrouter | xai | mistral | fireworks | together | deepinfra | vercel_gateway | other
  "model_raw": "gpt-4o-mini",
  "input_tokens": 812,
  "output_tokens": 143,
  "cache_read_tokens": null,
  "cache_write_tokens": null,
  "reasoning_tokens": null,
  "latency_ms": 641,
  "is_batch": false,
  "usage_complete": true,
  "error_code": null,
  "trace_id": "01930b...",
  "parent_span_id": null,
  "action": "generate-report",
  "customer_ref": "acme",
  "feature": "reports",
  "team": null,
  "environment": null,
  "project": null,
  "member_ref": null,
  "tags": {},
  "sdk_version": "0.2.0"
}

event_id and the batch's batch_id must be UUIDs, and occurred_at must be within the last 90 days and no more than 2 hours ahead; otherwise the event is held back and shown on the dashboard as held, not counted.

The SDK adds some tags on its own: preflight_id inside a governed call, prompt_topic and the optimization measurements when you opt in, and otel_trace_id / otel_span_id when an OpenTelemetry span is active. The OTel ids let ingest recognise the same call arriving through an OTel GenAI instrumentation as well, and keep only one copy.

Other exports
  • normalizeOpenAIUsage, normalizeAnthropicUsage, normalizeGeminiUsage, normalizeBedrock*Usage, normalizeAzureOpenAIUsage: the per-provider usage normalizers the wrappers use, exported so you can build events by hand. See Providers → Normalization arithmetic.
  • uuidv7(): the id generator used for event_id and trace ids.
  • SDK_NAME, SDK_VERSION, SCHEMA_HASH (TypeScript) and the Python package's __version__.