OpenAI's reasoning models bill you for thinking you never see. So it seems careful to add that thinking to your cost tracking, like this:
from decimal import Decimal
INPUT_RATE = Decimal("0.50")
OUTPUT_RATE = Decimal("2.00")
def estimate_cost(usage):
"""Estimated USD cost of one OpenAI Responses API call."""
reasoning = usage["output_tokens_details"]["reasoning_tokens"]
billed_output = usage["output_tokens"] + reasoning
return (usage["input_tokens"] * INPUT_RATE
+ billed_output * OUTPUT_RATE) / 1_000_000
It runs, and it returns a believable number. It's also wrong for every response that used reasoning. Have a guess at why before you read on.
OpenAI's Responses API reports everything the model generates as output_tokens. Its token counting guide says: "Reported output token usage includes all tokens generated by the model, not only the text visible in a response." The reasoning_tokens value under output_tokens_details breaks that number down: it tells you how many of the output tokens were reasoning. So estimate_cost counts the reasoning twice.
You can see it in the example response in OpenAI's reasoning guide:
{
"input_tokens": 75,
"input_tokens_details": { "cached_tokens": 0 },
"output_tokens": 1186,
"output_tokens_details": { "reasoning_tokens": 1024 },
"total_tokens": 1261
}
The total is 75 + 1,186 = 1,261, so the 1,024 reasoning tokens are already inside the 1,186. Add them again and your log says 2,285 tokens for a response that reported 1,261. The same guide confirms the billing: "While reasoning tokens are not visible via the API, they still occupy space in the model's context window and are billed as output tokens."
Use output_tokens as it is. Here's the fixed function, with a made-up response to run both versions on:
def estimate_cost_fixed(usage):
"""Same estimate; output_tokens already includes the reasoning tokens."""
billed_output = usage["output_tokens"]
return (usage["input_tokens"] * INPUT_RATE
+ billed_output * OUTPUT_RATE) / 1_000_000
usage = {
"input_tokens": 1200,
"output_tokens": 900,
"output_tokens_details": {"reasoning_tokens": 700},
}
for label, cost in [("Counted once: ", estimate_cost_fixed(usage)),
("Counted twice:", estimate_cost(usage))]:
print(f"{label} ${cost:.4f} per request, ${cost * 10_000:.2f} per 10,000")
Put both blocks in one file and run it:
Counted once: $0.0024 per request, $24.00 per 10,000
Counted twice: $0.0038 per request, $38.00 per 10,000
On this made-up response, the buggy version is 58% too high. Both numbers look perfectly plausible, which is why the mistake is easy to miss.
Google's Gemini API splits it differently. In the generateContent reference, thoughtsTokenCount is the "Number of tokens of thoughts for thinking models", and totalTokenCount is "prompt + tool-use prompt + thoughts + response candidates". The thinking tokens sit outside candidatesTokenCount, so if you treat candidatesTokenCount as all of the output, they go missing.
No single formula covers both APIs. Adding the reasoning field counts tokens twice on OpenAI, and leaving it out drops tokens on Gemini. Read each provider's definitions before you write the formula.
Some providers report cached tokens inside the input count. Gemini's promptTokenCount, for example, "includes the number of tokens in the cached content", so adding the cached count on top counts those tokens twice.
Did you spot the bug before the answer? And if you call more than one provider, how do you keep their usage fields straight? Tell me in the comments.