fix(zen): OpenAI cache-write tokens are part of input_tokens, not extra - #44229
fix(zen): OpenAI cache-write tokens are part of input_tokens, not extra#44229xyzs996 wants to merge 1 commit into
Conversation
normalizeUsage subtracted cached_tokens from input_tokens but left cache_write_tokens in. Both are subsets of input_tokens -- OpenAI's own sample is 2600 = 2000 read + 400 written + 200 neither -- so the written tokens stayed in inputTokens while also being reported as cacheWrite5mTokens. Two effects in handler.ts: the long-context threshold sums all four buckets, so it overshot the real prompt by exactly cache_write_tokens and tripped early; and inputCost priced those tokens at the input rate while cacheWrite5mCost priced them again at the cache-write rate. session.ts already subtracts both on the local side. Anthropic's helper is unchanged and must stay that way: its input_tokens genuinely excludes both cache buckets.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
The accounting model (four disjoint buckets partitioning
Otherwise clean, minimal, and well-tested. |
Issue for this PR
Refs #42910 (comment: #42910 (comment)). Independent of #44223, which fixes the local-side
experimentalOver200Kfallback.Type of change
What does this PR do?
normalizeUsagein the Zen OpenAI helper subtractscached_tokensfrominput_tokensbut leavescache_write_tokensin. Both are subsets ofinput_tokens, per OpenAI's own sample in the prompt-caching guide:2,000 + 400 + 200 = 2,600 — https://developers.openai.com/api/docs/guides/prompt-caching
So the written tokens stayed inside
inputTokensand were reported ascacheWrite5mTokens, andhandler.tsuses those buckets two ways:1. Long-context threshold trips early.
calculateCostsums all four buckets to pick the tier. Substituting, that sum is(input_tokens − cached) + cached + written=input_tokens + written— over the real prompt by exactlycache_write_tokens, every request. On a well-cached turnwritten ≈ inputTokens, so the sum runs near 2× the prompt and the gate trips at roughly half the real context. This matches what @Zaczero measured on a 302-request Luna burst in #42910:inputTokensandcacheWrite5mTokensdiffering by 3 on almost every row (that 3 is the "neither read nor written" remainder), 94 requests in the 200–272k band paying the high rate, +$2.53 on one afternoon.2. The same tokens are billed twice.
inputCost = modelCost.input * inputTokenscharges the written tokens at the input rate, thencacheWrite5mCost = modelCost.cacheWrite5m * cacheWrite5mTokenscharges them again. OpenAI rules that out explicitly: "Tokens written to the cache are billed at 1.25× the uncached input token rate." … "It is not an additional charge on top of another full input-token charge." Ongpt-5.6-luna($0.20/M input) a written token should cost $0.25/M and is billed $0.45/M — 1.8×, on every cached request, regardless of any threshold.One line fixes both.
session.tsalready does this correctly on the local side (it subtracts both, with a comment explaining why), so this brings Zen in line with it.Anthropic is deliberately untouched and must stay that way — Anthropic's
input_tokensgenuinely excludes both cache buckets, soanthropic.tsis right to pass it through and the four-bucket sum is already correct there. Google has no cache-write bucket. That asymmetry is why the fix belongs inopenai.tsrather than incalculateCost.How did you verify your code works?
packages/console/app/test/providerUsage.test.ts:input_tokens: 10,cached_tokens: 4,cache_write_tokens: 3) expectedinputTokens: 6; it now expects3. That existing expectation was pinning the bug, so it changes with the fix rather than being worked around.2600 / 2000 / 400→inputTokens: 200, plus an assertion that the four buckets sum back to exactly2600, which is the invarianthandler.tsrelies on for the tier.Screenshots / recordings
N/A
Checklist