Repository navigation
Anthropic server-side fallback: summed iteration usage makes the next step see about twice the real prompt size #60792
Description
Description
With Anthropic's server-side refusal fallback (server-side-fallback-2026-07-01), a turn that the requested model declines and a fallback model then serves comes back with one usage.iterations entry per attempt. Per Anthropic's docs, the top-level usage covers only the attempt that produced the returned message, and "tokens from different models are never summed into one field" (Refusals and fallback, billing and rate limits).
mapUsage sums every iteration into inputTokens, cacheReadInputTokens and cacheWriteInputTokens (anthropic-messages.ts#L1155-L1183). That sum is right for cost, because both attempts are billed. The problem is what happens to it next:
SessionUsage.tokensstores the summed fields as the assistant message'stokens(usage.ts#L11-L19).SessionCompaction.estimatePromptreadsinput + cache.read + cache.writefrom the latest assistant message as the measured prompt size (compaction.ts#L1104-L1127). The runner passes that estimate to the auto-compaction check and tooutputLimitfor the next step (llm.ts#L241-L270, model-request.ts#L91-L99).- Plugins that read message tokens as context size get the same figure.
So the step after a fallback sees roughly twice its real prompt size: the declined attempt read the same prompt as the attempt that answered.
contextTokens looks like it was meant for this case, but mapUsage only sets it when the last iteration has type === "message". On a fallback the last iteration is fallback_message, so it stays undefined. dev (e8238bae) has the same code.
Unit repro on v2.2.4, written like the fixtures in packages/ai/test/provider/anthropic-compaction.test.ts. The usage block is copied from the fallback example in Anthropic's docs:
const usage = {
input_tokens: 412, output_tokens: 264, cache_read_input_tokens: 0, cache_creation_input_tokens: 0,
iterations: [
{ type: "message", model: "claude-fable-5", input_tokens: 535, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
{ type: "fallback_message", model: "claude-opus-4-8", input_tokens: 412, output_tokens: 264, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
],
}
// fixedResponse(sseEvents({ type: "message_start", message: { usage } },
// { type: "message_delta", delta: { stop_reason: "end_turn" }, usage }, { type: "message_stop" }))
// LLMClient.generate(ai.request({ model: Anthropic.configure({ apiKey: "test" }).model("claude-opus-4-8"), prompt: "hello" }))Result: inputTokens: 947, nonCachedInputTokens: 947, contextTokens: undefined. The response itself reports input_tokens: 412.
The same thing in real sessions (Claude Opus 5.5 falling back to Opus 4.8, with prompt caching). Each value is input + cache.read + cache.write from the stored assistant message:
| Previous step | Step served by the fallback | Next step |
|---|---|---|
| 123,005 | 315,425 | 119,785 |
| 134,023 | 270,916 | 136,265 |
| 76,433 | 156,288 | 81,749 |
| 107,754 | 221,773 | 117,795 |
| 124,977 | 248,960 | 131,754 |
| 167,626 | 336,678 | n/a (see below) |
Most other steps served by the fallback model in the same sessions stay at about 1.0x. I take those to be turns that sticky routing sent straight to the fallback, with a single attempt. The raw iterations array is not persisted, so these pairs back up the repro but do not prove it on their own.
The last row is where I noticed it. A context-management plugin read 336,678 tokens from that message, 111% of its configured limit, while its own estimate of the prompt was about 120K. It went into its emergency path and the turn was interrupted (reported as cortexkit/magic-context#828). Inside OpenCode the inflated figure goes into the auto-compaction trigger and the output limit of the next step. With a 1M window I did not see either one fire, so I can't show that part happening.
Expected: whatever measures the prompt uses the attempt that produced the message (the last iteration, fallback_message included), and cost keeps using the per-attempt sum.
Plugins
@cortexkit/opencode-anthropic-auth 2.4.7 (sends the server-side fallback beta), @cortexkit/opencode-magic-context 0.49.33, oh-my-opencode-slim
OpenCode version
2.2.4 (also seen on 2.2.2)
Steps to reproduce
- Use an Anthropic model with server-side fallback enabled (
anthropic-beta: server-side-fallback-2026-07-01plusfallbacks). - Send a turn that the requested model declines and the fallback model answers.
- Compare that assistant message's
tokens(input + cache.read + cache.write) with the steps before and after it. It is about double.
Or run the unit fixture above.
Screenshot
No response
Operating System
Ubuntu 24.04 (aarch64)
Terminal
tmux
Activity
opencodejr commentedon Oct 31, 2026
Thanks for the detailed report and the fixture. I reproduced this on v2.2.4, and it still happens on the latest
dev(e8238ba).The script below feeds the fallback usage block from Anthropic's docs through
LLMClient.generate, then through core'sSessionUsage.tokens. On both versions:inputTokensandnonCachedInputTokenscome out as 947, andcontextTokensisundefined.tokens.inputis 947, soinput + cache.read + cache.write(the figureestimatePrompttreats as the measured prompt) is 947.The cause matches your analysis:
mapUsagesums every iteration and only setscontextTokenswhen the last iteration ismessage, notfallback_message. I only checked the usage mapping. I didn't run a full session to see the next step's auto-compaction or output limit react to the inflated number.Related: #46285 (the PR that added iteration summing for Anthropic usage).
Reproduction script (anthropic-fallback-usage.test.ts)
Run it:
bun anthropic-fallback-usage.test.tsdhaern commentedon Oct 31, 2026
Another case from today on v2.2.4, same session, which shows how far the inflated number travels.
One Opus 5.5 step at 04:56:35 UTC went through a server-side fallback (the stored message carries the fallback marker). The stored usage for that step is:
{"input":4,"output":1590,"reasoning":0,"cache":{"read":416217,"write":1439}}The step before it reported 217,121 input tokens. Magic Context read the next value as
inputTokens=417660against a 302,222 limit (138%) and went into emergency mode. Its emergency drop then refused to run because the "fixed floor ≈391071 already exceeds ceiling 250000", and that floor is derived from the same summed figure. The turn was cut twice in a row (04:58:01 and 05:00:21) withEmergencyFailClosedErrorbefore a third attempt went through at 58,845 tokens. The Magic Context side is tracked in cortexkit/magic-context#828.For anyone hitting this with
@cortexkit/opencode-anthropic-auth: the plugin addsfallbacks: "default"and theserver-side-fallback-2026-07-01beta to Opus requests by default. SettingOPENCODE_ANTHROPIC_AUTH_CONTENT_FILTER_FALLBACK=legacyin the server's environment stops it from sending them, so there are no extra iterations formapUsageto add up. A refusal then comes back as a normalcontent-filterfinish instead of being answered byclaude-opus-4-8in the same response. That avoids the trigger, but the summing inmapUsageis still wrong for every client that uses server-side fallback.