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
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.
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 customfetch, 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.
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.
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 setsstream_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.
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
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
{ tokens, provider, model, type } and one of these non-zero categories:
- OpenAI-compatible and Gemini:
input,cached_input,output, andreasoning. - Anthropic:
input,cached_input,cache_write, andoutput.
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’stransaction_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.
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
Usenozle.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
parentTransactionIdorexternalCustomerId. Without either, it throws before sending. timestampis a number of Unix seconds. An ISO string returns400 Bad Request.- Returns
{ status: 'accepted', cost_event_id }. nozle.costEvents.createCostEventId()returns a UUIDv7. The SDK generates one whencostEventIdis omitted.- Posts to the Engine host (
baseUrl). - The same
costEventIdsent again is accepted with202and de-duplicated during processing. - Send one event per non-zero token type:
input,cached_input,cache_write,output, orreasoning.