Skip to content

fix(http): prompt_tokens double-counted cache reads on the OpenAI wire - #154

Merged
Salil Das (sadlilas) merged 1 commit into
mainfrom
fix/http-usage-cache-read-double-count
Aug 26, 2026
Merged

fix(http): prompt_tokens double-counted cache reads on the OpenAI wire#154
Salil Das (sadlilas) merged 1 commit into
mainfrom
fix/http-usage-cache-read-double-count

Conversation

@sadlilas

Copy link
Copy Markdown
Collaborator

Summary

extract_usage() computed prompt_tokens as inputTokens + cacheReadTokens + cacheWriteTokens, on the premise that the three are disjoint buckets. They are not — inputTokens already contains the cache reads, so the cached portion was counted twice.

On a cache-heavy turn (the normal case in an agent loop) that roughly doubles the reported prompt. The value flows to the terminal SSE chunk's usage block, so any OpenAI-compatible client doing its own cost arithmetic from prompt_tokens sees close to twice the real prompt.

Why the old premise was wrong

amplifier-core's docs/contracts/PROVIDER_CONTRACT.md specifies the llm:response usage payload:

Key Description
input_tokens Total input tokens — gross total (fresh + cache_read combined)
cache_read_tokens Tokens served from prompt cache
cache_write_tokens Tokens written to prompt cache (billed on top of gross)

Providers normalize to that shape:

  • amplifier-module-provider-anthropic adds cache_read_input_tokens into input_tokens before emitting.
  • amplifier-module-provider-openai subtracts only cache_write out of the vendor total, deliberately leaving cache reads inside.

So cacheReadTokens on the wire is a reported subset of inputTokens, surfaced for visibility — not an addend. cacheWriteTokens is the one bucket billed on top, so it is added.

The ecosystem's other consumer of this same event already encodes exactly this. From amplifier-module-hooks-streaming-ui:

All providers report input_tokens as the gross total — cache_read is already counted inside input_tokens, so adding it again would double-count. cache_write_tokens is the exception: cache creation cost is billed on top of gross and is NOT included in input_tokens.

return input_tokens + cache_create

The change

- prompt_total = new_input + cache_read + cache_write
+ # cache_read is deliberately absent: it is already inside gross_input.
+ prompt_total = gross_input + cache_write

Also renames the local new_input to gross_input so the name stops asserting the thing that was wrong, rewrites the docstring's three-bucket explanation, and corrects the matching comment on the accumulator in routes/chat_completions.py.

What is deliberately unchanged

  • cached_tokens stays as cacheReadTokens. It is a subset of prompt_tokens under both the old and the new formula — but only the new one gives it a correct whole to be a subset of.
  • cost_usd is unaffected. It is summed from the per-call cost the provider computed from the raw buckets, and was already right. Note that this is precisely why the bug was invisible: cost_usd and prompt_tokens in the same usage block did not reconcile, and only the token figure was wrong.

Verification

ruff check src/          All checks passed
ruff format --check      74 files already formatted
pyright src/             0 errors, 0 warnings

No test covered the arithmetic — the POC translator has no unit tests for extract_usage, which is how this survived. Worth a follow-up; not added here to keep the fix reviewable as a one-line correction.

🤖 Generated with Amplifier

extract_usage() computed prompt_tokens as inputTokens + cacheReadTokens +
cacheWriteTokens, on the premise that the three are disjoint buckets. They are
not. amplifier-core's docs/contracts/PROVIDER_CONTRACT.md specifies a provider's
input_tokens as the "gross total (fresh + cache_read combined)", and providers
normalize to that shape -- the Anthropic module adds cache_read_input_tokens
into input_tokens, and the OpenAI module subtracts only cache_write out of the
vendor total, leaving cache reads inside. cacheReadTokens is therefore a
reported SUBSET of the gross figure, surfaced for visibility, not an addend.

Adding it a second time roughly doubles prompt_tokens on a cache-heavy turn,
which is the normal case in an agent loop. The value flows to the terminal SSE
chunk's usage block, so any OpenAI-compatible client doing its own cost
arithmetic from prompt_tokens sees close to twice the real prompt.

prompt_tokens is now gross input + cache writes -- cache writes being the one
bucket billed on top of the gross total. That matches _compute_total_input in
amplifier-module-hooks-streaming-ui, which consumes the same event. cached_tokens
is unchanged and stays correct: it is a subset of prompt_tokens under both the
old and the new formula, but only the new one gives it a correct whole to be a
subset of.

Renames the local new_input to gross_input so the name stops asserting the thing
that was wrong, and corrects the matching comment on the accumulator in
routes/chat_completions.py.

No behavior change beyond the corrected total. cost_usd is unaffected: it is
summed from the per-call cost the provider computed from the raw buckets, and
was already right.

Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@sadlilas
Salil Das (sadlilas) merged commit 71da648 into main Aug 26, 2026
4 checks passed
@sadlilas
Salil Das (sadlilas) deleted the fix/http-usage-cache-read-double-count branch August 26, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant