Skip to main content
nozle.captureFetch() returns a fetch that records the token usage of every successful LLM response it carries. Pass it to any client that accepts a custom fetch: the OpenAI and Anthropic SDKs, Vercel AI SDK providers, LangChain, or your own calls. Requests and responses pass through unchanged, and prompts and completions are never sent to Nozle. captureFetch() and recordUsage() require @nozle-js/node 0.13.0. The client wrappers are available in earlier versions. Usage is recognised by the shape of the response, not by provider name: JSON responses and streams are both read. A stream is read in the background as your app reads it; usage seen before the stream ends or is aborted still counts.
Capture records usage events; they bill only when a metered Feature has the same event code. See Set up AI token billing for the Feature, charge and cost model to create first.

Capture any client

The same fetch works with other clients:

One client for every customer

Set the customer per request instead of per client. customerId can be a function, called with each request, or a request can name its customer with the X-Nozle-Customer-Id header. The header wins and is removed before the request reaches the provider; so is X-Nozle-Feature, which sets the feature property.
A request without a customer passes through and nothing is recorded.

CaptureFetchOptions

The second argument of captureFetch is the fetch to wrap; it defaults to the global fetch.

Usage you read yourself

For a client that cannot take a custom fetch, record the usage after the call. recordUsage rejects if the usage could not be recorded.

Client wrappers

wrapOpenAI and wrapAnthropic record the usage of one client’s create method instead. They are kept for existing integrations; captureFetch covers more APIs and providers.
From 0.13.1, wrapOpenAI and wrapAnthropic return a new view of the client and never change the client you pass in. Always call the returned client: calls on the original client record nothing. Code that ignored the return value and kept calling the original client recorded usage in 0.13.0 and earlier, and records nothing from 0.13.1.
Each view records usage once per call, for its own customerId, so a server can keep one provider client and wrap it per request. Views over the same client are independent, including concurrent calls, and wrapping an already wrapped client wraps its original, so wrappers never stack. Before 0.13.1, each wrap replaced create on the shared client, so one call recorded an event for every customer the client had been wrapped for.
The wrapped client keeps the full SDK surface. create returns the SDK’s own APIPromise or Stream, so withResponse(), asResponse(), chat.completions.parse(), messages.stream() / finalMessage(), stream.controller and toReadableStream() work as without the wrapper. Usage is recorded once, when your code reads the response. asResponse() leaves the body to you, so nothing is recorded for it. A provider error rejects to your code only and records nothing. openai and @anthropic-ai/sdk are optional peer dependencies of @nozle-js/node. Install the one you wrap.

OpenAI

OpenAI streaming

The wrapper sets stream_options.include_usage to true for streaming calls and reads the final usage-bearing chunk. It overwrites only include_usage: other stream_options keys are kept, and your own object is not mutated.
A stream is recorded when it is fully iterated. If you stop reading early (break or return()), or read the stream through tee(), the wrapper sends no Feature Event and no Cost Events, for OpenAI and Anthropic. Only chat.completions.create is wrapped for OpenAI. The Responses API is not wrapped.

Anthropic

For Anthropic streams, the wrapper reads input usage from message_start and output usage from message_delta events. messages.stream() and finalMessage() read the whole stream, so they record usage once.

WrapOptions

Event properties

The Feature Event preserves the total input and output counts. Detailed Cost Events use canonical properties { tokens, provider, model, type } and one of these non-zero categories:
  • OpenAI-compatible and Gemini: input, cached_input, output, and reasoning.
  • Anthropic: input, cached_input, cache_write, and output.
provider is the response format (openai, anthropic or gemini); provider_host is the host the request went to, so an OpenAI-compatible provider is told apart by its host. provider_host is sent by captureFetch only. Cost calculation remains server-side. Configure the ai_tokens Cost Event Meter and effective-dated provider/model/category Cost Rules to interpret these events.

Delivery behavior

The usage event’s transaction_id is llm-<format>-<response id>, for example llm-openai-chatcmpl-123, so the same response is never recorded twice. A response without an id gets a generated one. The same id is the parent_transaction_id of every detailed Cost Event, so parent and child events may arrive in any order. Recording runs without delaying the provider response, and a recording failure never replaces a successful provider result.
Without onError, wrapOpenAI and wrapAnthropic record failures silently: no log line and no unhandled rejection, including for a customer with no active subscription. Pass onError, or use recordUsage, when you need to know.
For workflows requiring durable delivery, explicit transaction IDs, or strict error handling, track manually through your queue instead of relying on the wrapper.

Manual cost events

Use nozle.costEvents.track() for providers without a wrapper and for cost-only tracking. A Cost Event never bills the customer.
  • Requires a secret sk_ key.
  • Requires parentTransactionId or externalCustomerId. Without either, it throws before sending.
  • timestamp is a number of Unix seconds. An ISO string returns 400 Bad Request.
  • Returns { status: 'accepted', cost_event_id }.
  • nozle.costEvents.createCostEventId() returns a UUIDv7. The SDK generates one when costEventId is omitted.
  • Posts to the Engine host (baseUrl).
  • The same costEventId sent again is accepted with 202 and de-duplicated during processing.
  • Send one event per non-zero token type: input, cached_input, cache_write, output, or reasoning.
An unknown costMeterCode is accepted with 202. The event is then marked uncosted with unknown_cost_event_meter and listed in the Uncosted queue, and its cost is not counted until you fix the meter and rate it again. Check the code against your Cost Event Meters.