Guides
Providers
Every provider reports token usage in its own shape: different fields, different subtraction rules, different streaming semantics. This page is the honest map of what's supported and how each one normalizes.
Support matrix
| Provider | Client | Streaming | Cache tokens | Reasoning tokens |
|---|---|---|---|---|
| OpenAI | Chat Completions + Responses API | Yes, both | Reported, subset: subtract | Reported, subset: subtract |
| Anthropic | Messages API | Yes, deltas merged | Separate fields: passthrough | N/A |
| Google Gemini | generateContent | Yes, separate method | Subset: subtract | Separate field: do not subtract |
| AWS Bedrock | Converse / ConverseStream, InvokeModel | Yes | Already disjoint: do not subtract | Always null (billed inside output) |
| Azure OpenAI | Chat Completions + Responses API | Yes, both | Same rule as OpenAI (inferred) | Same rule as OpenAI (inferred) |
Wrappers: Python tokenmeter.wrap(client) for all of them. TypeScript wrapOpenAI, wrapAnthropic, wrapGemini, wrapBedrock, wrapAzureOpenAI, each (client, tl, defaults?). Anthropic's Bedrock and Vertex clients go through wrapAnthropic and are recorded as Bedrock and Google.
OpenAI-compatible providers. An OpenAI SDK client pointed at another OpenAI-compatible provider is wrapped under that provider, so it's priced at that provider's rates: wrapOpenAICompatible(client, tl, provider) in TypeScript, tokenmeter.wrap_openai_compatible(client, provider) in Python, where provider is one of openrouter, xai, mistral, fireworks, together, deepinfra or vercel_gateway. Usage is normalized with the OpenAI rules.
For OpenAI and Azure OpenAI streams, the wrapper sets stream_options.include_usage itself so the final usage frame is sent; you don't need to add it.
Bedrock breaks down further by call shape: Converse/ConverseStream is the primary, fully normalized path across every model family. InvokeModel has dedicated body parsers for the Anthropic and Amazon Nova shapes; every other family falls back to input/output-only counts (Python reads the x-amzn-bedrock-*-token-count response headers; the JS SDK has no such header fallback and records usage_complete: false instead). Mistral bodies carry no usage at all in either language.
Normalization arithmetic, per provider
Every provider is converted to the same disjoint shape: input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, each independently nullable, so that sum(class_count × class_price) is correct downstream by construction. The arithmetic that gets there is not the same across providers:
- OpenAI / Azure OpenAI:
input = prompt_tokens − cached_tokens,output = completion_tokens − reasoning_tokens. Both are subsets of the raw totals and must be subtracted out. - Anthropic: cache reads/writes are reported separately from
input_tokensalready. Passthrough, no subtraction. Subtracting would double-count them as missing. - AWS Bedrock (Converse):
input = inputTokensas-is. It already excludes cache reads/writes (total input =inputTokens + cacheReadInputTokens + cacheWriteInputTokens).reasoning_tokensis always null. Claude thinking tokens are billed insideoutputTokenson Bedrock. - Google Gemini:
input = promptTokenCount − cachedContentTokenCount(cached is a subset, same direction as OpenAI), butoutput = candidatesTokenCountas-is;thoughtsTokenCountis separate, not a subset.
The two inversions
OpenAI trains a reflex ("cache and reasoning tokens are subsets, subtract them out") that two providers directly invert. This is the single most common place to get provider normalization wrong.
Bedrock's inputTokens is already disjoint
SubtractingcacheReadInputTokens from Bedrock's inputTokens double-discounts. The field is already net of cache reads. Apply the OpenAI subtraction rule here and you undercount input spend.Gemini's thoughtsTokenCount is already separate
SubtractingthoughtsTokenCount from candidatesTokenCount undercounts. The two are already disjoint fields, not a total-and-subset pair. Apply the OpenAI subtraction rule here and reasoning-heavy traffic looks cheaper than it is.Azure: the deployment-name landmine
Azure's Responses API returns the deployment name in the model field, not the canonical model id. A deployment named prod-summarizer matches nothing in the pricing catalog. (Azure's Chat Completions API already returns the canonical id, so this only bites the Responses API path.) Pass a deployment → model id map when wrapping:
const azure = wrapAzureOpenAI(
new AzureOpenAI({ ... }),
tl,
{},
{ deploymentModelMap: { "prod-summarizer": "gpt-5-2025-08-07" } },
);Unmapped deployments pass through unresolved and land in the unpriced quarantine rather than being silently mispriced. The map is ignored for every other provider.
Fixture-verified vs. live-verified
Not every rule above has been checked against a live provider call. Some are inferred from provider documentation and pinned only against recorded fixtures. Stated plainly, per provider:
- OpenAI (proven live):
prompt_tokensheld constant at 2107 across cached/uncached calls in a real 5-call probe. - Anthropic: doc-stated, fixture-verified.
- Google Gemini: doc-verified against provider documentation; the streaming "last chunk is authoritative" behavior is community-observed, not doc-confirmed.
- AWS Bedrock: doc-verified disjoint shape for Converse; Nova cache-counter key casing is unconfirmed (AWS's own docs disagree) and probed defensively rather than guessed.
- Azure OpenAI: doc-inferred only, not empirically verified. Azure shares OpenAI's field shape, so the same subtraction rule is assumed to apply, but the live 5-call cache probe that proved OpenAI's rule has not been replayed against a real Azure deployment. Treat as provisional.
A provider integration isn't considered done until its CI fixture exists. Every rule above is pinned by a test against a recorded fixture, one file per provider, so a normalization regression fails CI rather than silently shipping.