OpenAI or AsyncOpenAI client in place and instruments both API surfaces:
client.chat.completions.create(...)client.responses.create(...)
Install
Wrap and call
Responses API
Same client, same wrap. The SDK detects the API shape from the usage payload and tags the event withapi: "responses" instead of api: "chat_completions".
Streaming
Streamed usage is handled for you. Chat Completions only reports usage on a streamed response when
stream_options.include_usage is set, so the SDK injects stream_options={"include_usage": True} whenever stream=True is passed and you have not set the flag yourself. If you set it explicitly, your choice wins. The Responses API rejects that option, so the SDK skips the injection there and reads usage from the terminal response.completed event instead.Events are emitted from the iterator’s
finally block, so a partially consumed or abandoned stream still bills what OpenAI reported. If the stream never reaches its final chunk, there is no usage payload to bill and nothing is emitted.Async
WrapAsyncOpenAI the same way. The SDK detects the async client and installs async wrappers on both surfaces.
Python
OpenAI client is already promise-based. The SDK preserves OpenAI’s APIPromise interface (including .withResponse() and .asResponse()) by proxying the returned promise rather than replacing it.
Per-call override
extra_lago / lago before forwarding, so OpenAI’s strict request validation never sees it.
What gets captured
Not exposed by either API:
cache_write, cache_write_5m, cache_write_1h. OpenAI auto-caches without surfacing creation counts, so you only ever see cache reads. image_input is not surfaced separately either.
On OpenAI everything is a subset: reasoning sits inside completion_tokens, and cache_read and audio_input sit inside prompt_tokens. Bill llm_input_tokens and llm_output_tokens as the totals — Bill in tokens shows the metric setup that handles this across providers.
Reasoning tokens populate automatically on o-series models (
o1, o4-mini, and friends). OpenAI was the first provider to expose the metric separately.Predicted Outputs are not billed separately.
accepted_prediction_tokens is a subset of completion_tokens and is skipped to avoid double-counting. rejected_prediction_tokens is extra cost beyond completion_tokens and is not surfaced as a canonical field. Unrecognized usage fields land in CanonicalUsage.extras for drift detection.Pricing
In price mode, OpenAI models are priced from OpenRouter’s public model list, per token, refreshed hourly on the background thread. No API key needed. See Billing for setup.What is and isn’t instrumented
wrap() patches exactly two methods — chat.completions.create and responses.create — on both the sync OpenAI and async AsyncOpenAI clients.
The .stream() helpers are covered. chat.completions.stream(...) and responses.stream(...) pass self.create into their stream manager, which resolves to the patched method, so they bill normally.
parse() is not. Both chat.completions.parse and responses.parse issue their own request instead of calling create, so the patch never sees them.
Other billable surfaces on the client are also unmetered: embeddings, images, audio, batches, videos, realtime, conversations, and beta. If you use any of them, build a CanonicalUsage yourself and pass it to sdk.emit().
Audio tokens are captured but not priced separately. They are reported as llm_audio_input_tokens and llm_audio_output_tokens, and in price mode they are billed at the model’s text rate. Providers often charge considerably more for audio — see Known limits.
Next steps
Configuration reference
Every config knob, in both SDKs.
Per-token pricing template
A complete plan built on OpenAI token metrics.