UsageTap SDK Reference

Complete reference for the @usagetap/sdk TypeScript/JavaScript SDK (v1.4.0).

Table of Contents


Installation

# Core SDK + OpenAI adapter
npm install @usagetap/sdk openai

# Optional: Anthropic adapter
npm install @usagetap/sdk @anthropic-ai/sdk

# Optional: OpenTelemetry exporter
npm install @usagetap/otel-exporter

Peer dependencies are optional — install only what you use:

Subpath Peer Dependency
@usagetap/sdk None
@usagetap/sdk/openai openai >= 4.0.0
@usagetap/sdk/anthropic @anthropic-ai/sdk >= 0.20.0
@usagetap/sdk/openrouter openai >= 4.0.0
@usagetap/sdk/express express >= 4.0.0
@usagetap/sdk/react react >= 18.0.0

Module formats: Ships dual ESM (.mjs) and CommonJS (.cjs).

// ESM
import { UsageTapClient } from "@usagetap/sdk";

// CommonJS
const { UsageTapClient } = require("@usagetap/sdk");

Requirements: Node.js >= 18.17


Quick Start

import OpenAI from "openai";
import { withMetering } from "@usagetap/sdk/openai";

const openai = withMetering(new OpenAI(), "cust_123");

// Existing calls stay the same and are metered automatically.
const result = await openai.chat.completions.create({
  model: "gpt-5.6-luna",
  messages: [{ role: "user", content: "Hello!" }],
});

withMetering preserves the provider client and finalizes usage, but it cannot invent an application-approved model fallback. Use the explicit UsageTapClient.withUsage() lifecycle when BLOCK or DOWNGRADE must control whether the provider is invoked or which model is selected.

Environment variables:

USAGETAP_API_KEY=utk-...       # Add the scopes used by this service
USAGETAP_BASE_URL=https://api.usagetap.com/ # Optional override
OPENAI_API_KEY=sk-...           # If using OpenAI
OPENROUTER_API_KEY=sk-or-...    # If using OpenRouter

Client Configuration

const client = new UsageTapClient({
  // Optional when using the standard environment variables/default endpoint
  apiKey?: string;             // Defaults to USAGETAP_API_KEY
  baseUrl?: string;            // Defaults to USAGETAP_BASE_URL, then production

  // Optional defaults
  defaultFeature?: string;     // Applied to all calls
  defaultTags?: string[];      // Applied to all calls

  // Network
  fetchImpl?: typeof fetch;    // Custom fetch (default: globalThis.fetch)
  headers?: Record<string, string>;  // Additional headers on every request
  retries?: {
    maxAttempts?: number;      // Default: 3
    baseDelayMs?: number;      // Default: 250
    maxDelayMs?: number;       // Default: 5000
    jitterRatio?: number;      // Default: 0.2
  };

  // Idempotency
  idempotencyGenerator?: () => string;  // Custom UUID generator
  autoIdempotency?: boolean;   // Default: true — auto-generate when missing

  // Observability
  onLog?: (entry: UsageTapLogEntry) => void;          // Logging callback
  onUsageMetric?: (event: UsageMetricEvent) => void;   // OTEL / metrics callback

  // Auth style
  useApiKeyHeader?: boolean;   // Use x-api-key instead of Authorization Bearer

  // Safety
  allowBrowser?: boolean;      // Allow browser usage (testing only, default: false)
  circuitBreaker?: {
    maxCallsPerRun: number;     // Local hard stop for calls sharing customerId + runId
    runInactivityMs?: number;   // Forget inactive runs (default: 60 minutes)
  };

  // Optional remote prompt compression provider
  tokenCompanyApiKey?: string;
  tokenCompanyEndpoint?: string;
  tokenCompanyModel?: string;           // Default: "bear-2"
  aggressiveness?: number;              // 0.0 to 1.0, default: 0.2
  tokenCompanyAppId?: string;           // Optional provider app identifier
  usageTapCompressionEndpoint?: string;          // Single-text endpoint override
  usageTapCompressionMessagesEndpoint?: string;  // Message/request endpoint override
});

The SDK automatically sends Accept: application/vnd.usagetap.v1+json and negotiates the canonical response envelope on every request.


Core Methods

Runaway circuit breaker

The minimal circuit breaker is an SDK-local hard cap on model-call attempts in one application workflow. Configure the cap once and include a stable runId on each call:

const client = new UsageTapClient({
  circuitBreaker: { maxCallsPerRun: 20 },
});
const run = { customerId: "cust_123", runId: crypto.randomUUID() };

try {
  for (;;) {
    const result = await client.meter(run, async () => callModel());
    if (result.done) break;
  }
} catch (error) {
  if (error instanceof UsageTapError &&
      error.code === "USAGETAP_CIRCUIT_OPEN") {
    return buildPartialResult(error.details);
  }
  throw error;
} finally {
  client.resetRun(run);
}

The SDK reserves a slot before call_begin, so an open circuit prevents the next paid provider request. Requests with the same idempotency key count once. canRunContinue(run) inspects the decision without consuming a slot, and resetRun(run) releases the local state when a workflow finishes.

This guard is process-local and deliberately adds no synchronous database dependency. Keep account-level UsageTap limits enabled for enforcement across multiple SDK processes.

runId is SDK-only metadata and is not sent to the UsageTap API.


Sampling

Sampling retains a policy-controlled fraction of complete model calls for compression analysis. When no local sampling policy is supplied, the SDK loads the policy saved on the Sampling page and caches it for up to five minutes. Configuration failures are fail-closed: model calls continue, but raw input is not sampled.

For the complete path from call-list opportunity through Production Sample collection to measured cost per passing task, see Benchmark Models on Production Tasks.

For supported providers, use withSampling():

import OpenAI from "openai";
import { withSampling } from "@usagetap/sdk/openai";

const openai = withSampling(new OpenAI());

await openai.responses.create(
  { model: "gpt-5.6", input: longPrompt },
  { usageTap: { customerId: "cust_123", feature: "assistant.answer" } },
);

withSampling() is also exported by @usagetap/sdk/anthropic and @usagetap/sdk/openrouter. To compose it with hosted compression, pass sampling: true to withCompression().

For custom providers, let the SDK load and evaluate the saved policy locally:

const selected = await client.shouldSampleAsync({
  customerId: "cust_123",
  feature: "assistant.answer",
  input: messages,
});

if (selected) {
  await client.captureSample({
    customerId: "cust_123",
    feature: "assistant.answer",
    provider: "custom",
    model: result.model,
    input: messages,
    output: result.output,
    usage: result.usage,
  });
}

The lower-level methods are:

  • getSamplingSettings({ forceRefresh? }): fetch and cache the canonical policy.
  • shouldSample(request, policy): synchronously evaluate an explicit local policy.
  • shouldSampleAsync(request, policy?): evaluate an override, configured local policy, or cached remote policy in that order.
  • decideSample({ samplingKey?, customerId?, feature?, inputTokens? | inputCharacters? }): obtain a deterministic metadata-only server decision.
  • captureSample(request): store a selected call and return its expiration time.

Set sampling: false on UsageTapClientOptions to disable automatic sampling, or provide a SamplingOptions object to override the saved policy locally. samplingSettingsCacheMs caps the local cache lifetime.


beginCall

Start a usage tracking session and retrieve customer entitlements.

const response = await client.beginCall({
  customerId: "cust_123",              // Required
  customerUserId: currentUser.id,       // Optional stable end-user ID
  customerUserName: currentUser.name,   // Optional display metadata
  customerUserEmail: currentUser.email, // Optional display metadata
  runId: "workflow_abc",               // Optional local circuit-breaker scope
  feature: "chat.send",               // Feature being accessed
  requested: {                         // Requested capabilities
    standard: true,
    premium: true,
    audio: false,
    image: false,
    search: true,
    reasoningLevel: "HIGH",            // "NONE" | "LOW" | "MEDIUM" | "HIGH"
  },
  idempotencyKey: crypto.randomUUID(), // Safe retries
  tags: ["production", "web-app"],     // Analytics tags
  customerName: "Acme Corp",           // Display name (new customers)
  customerEmail: "billing@acme.com",   // Email (new customers)
  stripeCustomerId: "cus_...",         // Link to Stripe
  batch: false,                        // Batch pricing (50% discount)
  pricingMode: "standard",            // "batch" | "standard"
});

// Access the response
const { callId, allowed, entitlementHints, meters, subscription } = response.data;

Use customerId for the customer account and customerUserId for the person or application user responsible for the call. Prefer a stable, non-PII user ID. All end-user fields are optional; name and email are display metadata and may be omitted.

Response fields:

Field Type Description
callId string Use in endCall
newCustomer boolean true if customer was just created
canceled boolean true if subscription is canceled
policy "NONE" | "BLOCK" | "DOWNGRADE" Quota enforcement policy
allowed AllowedEntitlements What the customer can use
entitlementHints EntitlementHints Model selection guidance
rateLimits RateLimitsSnapshot Optional rolling-call limit state
meters Record<string, MeterSummary> Per-meter usage snapshots
subscription SubscriptionSnapshot Plan and billing details
models Record<string, string[]> Model hints by tier
idempotency { key, source } Resolved idempotency key

promptCompress

Opt-in prompt compression for manual flows. Call it after beginCall and before the vendor request. beginCall remains unchanged and never compresses prompts automatically.

const begin = await client.beginCall({
  customerId: "cust_123",
  feature: "chat.send",
  requested: { standard: true },
  idempotencyKey: crypto.randomUUID(),
});

const compressed = await client.promptCompress({
  callId: begin.data.callId,
  input: {
    prompt: "Summarize this report for finance",
    data: [
      { account: "A-100", spend: 1200, status: "active" },
      { account: "B-200", spend: 850, status: "paused" },
    ],
  },
});

const response = await openai.responses.create({
  model: "gpt-5.6-luna",
  input: compressed.compressedInput,
});

await client.endCall({
  callId: begin.data.callId,
  modelUsed: "gpt-5.6-luna",
  inputTokens: response.usage?.input_tokens ?? 0,
  responseTokens: response.usage?.output_tokens ?? 0,
});

For exact spans that compatible compressors should leave unchanged, wrap the span with protectPromptText():

import { protectPromptText } from "@usagetap/sdk";

const compressed = await client.promptCompress({
  callId: begin.data.callId,
  input: `Keep ${protectPromptText("PLAN_ID_PRO_2026")} exact.`,
});

Request fields:

Field Type Description
callId string Required. The call returned by beginCall
input unknown Prompt string, message payload, or structured data to compress locally
text string Optional The Token Company-style alias for single-text compression
provider "heuristic" | "toon" | "thetokencompany" | "usagetap" Optional. Defaults to conservative local heuristic
model string Optional compression model override, e.g. "bear-2"
tokenCompanyModel string Optional The Token Company model override
aggressiveness number Optional compression aggressiveness from 0.0 to 1.0
tokenCompanyAppId string Optional The Token Company app identifier
usageTapCompressionModel string Optional UsageTap compression model override

Response fields:

Field Type Description
compressedInput unknown Send this to your LLM vendor
savedTokens number Approximate input tokens avoided
tokenSavingsRatio number Approximate token reduction from 0 to 1
savedCharacters number Character savings
techniques string[] Compression strategies applied

The default compressor is conservative: it normalizes whitespace, preserves fenced code indentation, minifies valid embedded JSON, and converts eligible JSON blocks to TOON when smaller. Counts use lightweight regex token estimation, not a model-specific BPE tokenizer. If compression or savings reporting fails, the SDK returns the original input with zero savings so your vendor call can continue.

For provider: "usagetap", manual promptCompress() and compressPromptInput() use the single-text The Token Company-compatible endpoint at https://compress.usagetap.com/v1/compress; aggressiveness is a single number from 0.0 to 1.0 for this path.

wrapOpenAI() and wrapAnthropic() also support opt-in automatic prompt compression:

const ai = wrapOpenAI(openai, client, {
  defaultContext: { customerId: "cust_123", feature: "chat.send" },
  promptCompression: {
    provider: "heuristic",
    roles: { user: true, tool: true },
    minTokens: 500,
  },
});

console.log(ai.promptCompression.totalTokensSaved);

With provider: "usagetap", wrappers use https://compress.usagetap.com/v1/messages/compress and can pass The Token Company-style per-role aggressiveness:

const ai = wrapOpenAI(openai, client, {
  defaultContext: { customerId: "cust_123", feature: "chat.send" },
  promptCompression: {
    provider: "usagetap",
    aggressiveness: { user: 0.5, system: 0.5, tool: 0.5 },
  },
});

User messages are compressed by default. System instructions, tool content, and assistant messages are skipped. If you pass a roles object, only the listed roles are compressed. The wrappers aggregate compression telemetry once per UsageTap call and keep raw prompt content out of UsageTap telemetry.

Lower-level helpers are available for custom pipelines:

const result = await client.compressPromptInput(input, {
  provider: "heuristic",
});

await client.recordPromptCompression({
  callId: begin.data.callId,
  promptCompression: {
    provider: result.provider,
    originalCharacters: result.originalCharacters,
    compressedCharacters: result.compressedCharacters,
    savedCharacters: result.savedCharacters,
    originalTokens: result.originalTokens,
    compressedTokens: result.compressedTokens,
    savedTokens: result.savedTokens,
    tokenSavingsRatio: result.tokenSavingsRatio,
    savingsRatio: result.savingsRatio,
    techniques: result.techniques,
  },
});

endCall

Report actual usage for a tracked call. Always call this, even on errors.

const response = await client.endCall({
  callId: "call_xyz789",               // Required: from beginCall
  providerUsed: "openai",              // Vendor that executed the call
  modelUsed: "gpt-5.6-sol",           // Model identifier
  reasoningEffort: "high",             // Actual/best-known execution effort
  reasoningEffortSource: "provider_response", // How effort was established
  reasoningMode: "enabled",            // Optional provider-specific mode
  reasoningBudgetTokens: 2048,          // Optional explicit thinking budget
  inputTokens: 512,                   // Prompt tokens
  responseTokens: 256,                // Completion tokens
  cachedInputTokens: 100,             // Cached prompt tokens (included in inputTokens)
  cacheWriteInputTokens: 25,          // Cache-write tokens (included in inputTokens)
  reasoningTokens: 0,                 // Reasoning tokens (o1/o3 models)
  searches: 1,                        // Web searches used
  audioSeconds: 0,                    // Audio processing seconds
  batch: false,                       // Batch pricing flag
  pricingMode: "standard",            // "batch" | "standard"
  error: undefined,                   // Set if call failed: { code, message }
  stripeCustomerId: "cus_...",        // Override Stripe customer
});

console.log("Cost:", response.data.costUSD);
console.log("Metered:", response.data.metered);

The OpenAI, Anthropic, OpenRouter, and OpenAI-compatible fetch wrappers populate execution metadata automatically when it is present in the provider request or response. Reasoning token counts never imply an effort level.

Response fields:

Field Type Description
callId string Echo of call ID
status "COMPLETED" | "FAILED" Provider outcome recorded by UsageTap
error { code, message } Normalized provider error when status is FAILED
providerUsed / modelUsed string Execution vendor and resolved model
reasoningEffort "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" Execution effort, distinct from the entitlement ceiling returned by beginCall
reasoningEffortSource string provider_response, provider_request, gateway_config, or model_default
costUSD number Final calculated cost after the selected pricing mode
costUsdNano string Optional high-precision cost representation
usage object Normalized input, cache-read, cache-write, output, and reasoning tokens
standardCostUSD number Cost before a vendor-batch discount
pricingMultiplier number 0.5 for batch or 1 for standard
metered object Usage that was counted against quotas
spendVelocity SpendVelocitySnapshot Aggregate-backed customer spend for current UTC hour/day
balances object Remaining quotas after this call

For gateway calls, reasoningEffort is the effective normalized value that was sent after provider/model capability mapping, not an unsupported requested value. Native mode and fixed-budget details are reported separately through reasoningMode and reasoningBudgetTokens when known.


withUsage

High-level helper that wraps the entire begin → vendor call → end lifecycle. Automatically calls endCall even on errors or aborts.

const result = await client.withUsage(
  {
    customerId: "cust_123",
    feature: "chat.send",
    idempotencyKey: crypto.randomUUID(),
    requested: { standard: true, premium: true },
  },
  async ({ begin, setUsage, setError }) => {
    // 1. Select model based on entitlements
    const model = begin.data.allowed.premium
      ? "gpt-5.6-sol"
      : begin.data.allowed.standard
        ? "gpt-5.6-luna"
        : null;
    if (!model) {
      throw new Error("Usage is not allowed for this customer");
    }

    try {
      // 2. Make your vendor call
      const response = await openai.chat.completions.create({
        model,
        messages: [{ role: "user", content: "Hello" }],
      });

      // 3. Report usage
      setUsage({
        modelUsed: model,
        inputTokens: response.usage?.prompt_tokens ?? 0,
        responseTokens: response.usage?.completion_tokens ?? 0,
        cachedInputTokens: response.usage?.prompt_tokens_details?.cached_tokens ?? 0,
        reasoningTokens: 0,
      });

      return response.choices[0].message.content;
    } catch (error) {
      // 4. Report errors
      setError({
        code: "VENDOR_ERROR",
        message: "Provider request failed",
      });
      throw error;
    }
  }
);

createCustomer

Create or retrieve a customer record. Fully idempotent — repeat calls return the existing snapshot.

const response = await client.createCustomer(
  {
    customerId: "cust_123",
    customerFriendlyName: "Acme Corp",  // Display name
    customerEmail: "billing@acme.com",  // Email for notifications
    stripeCustomerId: "cus_stripe123",  // Link to Stripe
  },
  {
    idempotencyKey: crypto.randomUUID(), // Optional request option
  },
);

console.log("New?", response.data.newCustomer);     // true on first call
console.log("Plan:", response.data.subscription.planName);
console.log("Entitlements:", response.data.allowed);

Tip: customerFriendlyName and customerEmail are highly recommended — they populate the customer profile and enable usage notification emails.


checkUsage

Query current usage status without creating a call session. Returns the same data as beginCall minus callId.

const status = await client.checkUsage({
  customerId: "cust_123",
});

console.log("Meters:", status.data.meters);
console.log("Allowed:", status.data.allowed);
console.log("Plan:", status.data.plan);
console.log("Balances:", status.data.balances);

Use this for dashboard widgets, pre-flight checks, or displaying quota status.


changePlan

Switch a customer to a different usage plan. Requires the customers:write API key scope.

const result = await client.changePlan({
  customerId: "cust_123",
  planId: "plan_premium_v2",
  strategy: "IMMEDIATE_RESET",
});

console.log("Success:", result.data.success);
console.log("New plan:", result.data.subscription.planName);

Strategy options:

Strategy Behavior
IMMEDIATE_RESET Switch now, reset all usage counters to zero
IMMEDIATE_PRORATED Switch now, prorate existing usage to new limits
AT_NEXT_REPLENISH Schedule change for next replenishment cycle

If AT_NEXT_REPLENISH is used, the subscription.pending field will indicate the scheduled change.


incrementCustomMeter

Track custom usage beyond standard LLM metrics — agent actions, document processing, API calls, etc.

const result = await client.incrementCustomMeter(
  {
    customerId: "cust_123",
    customerUserId: currentUser.id, // Optional stable end-user attribution
    meterSlot: "CUSTOM1",          // "CUSTOM1" or "CUSTOM2"
    amount: 5,                     // Positive number to decrement from quota
    feature: "agent_actions",      // Feature tracking
    tags: ["workflow_automation"], // Categorization
    metadata: {                    // Arbitrary metadata
      workflowId: "wf_abc123",
      actionType: "email_send",
    },
  },
  {
    idempotencyKey: "stable-background-job-key",
  },
);

console.log("Event ID:", result.data.eventId);
console.log("Remaining:", result.data.meter.remaining);
console.log("Blocked:", result.data.blocked);

Returns: Updated meter snapshot with remaining, limit, used, unlimited, and label fields.

Notes:

  • Custom meters must be enabled in the customer's usage plan
  • The SDK generates an idempotency key by default; pass a stable option when a background job can retry across processes
  • BLOCK policy throws an error when quota is exceeded
  • DOWNGRADE policy allows usage to continue past quota
  • Unlimited meters still record events for analytics

UsageTap Gateway

UsageTapClient.gateway exposes the OpenAI-compatible Gateway and native batch jobs without requiring the OpenAI SDK:

import { UsageTap } from "@usagetap/sdk";

const usageTap = new UsageTap();
const completion = await usageTap.gateway.chat.completions.create({
  model: "usagetap/standard",
  customerId: "cust_123",
  messages: [{ role: "user", content: "Hello" }],
});

The batch resource uses a consistent create, wait, and results lifecycle:

const submitted = await usageTap.gateway.batches.create({
  requests: jobs.map((job) => ({
    custom_id: job.id,
    body: {
      model: "usagetap/standard",
      messages: job.messages,
    },
  })),
});
const batch = await usageTap.gateway.batches.wait(submitted);
const results = await usageTap.gateway.batches.results(batch.id);

Available methods are gateway.models.list(), gateway.chat.completions.create(), and gateway.batches.create(), retrieve(), wait(), cancel(), and results(). The SDK generates the required batch idempotency key and parses NDJSON results into objects.

Context Summarization

UsageTapClient.summarization runs published profiles through the managed summarization API:

const summary = await usageTap.summarization.summaries.create({
  profile: "weekly-account-summary-abcd5678",
  wait: true,
  context: {
    id: "account-123",
    type: "account_history",
    content: accountHistory,
  },
});

Use summarization.batches.create() and .wait() for 1 to 20 source contexts. summarization.profiles.retrieve() loads the immutable prompt and settings for self-managed execution, and summarization.measurements.create() reports the resulting token savings.


OpenAI Integration

Reversible wrappers

Use withMetering for metering or withCompression for standalone prompt compression. Both preserve the provider client's methods and arguments, so the rest of the application stays unchanged.

import OpenAI from "openai";
import { withCompression, withMetering } from "@usagetap/sdk/openai";

const metered = withMetering(new OpenAI(), "cust_123");
const compressed = withCompression(new OpenAI(), {
  // Defaults to 1,000. Use 0 to always attempt compression.
  minContextTokens: 2_000,
});

// Optional settings are only needed when you want more control.
const both = withMetering(new OpenAI(), {
  customerId: "cust_123",
  feature: "chat.send",
  promptCompression: true,
});

Remove the wrapper or call .unwrap() to recover the original client. The same two functions are available from @usagetap/sdk/anthropic and @usagetap/sdk/openrouter.

withCompression first makes a fast estimate of the combined prompt context. It skips the compression call below 1,000 estimated tokens by default, avoiding an extra network round trip when likely savings are small. Override the cutoff with minContextTokens, or set it to 0 to always attempt compression. minTokens is separate and applies to individual text segments.

For advanced workloads, the wrapper passes the hosted Messages API settings without requiring a Runtime Compression Policy:

const openai = withCompression(new OpenAI(), {
  mode: "model_auto", // "model_force" or "deterministic"
  roles: {
    user: { aggressiveness: 0.2 },
    system: { aggressiveness: 0.1 },
  },
  latencyBudgetMs: 1_000,
  compactEmptyUserMessages: false,
  compactDuplicateUserTextParts: false,
  failOpen: true,
});

The zero-config withCompression(new OpenAI()) form remains the recommended starting point.

Wrappers are composable and remove one layer per .unwrap() call:

const openai = withMetering(
  withCompression(new OpenAI()),
  "cust_123",
);

Keep metering outside compression when the metered operation should include compression latency. Avoid enabling promptCompression on withMetering when a separate withCompression layer is already present.

wrapOpenAI

Wrap an OpenAI client for automatic usage tracking with zero boilerplate.

import OpenAI from "openai";
import { wrapOpenAI } from "@usagetap/sdk/openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

const ai = wrapOpenAI(openai, client, {
  defaultContext: {
    customerId: "cust_123",
    feature: "chat.send",
    requested: {
      standard: true,
      premium: true,
      search: true,
      reasoningLevel: "HIGH",
    },
  },
});

const completion = await ai.chat.completions.create(
  {
    model: existingModel,
    messages: [{ role: "user", content: "Hello!" }],
  },
  {
    usageTap: {
      idempotencyKey: crypto.randomUUID(),
    },
  }
);

Override per request:

await ai.chat.completions.create(
  { messages },
  {
    usageTap: {
      customerId: currentCustomer.id,
      customerUserId: currentUser.id, // Optional
      feature: "chat.assist",
      tags: ["beta"],
      requested: { standard: true, premium: true, reasoningLevel: "MEDIUM" },
    },
  }
);

Model selection: wrapOpenAI preserves the model supplied by the application. It does not invent a model mapping. Use withUsage() when BLOCK or DOWNGRADE must control whether the provider is called or which approved fallback is selected.

Supported methods:

  • ai.chat.completions.create() — chat completions (streaming & non-streaming)
  • ai.responses.create() — responses API

Streaming

Streaming responses are automatically instrumented. Use the exported helpers to pipe streams to clients:

Next.js App Router:

import { toNextResponse } from "@usagetap/sdk/openai";

export async function POST() {
  const stream = await ai.chat.completions.create(
    {
      messages: [{ role: "user", content: "Stream it" }],
      stream: true,
    },
    {
      usageTap: {
        customerId: "cust_123",
        idempotencyKey: crypto.randomUUID(),
      },
    }
  );

  return toNextResponse(stream, { mode: "text" });
}

Express:

import { pipeToResponse } from "@usagetap/sdk/openai";

app.post("/api/chat", async (req, res) => {
  const stream = await ai.chat.completions.create(
    { messages: req.body.messages, stream: true },
    { usageTap: { customerId: req.user.id } }
  );

  pipeToResponse(stream, res);
});

OpenRouter

OpenRouter uses the same reversible OpenAI-compatible wrapper:

import OpenAI from "openai";
import { withMetering } from "@usagetap/sdk/openrouter";

const openrouter = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY!,
});

const ai = withMetering(openrouter, "cust_123");

const completion = await ai.chat.completions.create({
  messages: [{ role: "user", content: "Hello from OpenRouter!" }],
  // Model selected via entitlements, using OpenRouter identifiers
});

begin.data.models surfaces OpenRouter-specific model identifiers for each tier.


Anthropic Integration

For the short path, import withMetering or withCompression from @usagetap/sdk/anthropic. They preserve anthropic.messages.create() and can be removed without changing downstream calls.

wrapAnthropic

Wrap an Anthropic client for automatic usage tracking and optional role-aware prompt compression.

import Anthropic from "@anthropic-ai/sdk";
import { wrapAnthropic } from "@usagetap/sdk/anthropic";

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const claude = wrapAnthropic(anthropic, client, {
  defaultContext: {
    customerId: "cust_123",
    feature: "chat.anthropic",
    requested: { standard: true, premium: true },
  },
  promptCompression: {
    provider: "heuristic",
    roles: { system: true, user: true, tool: true },
  },
});

const message = await claude.messages.create({
  model: "claude-3-5-haiku-latest",
  max_tokens: 512,
  system: "Long system prompt",
  messages: [{ role: "user", content: "Long user prompt" }],
});

wrapAnthropic() instruments messages.create(), preserves the supplied model, extracts usage.input_tokens, usage.output_tokens, and cache-read tokens when present, and exposes compression totals on claude.promptCompression. Compression understands Anthropic system, message text blocks, and tool_result blocks; assistant messages are skipped by default.


Express Middleware

Attach UsageTap context to all Express requests:

import express from "express";
import OpenAI from "openai";
import { UsageTapClient } from "@usagetap/sdk";
import { withUsage } from "@usagetap/sdk/express";

const app = express();
const usageTap = new UsageTapClient({
  apiKey: process.env.USAGETAP_API_KEY!,
  baseUrl: process.env.USAGETAP_BASE_URL!,
});

// Attach UsageTap to every request
app.use(withUsage(usageTap, (req) => req.user?.id || "anonymous"));

app.post("/api/chat", async (req, res) => {
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

  // Get a UsageTap-wrapped OpenAI client from the request
  const ai = req.usageTap!.openai(openai, {
    feature: "chat.assistant",
    requested: { standard: true, premium: true, reasoningLevel: "HIGH" },
  });

  const stream = await ai.chat.completions.create(
    { messages: req.body.messages, stream: true },
    { usageTap: { idempotencyKey: crypto.randomUUID() } }
  );

  // Pipes stream and finalizes usage automatically
  req.usageTap!.pipeToResponse(stream, res);
});

app.listen(3000);

React Hook

Build chat UIs with automatic usage tracking:

import { useChatWithUsage } from "@usagetap/sdk/react";

function Chat({ customerId, currentUser }: ChatProps) {
  const { messages, input, setInput, handleSubmit, isLoading, error } =
    useChatWithUsage({
      api: "/api/chat",          // Your server endpoint
      customerId,
      customerUserId: currentUser.id, // Optional hint; validate on the server
      feature: "chat.assistant",
    });

  return (
    <div>
      <div>
        {messages.map((m) => (
          <div key={m.id}>
            <strong>{m.role}:</strong> {m.content}
          </div>
        ))}
      </div>

      {error && <div className="error">{error.message}</div>}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Type a message..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? "Sending..." : "Send"}
        </button>
      </form>
    </div>
  );
}

The hook works with any server route that uses the UsageTap SDK.


wrapFetch (Minimal Integration)

The smallest possible integration — wraps fetch and instruments OpenAI calls automatically:

import OpenAI from "openai";
import { UsageTapClient, wrapFetch } from "@usagetap/sdk";

const usageTap = new UsageTapClient({
  apiKey: process.env.USAGETAP_API_KEY!,
  baseUrl: process.env.USAGETAP_BASE_URL!,
});

const wrappedFetch = wrapFetch(usageTap, {
  defaultContext: {
    customerId: "cust_123",
    feature: "chat",
    requested: { standard: true, premium: true },
  },
});

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
  fetch: wrappedFetch,
});

// Zero changes to your OpenAI code
const completion = await openai.chat.completions.create({
  model: "gpt-5.6-sol",
  messages: [{ role: "user", content: "Hello!" }],
});

Override context per request via headers:

await openai.chat.completions.create(
  { messages: [{ role: "user", content: "Hello!" }] },
  {
    headers: {
      "x-usagetap-customer-id": currentUser.id,
      "x-usagetap-feature": "chat.premium",
    },
  }
);

OpenTelemetry Exporter

Export UsageTap metrics to any OTLP-compatible backend (Datadog, Grafana Cloud, New Relic, etc.).

Installation

npm install @usagetap/otel-exporter

Setup

import { createOtelExporter } from "@usagetap/otel-exporter";
import { UsageTapClient } from "@usagetap/sdk";

const otel = createOtelExporter({
  endpoint: "https://otel-collector.example.com:4318/v1/metrics",
  headers: { "Authorization": "Bearer your-otel-key" },
  serviceName: "my-app",
  serviceVersion: "1.0.0",
  resourceAttributes: { "deployment.environment": "production" },
  exportIntervalMs: 60000,  // Default: 60s
});

const usageTap = new UsageTapClient({
  apiKey: process.env.USAGETAP_API_KEY!,
  baseUrl: process.env.USAGETAP_BASE_URL!,
  onUsageMetric: otel.send,  // Wire up metrics export
});

// On application shutdown:
await otel.shutdown();

Configuration

Option Type Default Description
endpoint string Required OTLP HTTP endpoint
headers Record<string, string> {} Auth and custom headers
serviceName string "usagetap" OTEL service name
serviceVersion string "0.1.0" Service version
resourceAttributes Record<string, string> {} OTEL resource attributes
exportIntervalMs number 60000 Export interval in ms

Exported Metrics

Metric Type Description
usagetap.calls Counter Total calls tracked
usagetap.input_tokens Counter Input tokens consumed
usagetap.output_tokens Counter Output tokens generated
usagetap.cached_tokens Counter Cached tokens (prompt cache hits)
usagetap.reasoning_tokens Counter Reasoning tokens used
usagetap.cost_usd Counter Cost in USD
usagetap.searches Counter Web searches performed
usagetap.audio_seconds Counter Audio processing seconds
usagetap.custom_meter Counter Custom meter increments

All metrics include attributes: customer_id, feature, model, tags.

Platform Examples

Datadog:

const otel = createOtelExporter({
  endpoint: "https://http-intake.logs.datadoghq.com/api/v2/otlp/v1/metrics",
  headers: { "DD-API-KEY": process.env.DD_API_KEY! },
  serviceName: "my-ai-app",
});

Grafana Cloud:

const otel = createOtelExporter({
  endpoint: "https://otlp-gateway-prod-us-central-0.grafana.net/otlp/v1/metrics",
  headers: {
    Authorization: `Basic ${Buffer.from(
      `${process.env.GRAFANA_INSTANCE_ID}:${process.env.GRAFANA_API_KEY}`
    ).toString("base64")}`,
  },
  serviceName: "my-ai-app",
});

New Relic:

const otel = createOtelExporter({
  endpoint: "https://otlp.nr-data.net:4318/v1/metrics",
  headers: { "api-key": process.env.NEW_RELIC_LICENSE_KEY! },
  serviceName: "my-ai-app",
});

Embeddable Widgets

Display customer usage data on any webpage with embeddable widgets.

HTML Data Attributes

<div class="usagetap-widget"
     data-api-key="ek-..."
     data-organization-id="org123"
     data-customer-id="cust456"
     data-type="usage"
     data-format="compact"
     data-refresh="60"
     data-show-refresh-button="true"
     data-page-size="20"
     data-width="600px"
     data-height="auto">
</div>
<script src="https://usagetap.com/widget.js"></script>

JavaScript API

UsageTap.createWidget('#container', {
  apiKey: 'ek-...',
  organizationId: 'org123',
  customerId: 'cust456',
  type: 'usage',              // "usage" | "calls" | "custom"
  format: 'compact',          // "compact" | "detailed"
  refreshInterval: 60,        // Auto-refresh in seconds
  showRefreshButton: true,    // Manual refresh button
  pageSize: 20,               // Items per page (detailed mode)
  width: '600px',
  height: 'auto',
});

Widget Types

Type Description
usage Overall usage summary with meter cards
calls Paginated call history list
custom Custom meter visualization

Widget Data API

Fetch widget data programmatically:

GET /embed/widget-data?organization_id=org123&customer_id=cust456&type=usage&format=compact
Authorization: Bearer ek-...

Query Parameters:

Parameter Required Default Description
organization_id Yes Organization ID
customer_id Yes Customer ID
type No "usage" "usage", "plan", or "calls"
format No "compact" "compact" or "detailed"
metrics No Comma-separated metrics to filter
debug No "1" to enable debug mode

Error Handling

The SDK throws UsageTapError for all API errors:

import { UsageTapError } from "@usagetap/sdk";

try {
  await client.beginCall({ customerId: "cust_123" });
} catch (error) {
  if (error instanceof UsageTapError) {
    console.error("Code:", error.code);
    console.error("Message:", error.message);
    console.error("Status:", error.status);
    console.error("Retryable:", error.retryable);
  }
}

Error Codes

Code Description Retryable
USAGETAP_AUTH_ERROR Invalid API key or unauthorized No
USAGETAP_BAD_REQUEST Invalid request parameters No
USAGETAP_RATE_LIMITED Too many requests Yes
USAGETAP_CIRCUIT_OPEN Per-run model-call cap reached No
USAGETAP_SERVER_ERROR Server-side error Yes
USAGETAP_NETWORK_ERROR Network failure Yes
USAGETAP_INVALID_RESPONSE Unexpected response format No
USAGETAP_END_CALL_ERROR Failed to finalize call No
USAGETAP_BROWSER_RUNTIME SDK used in browser without allowBrowser No

Retry Behavior

The SDK automatically retries on 429, 500, 502, 503, 504 with exponential backoff + jitter. Configure via retries option:

const client = new UsageTapClient({
  apiKey: "...",
  baseUrl: "...",
  retries: {
    maxAttempts: 5,       // Default: 3
    baseDelayMs: 500,     // Default: 250
    maxDelayMs: 10000,    // Default: 5000
    jitterRatio: 0.3,     // Default: 0.2
  },
});

Type Definitions

ReasoningLevel

type ReasoningLevel = "NONE" | "LOW" | "MEDIUM" | "HIGH";

LimitType

type LimitType = "NONE" | "BLOCK" | "DOWNGRADE";

RequestedEntitlements

interface RequestedEntitlements {
  standard?: boolean;
  premium?: boolean;
  audio?: boolean;
  image?: boolean;
  search?: boolean;
  reasoningLevel?: ReasoningLevel;
}

AllowedEntitlements

type AllowedEntitlements = Required<RequestedEntitlements>;

MeterSummary

interface MeterSummary {
  remaining: number;      // Always numeric (check unlimited flag instead)
  limit: number | null;
  used: number;
  unlimited: boolean;     // true = unbounded; remaining is informational
  ratio: number | null;   // remaining/limit (0-1), null when unlimited
  label?: string;
}

Note: remaining is always a number. The unlimited boolean flag indicates unbounded meters (not remaining === null).

SubscriptionSnapshot

interface SubscriptionSnapshot {
  id: string;
  usagePlanVersionId: string;
  planName: string;
  planVersion: string;
  limitType: LimitType;
  reasoningLevel: ReasoningLevel;
  lastReplenishedAt: string;      // ISO 8601
  nextReplenishAt: string;        // ISO 8601
  subscriptionVersion: number;
  customerFriendlyName?: string;
  customerEmail?: string;
  stripeCustomerId?: string;
  pending?: {
    usagePlanVersionId: string;
    strategy: string;
    effectiveAt: string;
  };
}

EntitlementHints

interface EntitlementHints {
  suggestedModelTier: "premium" | "standard" | "none";
  reasoningLevel: ReasoningLevel;
  policy: LimitType;
  downgrade?: {
    reason: string;
    fallbackTier?: "premium" | "standard" | "none";
  };
  rateLimit?: {
    tier: string;
    windowHours: number;
    limit: number;
    used: number;
    remaining: number;
    blocked: boolean;
  };
}

SpendVelocitySnapshot

Returned from call_end as aggregate-backed customer spend telemetry. UsageTap does not enforce limits from this field yet. Aggregates are updated asynchronously, so currentCallCostUsd is included separately from the aggregate totals.

interface SpendVelocitySnapshot {
  currency: "USD";
  source: "usage_aggregate";
  generatedAt: string;           // ISO 8601
  customerId: string;
  currentCallCostUsd: number;
  windows: Record<"hour" | "day", {
    bucket: string;               // UTC bucket key
    windowMinutes: number;
    startedAt: string;           // ISO 8601
    endedAt: string;             // ISO 8601
    completedCostUsd: number;
    completedCalls: number;
  }>;
}

UsageMetricEvent

interface UsageMetricEvent {
  type: "call_end" | "custom_meter";
  timestamp: string;              // ISO 8601
  customerId: string;
  callId?: string;
  feature?: string;
  tags?: string[];
  metrics: {
    inputTokens?: number;
    cachedInputTokens?: number;
    responseTokens?: number;
    costUsd?: number;
    customMeterSlot?: "CUSTOM1" | "CUSTOM2";
    customMeterAmount?: number;
  };
  correlationId?: string;
}

PromptCompression

type PromptCompressionProvider = "heuristic" | "toon" | "thetokencompany" | "usagetap";

interface PromptCompressionRequest {
  callId: string;
  input?: unknown;
  text?: string;
  provider?: PromptCompressionProvider;
  model?: string;
  tokenCompanyModel?: string;
  aggressiveness?: number;
  /** @deprecated Use aggressiveness instead. */
  tokenCompanyAggressiveness?: number;
  tokenCompanyAppId?: string;
  usageTapCompressionModel?: string;
  /** @deprecated Use aggressiveness instead. */
  usageTapCompressionAggressiveness?: number;
}

interface PromptCompressionStandaloneOptions {
  provider?: PromptCompressionProvider;
  failOpen?: boolean;
  model?: string;
  tokenCompanyModel?: string;
  aggressiveness?: number;
  /** @deprecated Use aggressiveness instead. */
  tokenCompanyAggressiveness?: number;
  tokenCompanyAppId?: string;
  usageTapCompressionModel?: string;
  /** @deprecated Use aggressiveness instead. */
  usageTapCompressionAggressiveness?: number;
  signal?: AbortSignal;
}

type PromptCompressionMessageRole = "system" | "user" | "tool" | "assistant";
type PromptCompressionRoleAggressiveness =
  Partial<Record<PromptCompressionMessageRole, number>>;
type PromptCompressionMessagesAggressiveness =
  number | PromptCompressionRoleAggressiveness;

interface PromptCompressionMessagesOptions {
  provider?: "usagetap";
  failOpen?: boolean;
  aggressiveness?: PromptCompressionMessagesAggressiveness;
  /** @deprecated Use aggressiveness instead. */
  usageTapCompressionAggressiveness?: PromptCompressionMessagesAggressiveness;
  signal?: AbortSignal;
}

interface PromptCompressionTelemetry {
  provider: PromptCompressionProvider;
  originalCharacters: number;
  compressedCharacters: number;
  savedCharacters: number;
  originalTokens?: number;
  compressedTokens?: number;
  savedTokens?: number;
  tokenSavingsRatio?: number;
  savingsRatio: number;
  techniques: string[];
}

interface RecordPromptCompressionRequest {
  callId: string;
  promptCompression: PromptCompressionTelemetry;
}

interface PromptCompressionResult {
  input: unknown;
  compressedInput: unknown;
  provider: PromptCompressionProvider;
  originalCharacters: number;
  compressedCharacters: number;
  savedCharacters: number;
  originalTokens: number;
  compressedTokens: number;
  savedTokens: number;
  tokenSavingsRatio: number;
  savingsRatio: number;
  techniques: string[];
}

type AnthropicPromptCompressionRole = "system" | "user" | "tool" | "assistant";

interface AnthropicPromptCompressionOptions {
  enabled?: boolean;
  provider?: PromptCompressionProvider;
  roles?: Partial<Record<AnthropicPromptCompressionRole, boolean | {
    enabled?: boolean;
    provider?: PromptCompressionProvider;
    minTokens?: number;
    aggressiveness?: number;
    /** @deprecated Use aggressiveness instead. */
    tokenCompanyAggressiveness?: number;
    /** @deprecated Use aggressiveness instead. */
    usageTapCompressionAggressiveness?: number;
  }>>;
  /** Aggregate request threshold. withCompression defaults to 1,000. */
  minContextTokens?: number;
  /** Per-text-segment threshold. */
  minTokens?: number;
  failOpen?: boolean;
  tokenCompanyModel?: string;
  aggressiveness?: number | Partial<Record<AnthropicPromptCompressionRole, number>>;
  /** @deprecated Use aggressiveness instead. */
  tokenCompanyAggressiveness?: number | Partial<Record<AnthropicPromptCompressionRole, number>>;
  usageTapCompressionModel?: string;
  /** @deprecated Use aggressiveness instead. */
  usageTapCompressionAggressiveness?: number | Partial<Record<AnthropicPromptCompressionRole, number>>;
  tokenCompanyAppId?: string;
}

interface AnthropicPromptCompressionStats {
  history: PromptCompressionTelemetry[];
  failures: Array<{ callId: string; stage: "telemetry"; message: string; timestamp: number }>;
  calls: number;
  telemetryFailures: number;
  failOpenEvents: number;
  totalTokensSaved: number;
  totalCharactersSaved: number;
  tokenSavingsRatio: number;
  savingsRatio: number;
}

OpenAIPromptCompressionStats exposes the same aggregate counters plus telemetryFailures and failOpenEvents, with OpenAI-specific operation names in history.

function protectPromptText(text: string): string;

Last Updated: July 29, 2026 SDK Version: 1.4.0 OTEL Exporter Version: 0.1.1