You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: cache write tokens always 0 for Anthropic models behind an OpenAI-compatible provider #36749
I route Anthropic models (on Bedrock) through an OpenAI-compatible gateway, configured with npm: "@ai-sdk/openai-compatible". opencode always records cache.write: 0 for those models, even though the model really does write a cache prefix and gets billed for it. Cache reads are recorded fine, so I end up with sessions showing millions of cache read tokens and zero cache writes, which can't be true: something had to write the prefix that is being read.
The cost ends up understated, since cache writes bill at 1.25x input but get counted as plain input at 1.0x.
Why it happens: the OpenAI usage shape has no cache-write field, so @ai-sdk/openai-compatible hardcodes cacheWrite: void 0 and passes the provider payload through as LanguageModelUsage.raw. Gateways in front of Anthropic do report the count there (cache_creation_input_tokens), but usage() in packages/opencode/src/session/llm/ai-sdk.ts only reads inputTokenDetails.cacheWriteTokens, and the fallbacks in Session.getUsage only cover native anthropic / vertex / bedrock metadata. So it lands on 0.
#18440 reported the same thing for LiteLLM/Bedrock and was closed by #22224, which made getUsage read usage.cacheWriteInputTokens. That works for providers that populate the field, but @ai-sdk/openai-compatible never does, so gateways using it are still broken. #35974 (DeepSeek) and #34022 look like the same root cause on the same code path.
I have a fix working locally (fall back to raw.cache_creation_input_tokens when the SDK gives nothing) and can send a PR if you want it.
OpenCode version
1.17.19
Steps to reproduce
Configure a provider with npm: "@ai-sdk/openai-compatible" pointing at a gateway that fronts an Anthropic model with prompt caching enabled (LiteLLM or similar).
Send a prompt long enough to be cached.
Check the assistant message tokens: cache.write is 0, and the written tokens show up as plain input instead.
This reproduces in V2 as well, in a completely different file, so the fix will need porting rather
than merging.
V1's analysis pointed at packages/opencode/src/session/llm/ai-sdk.ts. V2 rewrote the provider layer
into packages/ai/src/protocols/, and the same gap is now here:
This is the path every OpenAI-compatible provider takes. openai-compatible-chat.ts doesn't define
its own protocol — it reuses OpenAIChat.protocol end to end and overrides only the route id:
So litellm, vLLM, LM Studio, Ollama-compat and every gateway fronting Anthropic all land on that one mapUsage, which reads cache counts only from prompt_tokens_details.cached_tokens and prompt_tokens_details.cache_write_tokens. A gateway reporting cache_creation_input_tokens at the
top level of usage — exactly the payload in your report — matches neither key.
Why this fails silently, which I think is the more important half
The two protocols use opposite conventions, and only one of them can hide a missing field.
input_tokens there is exclusive of cache, so inputTokens = nonCached + cacheRead + cacheWrite. Drop
a field on that path and the total comes out too small — visible, and something downstream can catch.
OpenAI Chat subtracts: prompt_tokens is inclusive, so nonCached = prompt_tokens - (cached + cacheWrite). Drop cacheWrite here and the total stays perfectly correct — the tokens simply move
into the nonCached column. Nothing reconciles wrong, no invariant breaks, no total looks off. The
bill is the only place the difference is observable, which is why this keeps getting refiled per
provider (#35974, #37745, this one) instead of being caught once.
That also means your cost estimate is understated by more than the 1.25x you mentioned whenever the
provider prices cache writes above input, and it is silently understated in every case.
The fix needs no schema change
The alternate keys already survive decoding. Both the usage struct and prompt_tokens_details are
declared with a rest record:
Schema.StructWithRest(..., [Schema.Record(Schema.String, Schema.Unknown)]) keeps unknown keys on the
decoded value, so cache_creation_input_tokens is present on the object mapUsage already receives.
Reading it there is additive — no schema edit, no new plumbing.
One caveat worth stating, because it constrains where the fix can go: mapUsage sets providerMetadata: { openai: usage }, but that does not survive persistence. I checked 105,379
assistant rows in my database and none carries a providerMetadata key — the persisted message keeps
only agent, model, content, finish, cost, tokens, time, snapshot, error. So a downstream fallback
that reads provider metadata off the stored message cannot work; it has to happen inside mapUsage
while the raw payload is still in hand.
Scope
#35974 is the same function and the same gap, with DeepSeek's key names instead. Both are one patch.
Closing as a duplicate of #35974, which tracks cache-write tokens not being recorded for OpenAI-compatible providers. As noted there, both cases are the same gap in the usage mapping and need one fix. Please add your gateway/model details there.
Activity
padraig-myers-sh commentedon Aug 10, 2026
Is it possible to get an update on this?
Currently having to monkey patch opencode with the fix in #36752 to correct cost when routing via llmlite.
Zaczero commentedon Aug 18, 2026
This reproduces in V2 as well, in a completely different file, so the fix will need porting rather
than merging.
V1's analysis pointed at
packages/opencode/src/session/llm/ai-sdk.ts. V2 rewrote the provider layerinto
packages/ai/src/protocols/, and the same gap is now here:opencode/packages/ai/src/protocols/openai-chat.ts
Lines 582 to 600 in 39be159
This is the path every OpenAI-compatible provider takes.
openai-compatible-chat.tsdoesn't defineits own protocol — it reuses
OpenAIChat.protocolend to end and overrides only the route id:opencode/packages/ai/src/protocols/openai-compatible-chat.ts
Lines 17 to 23 in 39be159
So litellm, vLLM, LM Studio, Ollama-compat and every gateway fronting Anthropic all land on that one
mapUsage, which reads cache counts only fromprompt_tokens_details.cached_tokensandprompt_tokens_details.cache_write_tokens. A gateway reportingcache_creation_input_tokensat thetop level of
usage— exactly the payload in your report — matches neither key.Why this fails silently, which I think is the more important half
The two protocols use opposite conventions, and only one of them can hide a missing field.
Native Anthropic adds up:
opencode/packages/ai/src/protocols/anthropic-messages.ts
Lines 686 to 700 in 39be159
input_tokensthere is exclusive of cache, soinputTokens = nonCached + cacheRead + cacheWrite. Dropa field on that path and the total comes out too small — visible, and something downstream can catch.
OpenAI Chat subtracts:
prompt_tokensis inclusive, sononCached = prompt_tokens - (cached + cacheWrite). DropcacheWritehere and the total stays perfectly correct — the tokens simply moveinto the
nonCachedcolumn. Nothing reconciles wrong, no invariant breaks, no total looks off. Thebill is the only place the difference is observable, which is why this keeps getting refiled per
provider (#35974, #37745, this one) instead of being caught once.
That also means your cost estimate is understated by more than the 1.25x you mentioned whenever the
provider prices cache writes above input, and it is silently understated in every case.
The fix needs no schema change
The alternate keys already survive decoding. Both the
usagestruct andprompt_tokens_detailsaredeclared with a rest record:
opencode/packages/ai/src/protocols/openai-chat.ts
Lines 155 to 181 in 39be159
Schema.StructWithRest(..., [Schema.Record(Schema.String, Schema.Unknown)])keeps unknown keys on thedecoded value, so
cache_creation_input_tokensis present on the objectmapUsagealready receives.Reading it there is additive — no schema edit, no new plumbing.
One caveat worth stating, because it constrains where the fix can go:
mapUsagesetsproviderMetadata: { openai: usage }, but that does not survive persistence. I checked 105,379assistant rows in my database and none carries a
providerMetadatakey — the persisted message keepsonly
agent, model, content, finish, cost, tokens, time, snapshot, error. So a downstream fallbackthat reads provider metadata off the stored message cannot work; it has to happen inside
mapUsagewhile the raw payload is still in hand.
Scope
#35974 is the same function and the same gap, with DeepSeek's key names instead. Both are one patch.
neriousy commentedon Oct 1, 2026
Closing as a duplicate of #35974, which tracks cache-write tokens not being recorded for OpenAI-compatible providers. As noted there, both cases are the same gap in the usage mapping and need one fix. Please add your gateway/model details there.