Repository navigation
Description
Description
Summary
#39009 reported that the v2 OpenRouter route never sets cache_control for Anthropic models. It was closed by #39907. On 2.0.18, opencode run with Anthropic models on OpenRouter still reports zero cache reads and zero cache writes on every step, so every step bills the full prompt as fresh input.
The #39907 PR description says it uses a "stable session prompt_cache_key for default OpenRouter caching without emitting explicit markers". But OpenRouter only caches Anthropic models when explicit cache_control markers are present. So caching looks fixed only when a cache policy is set explicitly. The default case is still broken.
We confirmed three things:
- The same prompt caches on opencode 1.18.32.
- It caches on 2.0.18 when
cache_controlis set by hand as a model option. This is the workaround from OpenRouter route never sets cache_control for Anthropic models #39009. - Interactive (TUI/desktop) sessions on 2.0.18 cache normally.
The impact is large for headless, multi-step runs. In our case, a 2-step extraction run over a ~120k-token prompt cost $0.94. We estimate that caching would have cut that roughly in half.
Environment
- opencode version: 2.0.18 (
~/.opencode/bin/opencode, install/channel: latest). The comparison is with 1.18.32 (Homebrew). - OS: Darwin 25.6.0 arm64 (macOS)
- Terminal: iTerm.app, TERM=xterm-256color
- Shell: /bin/zsh
- Provider:
openrouter(OpenRouter API key auth). OpenRouter routed every request in these tests to Amazon Bedrock (tool call IDs aretoolu_bdrk_…) on both versions and with and without the workaround. So the upstream provider is not the difference. - Active plugins: local files in
~/.config/opencode/plugins/:temp.ts(v2Plugin.define),rtk.tsandherdr-agent-state.js. The last two fail to load on v2. None are referenced inopencode.json. The repro uses the default agent and no plugin tools.
Reproduction
In a directory that has a README.md and a pyproject.toml (any two small files will do):
- opencode 2.0.18, default config:
Then run
opencode run --format json --title cache-repro-v2 \ --model openrouter/anthropic/claude-sonnet-5 \ "Read README.md, then read pyproject.toml, then reply with just: ok"opencode session export <id>and look atmessages[].tokens.cacheon the assistant messages. - Add this project
opencode.jsonand run the same command again:{ "provider": { "openrouter": { "models": { "anthropic/claude-sonnet-5": { "options": { "cache_control": { "type": "ephemeral" } } } }}} } - Optional: run the same prompt on opencode 1.18.32 without the override, and look at
part.tokens.cacheon thestep-finishevents.
Expected Behavior
With no extra config, step 1 writes the shared prefix (system prompt, tools, first user message) to the cache, and later steps read it. That is what 1.18.32 does by default:
| Step | input | cache write | cache read | cost |
|---|---|---|---|---|
| 1 | 2 | 12,044 | 0 | $0.031 |
| 2 | 2 | 1,715 | 12,044 | $0.008 |
| 3 | 2 | 218 | 13,759 | $0.004 |
| 4 | 2 | 431 | 13,977 | $0.004 |
Actual Behavior
On 2.0.18 with default config, every step reports cache: {read: 0, write: 0} and bills the whole prompt as input:
| Step | input | cache write | cache read | cost |
|---|---|---|---|---|
| 1 | 10,809 | 0 | 0 | $0.0227 |
| 2 | 11,295 | 0 | 0 | $0.0226 |
On 2.0.18 with the cache_control model option (step 2), caching works:
| Step | input | cache write | cache read | cost |
|---|---|---|---|---|
| 1 | 2 | 10,865 | 0 | $0.0278 |
| 2 | 2 | 84 | 10,865 | $0.0029 |
| 3 | 2 | 87 | 10,949 | $0.0025 |
A cache write of 0 on step 1 without the override shows that no breakpoint reaches the provider. The model is not simply missing an existing cache.
Additional Context
Consistent. Every opencode run session with an Anthropic model on 2.0.18 without the override shows zero cache use, across 10 runs and every variant we tried:
Variant (2.0.18, opencode run) |
Model | cache read |
|---|---|---|
Custom primary agent, large prompt attached with --file |
claude-sonnet-5 | 0 |
Custom primary agent, agent reads the file with the read tool (3 steps) |
claude-sonnet-5 | 0 |
Default agent, --standalone |
claude-sonnet-5 | 0 |
Default agent, --standalone |
claude-opus-5.5 | 0 |
Default agent, without --standalone |
claude-sonnet-5 | 0 |
Custom agent, variant #none |
claude-haiku-4.5 | 0 |
Interactive sessions are not affected. A long interactive session on 2.0.18 with openrouter/anthropic/claude-opus-5.5 shows 14.4M cache-read tokens. So the interactive path seems to supply a cache policy or hints that run does not. We have not confirmed this in the code.
Automatic-caching models are not affected. Models that OpenRouter caches automatically (for example moonshotai/kimi-k3) show cache reads under opencode run on 2.0.18. This fits the explanation that the Anthropic cache_control markers are what's missing.
Related:
- OpenRouter route never sets cache_control for Anthropic models #39009: the original report. It has the same symptom and the same workaround, and was closed as fixed by feat(ai): expand OpenRouter native support #39907.
- feat(ai): expand OpenRouter native support #39907: its description says default OpenRouter caching uses
prompt_cache_key"without emitting explicit markers". That is not enough for Anthropic models on OpenRouter. - [FEATURE]: Set prompt_cache_ttl for OpenRouter provider #16848 and fix(opencode): set 1h prompt cache TTL for OpenRouter #16850: an earlier report and PR about OpenRouter and
cache_controlfor Anthropic models. - v2: session ID in the first instruction block defeats cross-session prompt caching #51007: cross-session caching in v2. It is a different issue, but it is in the same area.
Suggested fix: when an OpenRouter model is Anthropic-family (anthropic/*, ~anthropic/*), emit cache_control markers by default, or the top-level cache_control field. run should get the same behaviour as the interactive path.
Workaround: set "options": { "cache_control": { "type": "ephemeral" } } for each Anthropic model under provider.openrouter.models.
Plugins
Temp plugin
OpenCode version
2.0.18
Steps to reproduce
Run the same prompt in any directory that has a README.md and a pyproject.toml (any two small files will do):
- opencode 2.0.18:
Then
~/.opencode/bin/opencode run --format json --title cache-repro-v2 \ --model openrouter/anthropic/claude-sonnet-5 \ "Read README.md, then read pyproject.toml, then reply with just: ok"
opencode session export <id>and look atmessages[].tokens.cacheon the assistant messages. - opencode 1.18.32:
Look at
/opt/homebrew/bin/opencode run --format json \ --model openrouter/anthropic/claude-sonnet-5 \ "Read README.md, then read pyproject.toml, then reply with just: ok"part.tokens.cacheon thestep-finishevents.
Screenshot and/or share link
No response
Operating System
Mac 25.6.0
Terminal
iTerm2
Activity
github-actions commentedon Sep 27, 2026
This issue might be a duplicate of existing issues. Please check:
r266-tech commentedon Sep 28, 2026
I reproduced the request-shaping difference locally on tag
v2.0.18(cd9a14a6), using the realcompileRequest(LLM.request(...))and OpenRouter package entrypoint withanthropic/claude-sonnet-5:cache_controlcache: "auto"/cache: "none"cache: { tools: true, system: true, messages: { tail: 1 } }cache_control: { type: "ephemeral" }The default/auto case returns early in cache-policy.ts. This narrows the missing-marker behavior to request compilation; I have not established why the reported interactive path differs or measured live cache usage.
Five local compilation cases plus the existing cache-policy/OpenRouter suites passed (37 tests). No provider requests were sent. AI-assisted investigation.
rekram1-node commentedon Sep 29, 2026
Fixed by #52110, now merged into
v2.OpenRouter now emits explicit
cache_controlbreakpoints by default for Anthropic and Qwen models, including latest-model aliases. Assistant and tool-result markers are serialized on text content parts; Qwen omits tool-definition markers. The default behavior was verified with real CLI tool loops and provider-reported cache reads/writes.Closing as completed on
v2. The installed build must include #52110 to pick up the fix.