Mapping Usage and Token-Count Objects Between APIs
9 min read · updated August 11, 2026
Every chat API returns how many tokens it charged you for. No two of them agree on what to call the numbers, which of them to include, or when to send them. A cost script written against one shape does not error on another — it reads undefined, coerces it to zero, and reports a month of free inference.
The four shapes
There are broadly four response envelopes in circulation, and the token counts live in a different place with different names in each. The distinction that matters most is prompt/completion versus input/output: those are the same two numbers under two vocabularies, and reading one name off the other object is the single most common way this goes wrong.
OpenAI Chat Completions usage.prompt_tokens
usage.completion_tokens
usage.total_tokens
OpenAI Responses usage.input_tokens
usage.output_tokens
usage.total_tokens
Anthropic Messages usage.input_tokens
usage.output_tokens
(no total field at all)
Google Gemini generateContent usageMetadata.promptTokenCount
usageMetadata.candidatesTokenCount
usageMetadata.totalTokenCountThree things in that table are worth stopping on. The object is called usage on three of the four and usageMetadata on the fourth, so a generic accessor that reaches for usage finds nothing on a Gemini response. Gemini uses camel case throughout while the other three use snake case, so even the names that mean the same thing do not match as strings. And Anthropic’s object has no total: if your schema requires one, you compute it, and you have to decide what goes into it before you can.
Note also that OpenAI’s two APIs disagree with each other. Moving from Chat Completions to the Responses API within the same vendor renames both counters. That is the migration that catches teams out, because nothing else about the credentials or the base URL changed and there is no reason to expect the cost path to have moved.
Cached, reasoning and tool tokens
The headline numbers are the easy part. The counts that decide whether your figure matches the invoice are in nested detail objects, and those have the least overlap of anything here.
- Cache reads. OpenAI reports them inside the prompt side, as
prompt_tokens_details.cached_tokenson Chat Completions andinput_tokens_details.cached_tokenson Responses — a subset of the prompt count, already included in it. Anthropic reports them as two sibling fields,cache_read_input_tokensandcache_creation_input_tokens, which are not included ininput_tokens. Gemini reportscachedContentTokenCount, which its reference describes as the cached portion of the prompt count. Same concept, three different answers to “do I add this or is it already counted?” - Reasoning tokens. OpenAI puts them in
completion_tokens_details.reasoning_tokens(Chat Completions) oroutput_tokens_details.reasoning_tokens(Responses), inside the output count. Google’s reference documentsthoughtsTokenCountas a separate field and says the total includes it. Anthropic bills extended thinking as ordinary output tokens, so there is no separate counter to read — which means a dashboard column labelled “reasoning” is simply empty for that provider rather than zero, and those are different facts. - Server-side tool use. Google documents
toolUsePromptTokenCount. Anthropic’s streaming reference shows aserver_tool_useobject insideusagecarrying counts such asweb_search_requests— a count of requests, not tokens, which will not sum with anything else on the page.
When usage arrives in a stream
On a non-streaming call the usage object is in the response body and there is nothing to think about. Streaming is where cost tracking silently stops working, because each API delivers the numbers at a different moment and one of them does not deliver them at all unless you ask.
OpenAI’s Chat Completions stream omits usage from every chunk by default. You opt in with stream_options: { include_usage: true }, after which a final chunk arrives with an empty choices array and the usage object populated. Code that iterates chunks and reads choices[0].delta without checking for an empty array will throw on exactly that chunk, which is why the option is so often added and then reverted.
Anthropic sends input tokens early and output tokens late. The message_start event carries a message object whose usage already has input_tokens and, on cached requests, the two cache fields. Output tokens arrive on message_delta, and Anthropic’s streaming reference is explicit that the counts in that event are cumulative — so you take the last one you saw, you do not add them up. Adding them up is a bug that scales with response length, which makes it look like a pricing change rather than an arithmetic error.
Gemini’s streaming endpoint sends a full response object per chunk, each of which may carry usageMetadata. Take the last. The general mechanics of these frames are covered in the streaming event mapping page; what matters here is only that “read usage from the response body” is not a thing you can do uniformly once streaming is on.
Total is not always a sum
It is tempting to normalise by discarding whatever total the provider sent and computing input plus output yourself. That produces a number that disagrees with the vendor’s own, because the vendor totals are not all the same quantity. Google’s reference describes totalTokenCount as prompt plus thoughts plus response, so it is larger than the two headline counters added together on a thinking model. OpenAI’s total_tokens is the sum of its two headline counters, with reasoning tokens already folded into the completion side. Anthropic has none, and if you build one you must decide explicitly whether cache-creation tokens belong in it — they are billed, and they are not in input_tokens.
The practical rule: never reconcile against a total. Reconcile against the priced quantities, one line per price. A billing model that has a row for cached input, uncached input, output and cache writes can be checked against an invoice. One that has a single token number cannot, and the discrepancy will be small enough to look like rounding right up until it is not. There is more on that failure in why your token count does not match your bill.
A normalised record that does not lie
The adapter that fixes this is small, and its only interesting design decision is how it represents a field the provider did not send. Use null, not 0. Zero is a measurement; null is the absence of one, and a dashboard that cannot tell them apart will average missing data into your cost per request.
type Usage = {
inputTokens: number | null; // uncached prompt tokens, as billed
outputTokens: number | null; // everything generated, reasoning included
cachedInputTokens: number | null; // read from cache, priced lower
cacheWriteTokens: number | null; // written to cache, priced higher; null where the
// provider has no such concept
reasoningTokens: number | null; // null means "not reported", not "none"
providerTotal: number | null; // whatever the provider called total, kept verbatim
};Keep providerTotal even though you will not price from it. When your computed figure and the vendor’s disagree, having both recorded turns an argument into a diff. And write the adapter so an unrecognised provider raises rather than returning an all-null record: a silent zero-cost provider is precisely the outcome this page exists to prevent.
The reference for each shape is the vendor’s own: Google publishes the generateContent reference including the full UsageMetadata field list, and Anthropic documents where usage appears in a stream in its streaming reference. Read those before trusting any table, this one included.