Skip to content

openrouter: Anthropic models still get no prompt caching by default in opencode run (#39009 not fully fixed) #51726

Closed

Description

@almcc

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 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 are toolu_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 (v2 Plugin.define), rtk.ts and herdr-agent-state.js. The last two fail to load on v2. None are referenced in opencode.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):

  1. opencode 2.0.18, default config:
    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"
    Then run opencode session export <id> and look at messages[].tokens.cache on the assistant messages.
  2. Add this project opencode.json and run the same command again:
    {
      "provider": { "openrouter": { "models": {
        "anthropic/claude-sonnet-5": { "options": { "cache_control": { "type": "ephemeral" } } }
      }}}
    }
  3. Optional: run the same prompt on opencode 1.18.32 without the override, and look at part.tokens.cache on the step-finish events.

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:

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):

  1. opencode 2.0.18:
    ~/.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"
    Then opencode session export <id> and look at messages[].tokens.cache on the assistant messages.
  2. opencode 1.18.32:
    /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"
    Look at part.tokens.cache on the step-finish events.

Screenshot and/or share link

No response

Operating System

Mac 25.6.0

Terminal

iTerm2

Activity

  1. github-actions commented on Sep 27, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #51557: Anthropic models on DigitalOcean in v2 also fail to use prompt caching by default, leaving cache reads at zero.
  2. r266-tech commented on Sep 28, 2026

    @r266-tech
    Contributor

    I reproduced the request-shaping difference locally on tag v2.0.18 (cd9a14a6), using the real compileRequest(LLM.request(...)) and OpenRouter package entrypoint with anthropic/claude-sonnet-5:

    Input Compiled cache_control
    omitted cache policy / cache: "auto" / cache: "none" absent
    cache: { tools: true, system: true, messages: { tail: 1 } } on the tool, system part, and user part
    model option cache_control: { type: "ephemeral" } preserved at the top level

    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.

  3. rekram1-node commented on Sep 29, 2026

    @rekram1-node
    Collaborator

    Fixed by #52110, now merged into v2.

    OpenRouter now emits explicit cache_control breakpoints 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions