# This LLM cost function looks right. Can you spot the bug?

> Source: <https://dev.to/samuel_ochaba_eb9c875fa89/this-llm-cost-function-looks-right-can-you-spot-the-bug-hh7>
> Published: 2026-11-09 20:39:57+00:00

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:

``` python
from decimal import Decimal

# Made-up prices, in USD per million tokens.
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](https://developers.openai.com/api/docs/guides/token-counting) 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](https://developers.openai.com/api/docs/guides/reasoning):

```
{
  "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:

``` python
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

# A made-up response in the shape of OpenAI's Responses API usage object.
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](https://aistudio.google.com/api-reference/generate-content), `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.
