Skip to content

Bedrock: prompt caching silently off when modelID is an application inference profile ARN #55938

Closed

Description

@rchhugani

Summary

When a Bedrock Claude model's modelID is an application inference profile ARN, OpenCode sends no cachePoint blocks, so prompt caching is silently off. Every step pays the full input price. For us this took cached input from ~93% to 0% and more than doubled daily Bedrock cost.

Environment

  • opencode version: 2.0.18 (bug is still present on main in packages/ai/src/protocols/utils/bedrock-cache.ts)
  • OS: Darwin 25.6.0 arm64 (macOS)
  • Terminal: xterm-256color (also reproduced headless with opencode run --standalone)
  • Shell: /bin/zsh
  • Install/channel: latest
  • Active plugins: none found in config

Reproduction

  1. Configure a Bedrock provider (@ai-sdk/amazon-bedrock / built-in Bedrock route) with a Claude model whose modelID is an application inference profile ARN backed by Claude Sonnet:

    {
      "providers": {
        "claude": {
          "models": {
            "global.anthropic.claude-sonnet-5": {
              "modelID": "arn:aws:bedrock:us-east-1:<account>:application-inference-profile/<id>",
              "compatibility": { "cacheFormat": "anthropic" }
            }
          }
        }
      }
    }
  2. Run a multi-step prompt: opencode run --standalone --format json --model claude/global.anthropic.claude-sonnet-5 "Read a.txt with the read tool, then read it again, then reply DONE"

  3. Inspect the step usage in the JSON output.

  4. Remove the modelID override (so the plain model ID is used) and repeat.

Expected Behavior

Caching works the same as with the plain model ID: step 1 writes the cache and later steps read it. A profile ARN is a recommended AWS way to tag cost per user or team. Using one should not turn off caching.

Actual Behavior

With the ARN, every step reports "cache":{"read":0,"write":0}, for example "input":11863 then "input":18431.

With the plain model ID, the same prompt gives "write":11860, then "read":11860, "write":6589.

compatibility.cacheFormat: "anthropic" does not help.

Additional Context

Cause, in packages/ai/src/protocols/utils/bedrock-cache.ts:

supported: modelID.includes("anthropic.claude-") && (...)

The comment says substring matching handles "model-bearing ARNs". An application inference profile ARN (arn:aws:bedrock:<region>:<account>:application-inference-profile/<id>) carries no model name, so supported is always false.

Suggested fixes, any of:

  • Use the configured model's catalog identity, such as the models map key, family, or the canonical model, rather than the raw modelID.
  • Honor compatibility.cacheFormat (or a Bedrock-specific flag) on the Bedrock Converse route to force cache points on.

Related: #53756 (same root cause: model family is read from an ARN, for effort variants), #53751.

Workaround: don't use profile ARNs, which loses per-user cost attribution.

Frequency: 100% reproducible.

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and for tracing the cause. I could reproduce this on 2.0.18, and it still happens on the latest dev (eeec76bb).

    Setup: a mock Bedrock runtime records each Converse request and counts its cachePoint blocks. OpenCode points at it through providers.amazon-bedrock.settings.baseURL, with a Claude Sonnet model and compatibility.cacheFormat: "anthropic".

    • Plain model ID: the Claude requests to /model/global.anthropic.claude-sonnet-4-5-.../converse-stream carry 2 and 4 cachePoint blocks.
    • modelID set to an application inference profile ARN: the requests to /model/arn:aws:bedrock:...:application-inference-profile/abc123/converse-stream carry 0, with or without cacheFormat: "anthropic".

    The mock can't show real cache read/write numbers, but with no cache points in the request Bedrock has nothing to cache, which fits the 0 read / 0 write you saw. As you found, BedrockCache.breakpoints() decides support only from the wire model ID, which an application profile ARN doesn't contain.

    The script below reproduces it. Related: #53756 is the same ARN problem for effort variants, and PR #53820 addresses that part only.

    Reproduction script (run.sh)
    #!/usr/bin/env bash
    # Reproduces #55938: no cachePoint blocks are sent to Bedrock when modelID is an application inference profile ARN.
    # Usage: ./run.sh            (uses `opencode` from PATH)
    #        OC_DEV=/workspace/repo ./run.sh   (runs from a source checkout)
    set -u
    DIR=$(cd "$(dirname "$0")" && pwd)
    PORT=18080
    LOG=$DIR/requests.jsonl; rm -f "$LOG"
    cat > "$DIR/mock.ts" <<'TS'
    // Mock Bedrock runtime: logs each Converse request (model in URL, cachePoint count) and returns a 400.
    const log = process.env.LOG ?? "/workspace/repro/requests.jsonl"
    Bun.serve({
      port: Number(process.env.PORT ?? 18080),
      async fetch(req) {
        const body = await req.text()
        const url = new URL(req.url)
        const cachePoints = (body.match(/"cachePoint"/g) ?? []).length
        await Bun.write(log, (await Bun.file(log).text().catch(() => "")) + JSON.stringify({ path: decodeURIComponent(url.pathname), cachePoints }) + "\n")
        return new Response(JSON.stringify({ message: "mock: stop here" }), { status: 400, headers: { "content-type": "application/json", "x-amzn-errortype": "ValidationException" } })
      },
    })
    TS
    LOG=$LOG PORT=$PORT bun "$DIR/mock.ts" & MOCK=$!; sleep 1
    if [ -n "${OC_DEV:-}" ]; then
      # the standalone server child is spawned without these flags, so pass them via BUN_OPTIONS too
      export BUN_OPTIONS="--conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js"
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else OC=(opencode); fi
    for MODE in plain arn; do
      P=$(mktemp -d); export HOME=$P/home XDG_CONFIG_HOME=$P/home/.config XDG_DATA_HOME=$P/home/.data XDG_STATE_HOME=$P/home/.state XDG_CACHE_HOME=$P/home/.cache
      mkdir -p "$HOME"; cd "$P"; git init -q
      if [ $MODE = arn ]; then MID='"modelID": "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123",'; else MID=''; fi
      cat > opencode.jsonc <<JSON
    {
      "providers": {
        "amazon-bedrock": {
          "settings": { "baseURL": "http://127.0.0.1:$PORT", "apiKey": "dummy", "region": "us-east-1" },
          "models": {
            "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { $MID "compatibility": { "cacheFormat": "anthropic" } }
          }
        }
      }
    }
    JSON
      echo "== $MODE"
      echo "{\"mode\":\"$MODE\"}" >> "$LOG"
      AWS_BEARER_TOKEN_BEDROCK=dummy timeout 60 "${OC[@]}" run --standalone --format json --model "amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0" "hello" 2>&1 | tail -3
    done
    kill $MOCK
    echo "== requests seen by mock Bedrock"; cat "$LOG"

    Run it: bash run.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions