Skip to content

fix(ai): stop inferring Anthropic prompt caching from Claude model ids - #3935

Closed
probepark wants to merge 2 commits into
Yeachan-Heo:devfrom
probepark:fix/anthropic-proxy-cache-control-limit
Closed

fix(ai): stop inferring Anthropic prompt caching from Claude model ids#3935
probepark wants to merge 2 commits into
Yeachan-Heo:devfrom
probepark:fix/anthropic-proxy-cache-control-limit

Conversation

@probepark

Copy link
Copy Markdown
Collaborator

What

Non-canonical Anthropic-compatible endpoints no longer receive a generated cache_control marker based on the model id alone. The capability must now be declared:

Endpoint Before After
api.anthropic.com automatic automatic (unchanged)
compatible gateway, claude-* id automatic none
compatible gateway, compat.promptCacheMode: automatic automatic automatic
compatible gateway, compat.promptCacheMode: explicit explicit explicit (unchanged)
compatible gateway, request-time cacheRetention: short|long automatic automatic

cacheRetention: none, request-over-model precedence, and supportsLongCacheRetention are unchanged.

Why

Fixes #3934.

8893c346a added isClaudeFamilyModel() to getCacheControl(), opting every Claude-named model on every unknown gateway into top-level caching. A gateway that already attaches its own block-level markers then sees a fifth breakpoint, and Anthropic rejects the request:

400 invalid_request_error: A maximum of 4 blocks with cache_control may be provided. Found 5.

A model id cannot describe what a proxy adds downstream, and those extra breakpoints are not visible in our serialized params — so clamping before send is not possible either. The capability has to be declared rather than guessed.

The second commit regenerates artifacts/issue-3670-anthropic-cache-eval.json, whose recorded evidence is pinned to the provider source blob oid/sha256 by design.

Testing

GJC_NO_STRICT=0 bun test packages/ai/test/anthropic-cache.test.ts \
  packages/ai/test/anthropic-stream-envelope.test.ts     # 37 pass
GJC_NO_STRICT=0 bun test packages/ai/test/anthropic-cache-eval.integration.test.ts  # 2 pass
GJC_NO_STRICT=0 bun test packages/ai/test                # 2197 pass, 1 fail
bun --cwd=packages/ai run check                          # pass

Two pre-existing failures reproduce identically on unmodified dev and are unrelated to this change:

  • no-strict-env.test.ts > keeps strict mode on when neither name is set — the test spawns a probe without clearing an inherited PI_NO_STRICT=1 from the shell environment
  • check:ts > check:sdk-closureverify-gjc-sdk-canonicalization.ts --self-test expects exit 1, gets 0

Not tested: live traffic against the reporting gateway.

GJC verdict

gajae.pr-review-verdict.v1 needs-human sha256:bb570da94 reviewer:human evidence:local-bun-test-packages-ai

No independent architect/critic review was run; this needs a human reviewer before merge.


  • Target branch dev
  • bun check passes — blocked by the pre-existing check:sdk-closure self-test failure documented above; bun --cwd=packages/ai run check passes
  • Tested locally
  • CHANGELOG updated (if user-facing)
  • Verdict matches exact PR head, not earlier commit

Claude-family model ids behind non-canonical Anthropic-compatible gateways
were opted into top-level automatic caching, but such a gateway may already
attach its own block-level cache_control markers. The generated top-level
marker then became a fifth breakpoint and Anthropic rejected the whole
request with HTTP 400 "A maximum of 4 blocks with cache_control may be
provided. Found 5." A model id cannot tell us what a proxy adds downstream,
so the capability now has to be declared instead of guessed.

Lore-id: 989c3170
Constraint: canonical api.anthropic.com automatic caching must not change
Constraint: cacheRetention none and request-over-model precedence stay intact
Rejected: clamp generated breakpoints before send | gateway-added markers are not visible in our params
Rejected: strip caller-supplied cache_control | silently disables a working proxy cache strategy
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test packages/ai/test/anthropic-cache.test.ts packages/ai/test/anthropic-stream-envelope.test.ts -- 37 pass
Not-tested: live traffic against the reporting gateway
Supersedes: 8893c34
…er source

The eval artifact pins the provider source blob oid and sha256 so the recorded
evidence cannot drift from the code it describes. Changing the cache-mode
resolution invalidates that binding by design, so the artifact is regenerated
against the new source instead of leaving the integration test red.

Lore-id: 989c3170
Tested: bun test packages/ai/test/anthropic-cache-eval.integration.test.ts -- 2 pass
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Superseded by #3933, merged as 42f1091. The accepted policy keeps canonical Anthropic on automatic caching but defaults noncanonical claude-* compatible endpoints to explicit block markers—not none—so CLIProxyAPI users receive moving cache boundaries without per-provider configuration. Explicit promptCacheMode: automatic remains available for verified gateways. This branch is also dirty against current dev.

@Yeachan-Heo Yeachan-Heo closed this Aug 6, 2026

@yazzang-homelab yazzang-homelab left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent architect review. The bug is real and well-diagnosed, but dev moved underneath this branch and now disagrees with it. Rebasing will not resolve that automatically — it needs a decision.

The conflict. This branch's base is 1f735b7b, which predates two merges that touch the exact same expression:

  • 538cb43d1 — fix(ai): advance explicit Anthropic cache through tool loops (#3930)
  • 42f1091c6 — fix(ai): default compatible Claude gateways to explicit caching (#3933)

Same defaulting block, three different answers for "Claude-family model on a non-canonical endpoint, no explicit promptCacheMode":

result
8893c346a (#3923, your base) automatic — the top-level marker that causes the 400
current dev (#3933) explicit
this PR none

So this is not a rebase conflict to resolve mechanically — merging it would revert #3933's default and possibly interact with #3930's tool-loop handling.

And #3933 may not actually close your bug. Your failure is a gateway that already emits its own cache_control blocks; ours becomes the fifth and Anthropic rejects the request with A maximum of 4 blocks with cache_control may be provided. Found 5. explicit mode still emits block-level breakpoints — it changes where the markers go, not how many we add. If the gateway contributes 4 on its own, explicit overflows exactly like automatic did. If that is right, #3933 narrowed the blast radius without removing it, and your none default is the actually-correct one.

Please confirm that against the gateway that produced the 400 and say so in the description. That reframes this from "revert of #3933" to "#3933 was incomplete", which is a much easier call for the maintainer.

On the design itself, two things I'd push on:

  1. || cacheRetention !== undefinedautomatic is a surprising opt-in. Setting a retention is a statement about how long to cache, not which framing to use. A user who sets cacheRetention: "long" on a gateway that rejects top-level markers gets the 400 back, and nothing in the name suggests they opted into automatic framing. If retention should imply caching, explicit is the safer implication; if the point is an escape hatch, promptCacheMode: "automatic" already is one and is explicit about what it does.

  2. none as the default silently disables caching for working gateways. Many Anthropic-compatible proxies pass cache_control through untouched and cache fine today. Defaulting them to none is a real cost regression with no error to notice — the opposite failure mode from the 400, and a quieter one. Both "Rejected" alternatives in your commit message are correctly rejected, but the third option — declare the capability per provider preset, defaulting unknown gateways to none — matches your own conclusion that "a model id cannot tell us what a proxy adds downstream". A compat.promptCacheMode on the known-good presets would keep them working while making unknown gateways safe.

What is good: the constraints are the right ones (canonical api.anthropic.com unchanged; cacheRetention: none and request-over-model precedence intact), isCanonicalApi still gates supportsLongCacheRetention so a gateway cannot claim 1h TTL off canonical metadata, and both rejected alternatives are rejected for correct reasons — clamping cannot see gateway-added markers, and stripping caller-supplied cache_control would break a working proxy strategy.

Also note this PR updates artifacts/issue-3670-anthropic-cache-eval.json provider-source hashes. Those pin a fetched upstream doc; after the rebase please re-derive them on the new base rather than carrying the values forward, or the evidence artifact will attest to a page you did not re-fetch.

gajae.pr-review-verdict.v1 merge-blocked sha256:bb570da948cf52235a2d76119175400ec95abeb9 reviewer:architect evidence:git merge-base is 1f735b7b (predates 42f1091c/#3933 and 538cb43d/#3930); compared anthropic.ts defaulting block on dev vs this head

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants