Skip to main content
nozle.wrap_http_client() makes the HTTP client your LLM SDK uses record the token usage of every successful response. It works with any client built on httpx or its fork httpx2, sync or async, including the OpenAI and Anthropic SDKs. Requests and responses pass through unchanged, and prompts and completions are never sent to Nozle. wrap_http_client(), capture_http_client(), capture_async_http_client() and record_usage() require nozle-sdk 0.13.0. The client wrappers are available in earlier versions. Usage is recognised by the shape of the response, not by provider name: an OpenAI-style usage (OpenAI chat completions, the Responses API and any OpenAI-compatible API such as OpenRouter, Groq, Azure OpenAI or vLLM), an Anthropic usage, or Gemini usageMetadata. JSON responses and streams are both read, including a stream your client closes at [DONE].
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

Pass the SDK’s own DefaultHttpxClient() (or DefaultAsyncHttpxClient() for AsyncOpenAI and AsyncAnthropic): each SDK expects the HTTP package it was built with, and wrap_http_client keeps that package. nozle.capture_http_client(customer_id=...) builds a new httpx.Client instead (module="httpx2" for httpx2), and capture_async_http_client an async one.

One client for every customer

customer_id can be a function, called with each httpx 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.
Recording runs in a background thread (with AsyncNozle, a task on the event loop) after the response has been read.

Usage you read yourself

record_usage raises if the usage could not be recorded.

Client wrappers

wrap_openai and wrap_anthropic record the usage of chat.completions.create (OpenAI) and messages.create (Anthropic) instead, for synchronous and asynchronous calls and streams. They are kept for existing integrations; wrap_http_client also covers the Responses API, Gemini and OpenAI-compatible providers.
From 0.13.1, wrap_openai and wrap_anthropic return a 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 customer_id, for sync and async clients and streams, so a server can keep one provider client and wrap it per request: wrap_openai(shared_openai, nozle, customer_id=request.team_id). Every other attribute is read from the original client, and isinstance(view, OpenAI) / isinstance(view, Anthropic) still holds. Views over the same client are independent, including concurrent async streams, 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.

OpenAI

OpenAI streaming

The wrapper sets stream_options.include_usage to true so the final stream chunk contains token counts. Only include_usage is overwritten; other stream_options keys you pass are kept.

Anthropic

Anthropic streams read input usage from message_start and output usage from message_delta events.

Wrapper arguments

Tracked properties

The Feature Event preserves total input and output counts. Detailed Cost Events use canonical properties { tokens, provider, model, type } and one of these non-zero categories:
  • OpenAI: input, cached_input, output, and reasoning.
  • Anthropic: input, cached_input, cache_write, and output.
Cost calculation remains server-side through the ai_tokens Cost Event Meter and effective-dated provider/model/category Cost Rules.

Failure behavior

The usage event’s transaction_id is llm-<format>-<response id> when the response has an id, so the same response is never recorded twice; otherwise one is generated. It is the parent_transaction_id of every detailed Cost Event. Parent and child events may therefore arrive out of order. Tracking failures do not replace a successful provider result; the SDK emits NozleTrackingWarning. The warning names only the exception type, not the cause, for example Nozle tracking failed after a successful LLM response (NozleAPIError). A customer with several active subscriptions makes every wrapped call emit it. Under python -W error, the SDK swallows the warning instead of raising it. The synchronous wrapper tracks inline: each wrapped call waits for one tracking request (about 255 ms observed). With cost_meter_code, it sends several requests per call. For async provider calls, synchronous Nozle tracking runs in a worker thread through asyncio.to_thread(). The wrapper does not expose subscription_id, so track() resolves the customer’s single active subscription. Use manual queued tracking when you need to choose the transaction identifier or coordinate delivery through your own queue.

Manual cost events

Use nozle.cost_events.track() for providers without a wrapper and for cost-only tracking. A Cost Event never bills the customer.
  • Requires an sk_ key.
  • Without parent_transaction_id or external_customer_id, it raises NozleValidationError before any request.
  • Returns {"status": "accepted", "cost_event_id": ...} after a 202.
  • The same cost_event_id sent twice returns 202 both times. Processing de-duplicates it.
An unknown cost_meter_code 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.