fix(http): prompt_tokens double-counted cache reads on the OpenAI wire - #154
Merged
Salil Das (sadlilas) merged 1 commit intoAug 26, 2026
Merged
Conversation
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>
Salil Das (sadlilas)
deleted the
fix/http-usage-cache-read-double-count
branch
August 26, 2026 20:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
extract_usage()computedprompt_tokensasinputTokens + cacheReadTokens + cacheWriteTokens, on the premise that the three are disjoint buckets. They are not —inputTokensalready 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
usageblock, so any OpenAI-compatible client doing its own cost arithmetic fromprompt_tokenssees close to twice the real prompt.Why the old premise was wrong
amplifier-core's
docs/contracts/PROVIDER_CONTRACT.mdspecifies thellm:responseusage payload:input_tokenscache_read_tokenscache_write_tokensProviders normalize to that shape:
amplifier-module-provider-anthropicaddscache_read_input_tokensintoinput_tokensbefore emitting.amplifier-module-provider-openaisubtracts onlycache_writeout of the vendor total, deliberately leaving cache reads inside.So
cacheReadTokenson the wire is a reported subset ofinputTokens, surfaced for visibility — not an addend.cacheWriteTokensis 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:The change
Also renames the local
new_inputtogross_inputso the name stops asserting the thing that was wrong, rewrites the docstring's three-bucket explanation, and corrects the matching comment on the accumulator inroutes/chat_completions.py.What is deliberately unchanged
cached_tokensstays ascacheReadTokens. It is a subset ofprompt_tokensunder both the old and the new formula — but only the new one gives it a correct whole to be a subset of.cost_usdis unaffected. It is summed from the per-callcostthe provider computed from the raw buckets, and was already right. Note that this is precisely why the bug was invisible:cost_usdandprompt_tokensin the same usage block did not reconcile, and only the token figure was wrong.Verification
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