AIFreeAPI Logo

Reasoning Token Billing: Stop Double Counting Gemini, OpenAI, and Claude

L
8 min readAPI Guides

The same addition can be correct for Gemini and wrong for OpenAI or Claude. Use the endpoint’s usage contract to count reasoning once, then reconcile requests before calculating money.

Token trays illustrate reasoning within an inclusive total and Gemini candidates and thoughts flowing into one output count.

Reasoning tokens are billed as output, but the output field does not mean the same thing across APIs. OpenAI and Claude include reasoning in their main output total. Native Gemini generateContent reports candidates and thoughts separately, so you add those two fields to obtain the ordinary text output quantity used for output pricing.

That difference is enough to inflate an otherwise plausible cost dashboard. A tracker that always adds a field named “reasoning” to a field named “output” can count the same work twice. A tracker that never adds reasoning can undercount native Gemini usage.

This guide covers the native response formats documented as of September 7, 2026. It provides a field map, arithmetic checks, and a small adapter you can test without calling an API. All examples below are synthetic unless explicitly identified as an official example. The focus is your usage ledger; an inflated local estimate does not establish that a supplier charged you twice.

Choose the formula by endpoint, not by model brand

Paths in this table are relative to the response's usage object: usage for OpenAI and Claude, usageMetadata for Gemini.

Native interfaceOutput quantity for ordinary output-token pricingReasoning detailHow to use it
OpenAI Responsesoutput_tokensoutput_tokens_details.reasoning_tokensAlready included; do not add it.
OpenAI Chat Completionscompletion_tokenscompletion_tokens_details.reasoning_tokensAlready included; do not add it.
Gemini generateContentcandidatesTokenCount + thoughtsTokenCountthoughtsTokenCountSeparate bucket; add it to candidates once.
Claude Messagesoutput_tokensoutput_tokens_details.thinking_tokens, when reportedAlready included; do not add it.

OpenAI explicitly describes both output totals as covering all model-generated tokens. Subtracting the reasoning detail gives non-reasoning output, not necessarily the exact token count of the text a person sees: formatting, channel information, and tool-related structure can also contribute. See the official output-token counting explanation.

Google's native UsageMetadata reference separates prompt, candidates, and thoughts. Its pricing tables describe the output rate as including thinking tokens. Therefore, for an ordinary text request with the relevant counts available, candidates plus thoughts is the output-rate quantity. totalTokenCount also includes input; it is not an output count, and you must not add thoughts to it again. Multimodal and tool usage can require additional categories and rates. See Gemini API pricing.

Claude's current thinking pricing documentation exposes output_tokens_details.thinking_tokens as a breakdown within output_tokens. Older SDKs, model responses, or gateways may omit that detail. Its absence does not make a reported output_tokens total unusable, and it does not mean the model used zero thinking tokens.

The table is deliberately specific about interfaces. Do not apply the Gemini row to Interactions, Live, a Vertex response, or a gateway merely because the model name contains “Gemini.” First establish the exact response format and the adapter version that produced your data.

Check the arithmetic before checking the price

OpenAI's reasoning guide provides this official Responses example:

json
{ "input_tokens": 75, "output_tokens": 1186, "output_tokens_details": {"reasoning_tokens": 1024}, "total_tokens": 1261 }

The output quantity is 1,186. Adding 1,024 produces 2,210, an overstatement of 1,024 tokens in the local tracker. The reconciliation is 75 + 1186 = 1261. The remaining 1186 - 1024 = 162 is non-reasoning output; it is not a promise that retokenizing the displayed answer will yield 162.

For comparison, consider this synthetic native Gemini text usage:

json
{ "promptTokenCount": 200, "candidatesTokenCount": 300, "thoughtsTokenCount": 900, "totalTokenCount": 1400 }

Here the output quantity is 1,200: 300 + 900. Using candidates alone misses 900 tokens. Treating the 1,400-token total as output charges the input at the output rate. Adding thoughts to that total counts the thoughts twice. The useful consistency check for this simple example is 200 + 300 + 900 = 1400.

Claude's official thinking-pricing example reports 348 output tokens, including 312 thinking tokens. Its correct output quantity is 348, not 660. The 36-token difference is the non-thinking portion of the reported output total. A displayed thinking summary is not a token-by-token representation of the underlying thinking, so its text length cannot replace usage accounting.

If the applicable output rate is P dollars per million tokens, the output component is:

text
output_cost = output_quantity × P / 1,000,000

For example, at a hypothetical rate of $10 per million, OpenAI's 1,186-token example has an output component of $0.01186. The wrong 2,210-token quantity produces $0.02210. This illustrates the counting error; it is not a quoted price for an OpenAI model.

There is no universal “reasoning multiplier” to add afterward. Look up the actual model, request date, modality, and service tier. In particular, do not assume thinking is excluded from every Batch discount: Google's pricing page lists output rates including thinking under both Standard and Batch pricing.

Normalize once, and keep unknown values unknown

An accounting adapter needs two independent answers: the output total and the optional reasoning breakdown. Making both fields mandatory creates another bug: a missing breakdown can cause you to discard a perfectly valid inclusive total.

Two accounting cards contrast a known inclusive output total with an additive total awaiting a missing thoughts count.
Two accounting cards contrast a known inclusive output total with an additive total awaiting a missing thoughts count.

A useful normalized record keeps output_tokens and reasoning_tokens separate. The second is a diagnostic breakdown, never a second amount to add after normalization. Store null when a value is unknown. Alongside it, retain the raw usage, native interface name, adapter version, model, and request identity so you can explain or repair the calculation later.

This Python example accepts the native usage object and an explicit interface identifier. It only normalizes the output portion; it does not compute an invoice or interpret arbitrary gateway payloads.

python
def token_count(value): if value is None: return None if type(value) is not int or value < 0: raise ValueError("Token counts must be nonnegative integers") return value def detail(usage, container, field): value = usage.get(container) if value is None: return None if not isinstance(value, dict): raise ValueError("Token details must be an object") return token_count(value.get(field)) def normalize_output(interface, usage): if not isinstance(usage, dict): raise ValueError("Pass a native usage object; absence is unknown") if interface == "openai.responses": output = token_count(usage.get("output_tokens")) reasoning = detail(usage, "output_tokens_details", "reasoning_tokens") elif interface == "openai.chat_completions": output = token_count(usage.get("completion_tokens")) reasoning = detail(usage, "completion_tokens_details", "reasoning_tokens") elif interface == "claude.messages": output = token_count(usage.get("output_tokens")) reasoning = detail(usage, "output_tokens_details", "thinking_tokens") elif interface == "gemini.generateContent": candidates = token_count(usage.get("candidatesTokenCount")) reasoning = token_count(usage.get("thoughtsTokenCount")) output = ( None if candidates is None or reasoning is None else candidates + reasoning ) else: raise ValueError("Unsupported native interface") if output is not None and reasoning is not None and reasoning > output: raise ValueError("Reasoning exceeds inclusive output") return {"output_tokens": output, "reasoning_tokens": reasoning}

The explicit names prevent accidental provider-wide dispatch. The integer check also rejects booleans, numeric strings, fractions, and negative counts. Rejecting bad data preserves the signal that something is wrong; silently clamping a negative remainder would hide it.

These synthetic fixtures exercise the essential cases:

python
assert normalize_output("openai.responses", { "output_tokens": 1200, "output_tokens_details": {"reasoning_tokens": 900} }) == {"output_tokens": 1200, "reasoning_tokens": 900} assert normalize_output("gemini.generateContent", { "candidatesTokenCount": 300, "thoughtsTokenCount": 900 }) == {"output_tokens": 1200, "reasoning_tokens": 900} assert normalize_output("claude.messages", { "output_tokens": 1200 }) == {"output_tokens": 1200, "reasoning_tokens": None} assert normalize_output("gemini.generateContent", { "candidatesTokenCount": 300 }) == {"output_tokens": None, "reasoning_tokens": None}

The last case is intentionally conservative. With an additive format, a missing thoughts count leaves the sum unresolved. If the exact model and interface documentation establishes that omission means no thinking for that request, a separate, documented adapter rule can supply zero. Do not infer that rule from an absent property alone. An explicitly reported thoughtsTokenCount: 0 is different: the sum is known.

An empty usage object also remains unknown. A response with no usage object should be recorded as pending or unavailable before this helper is called. Neither case means a free request.

For a gateway, inspect its published usage semantics and raw response first. A compatibility layer could turn native Gemini's two buckets into an inclusive completion_tokens value. Adding the original thoughts count after that transformation would reintroduce the same bug. Save the native or gateway interface identity with the record, and apply exactly one normalization step.

Count consumed attempts, not telemetry events

Correct field arithmetic cannot fix a ledger that records one response three times. A stream, an SDK final-message callback, and an observability export can all describe the same consumed request.

Timeline diagram shows cumulative usage observations, a separately consumed retry, and pending final usage beside ledger records.
Timeline diagram shows cumulative usage observations, a separately consumed retry, and pending final usage beside ledger records.

Claude documents streaming usage as cumulative. If successive snapshots show 80 and 140 output tokens, the later count is 140; summing the snapshots yields a fictitious 220. Use the final authoritative usage or the SDK's final message. Claude's thinking breakdown is reported on the final message_delta when supported. See the official streaming guide.

Organize the ledger around an attempt record, then attach observations to it:

ObservationAccounting action
Repeated delivery of the same final usage eventUpdate the existing attempt idempotently; do not append another charge.
A later cumulative usage snapshot for the same attemptReplace the earlier snapshot according to that interface's event semantics.
A retry that reaches the provider as another consumed requestKeep a separate attempt, even if its prompt and answer match.
An interrupted stream with no final usageMark usage incomplete or pending; do not invent a zero or a final total.

Retain provider request or response identifiers where available, your own attempt identifier, and the event source. Scope identifiers to the relevant provider account and interface. Do not deduplicate by prompt text: two identical prompts can represent two legitimate consumed requests. Conversely, a single provider response observed by two log pipelines is still one response.

This also explains why “I received no answer” is not evidence of zero output usage. OpenAI documents that an incomplete reasoning response can consume the output budget before producing visible text. Retrying that request may create another separately consumed attempt. Resolve both attempts with the provider's available usage records rather than deleting the first because its displayed answer was empty.

Reconcile tokens before reconciling an invoice

Start with one discrepant request. Preserve its raw payload and compare the local output quantity with the formula for that exact interface. Then inspect the attempt ledger for duplicated events or omitted retries. Only after those checks should you apply prices.

A historical browser-use issue illustrates the first failure: adding reasoning_tokens to an already inclusive completion_tokens inflated local usage reporting. The issue was closed with a fix. It is a useful bug pattern, not evidence that the current project remains broken or that OpenAI billed the same tokens twice.

For monetary reconciliation, record the actual model returned, account, time interval, pricing effective date, and service tier. Separate input, cache-related categories, output, and any tool or storage charges using the provider's definitions. Google's promptTokenCount includes cached content, for example; cache counts need the same attention to inclusive totals and breakdowns that reasoning counts do.

Likewise, reasoning used again as input in a later request is a separate billing event. Claude's thinking documentation describes model-dependent preservation of previous thinking in context. Where that retained content is billed as input later, the earlier output charge and later input charge concern different requests. Removing one as a supposed duplicate would understate consumption.

If tokens match but dollars do not, investigate the rate selection and remaining charge categories. If raw usage is missing, label the estimate unresolved and use provider usage records to reconcile it. If the local record still adds a reasoning breakdown to an inclusive output total, fix that calculation first.

For a broader provider-selection discussion, see our Gemini, OpenAI, and Claude cost comparison. Use the providers' current pricing pages for rates applicable to your own requests. A reliable comparison begins with the same foundation as a reliable invoice check: one normalized output quantity per consumed attempt.