Dashboard →

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

ProviderClientStreamingCache tokensReasoning tokens
OpenAIChat Completions + Responses APIYes, bothReported, subset: subtractReported, subset: subtract
AnthropicMessages APIYes, deltas mergedSeparate fields: passthroughN/A
Google GeminigenerateContentYes, separate methodSubset: subtractSeparate field: do not subtract
AWS BedrockConverse / ConverseStream, InvokeModelYesAlready disjoint: do not subtractAlways null (billed inside output)
Azure OpenAIChat Completions + Responses APIYes, bothSame 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_tokens already. Passthrough, no subtraction. Subtracting would double-count them as missing.
  • AWS Bedrock (Converse): input = inputTokens as-is. It already excludes cache reads/writes (total input = inputTokens + cacheReadInputTokens + cacheWriteInputTokens). reasoning_tokens is always null. Claude thinking tokens are billed inside outputTokens on Bedrock.
  • Google Gemini: input = promptTokenCount − cachedContentTokenCount (cached is a subset, same direction as OpenAI), but output = candidatesTokenCount as-is; thoughtsTokenCount is 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

Subtracting cacheReadInputTokens 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

Subtracting thoughtsTokenCount 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:

azure.ts
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_tokens held 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.