Repository navigation
fix(claude): keep the caller's 1h cache TTL on the translated lane - #7176
vadymhimself wants to merge 6 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
Live A/B on real Anthropic (Claude Code 2.1.281,
Without the fix the prefix expired during the pause and the whole conversation was re-written to cache; with it the caller's 1h TTL holds and the turn reads from cache like the native lane. Turns before the pause were equivalent across builds. |
|
@coderabbitai review |
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
✅ Deterministic PR hygiene checks passed. |
⏳ DRAFT
What to do
Review readiness checklist
0/4 boxes ticked. This PR stays in draft until every box above is ticked. |
0e73693 to
1d25ca4
Compare
The translated Messages lane drops the caller's cache_control breakpoints and the anthropic adapter places its own, with TTL from the global cacheRetention only. Claude Code on a subscription marks its breakpoints ttl:"1h"; the translated lane downgraded them to 5m, so any pause over 5 minutes re-wrote the whole prefix. When no operator cacheRetention is set and any caller breakpoint asks for ttl:"1h", run that request with cacheRetention "long". Precedence: operator config > caller hint > default. Breakpoint positions are unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
A caller that marks only its stable prefix 1h and keeps the moving tail at 5m was mapped to cacheRetention long, which also makes the adapter's per-turn breakpoints 1h and bills those writes at the 1h rate. Only map to long when every caller breakpoint asks for 1h.
A 1h system prefix with a 5m top-level cache_control was read as all-1h, so every adapter breakpoint, including the moving tail, was written at the 1h rate. The request-level control is a caller breakpoint too.
A 5m breakpoint on a tool_result's own text block was invisible to the caller TTL scan, so a 1h prefix still upgraded the whole request to 1h.
search_result and document sources nest content blocks below tool_result, so a one-level scan still read a 5m tail as all-1h. Walk block containers generically and skip tool inputs and schemas, which are caller data.
1d25ca4 to
f0086f3
Compare
|
Being carried into #7225 with your authorship preserved; this PR will be closed when the bundle lands. |
…s and usage metering (#7194, #7201, #7176, #7177, #7178, #7179, #7199, #7202, #7189, #7190, #7200) (#7225) Carried from #7194, #7201, #7176, #7177, #7178, #7179, #7199, #7202, #7189, #7190, #7200 (one commit per source PR, original author preserved). #7205 held: replay can relocate server-tool exchanges into an earlier assistant turn. Remote validation (hosted GitHub Actions unreliable; local tests not run by maintainer rule): union candidate codex/prr-integ-8 = dev c03eafd + #7224 + #7223 + #7225 + #7217 + #6454 + #6594 plus a Gentle Shell import-cycle fix (integ-9 e1f289c: typecheck + full suite on mini (Windows) and lidge (Linux), compared with the same host's dev runs) and a test-only Windows path-escaping fix for the reasoning-retention routing test (integ-10 0d4cba7, tree a2b309a: that file re-run on both hosts). Co-authored-by: Vadym O <bolein95@gmail.com>
Summary
On the translated Messages lane, opencodex ignores the caller's
cache_controlbreakpoints and the anthropic adapter places its own 4. This lane covers combo and policy routes, and any route the native Messages lane declines. The adapter's TTL comes only from the globalcacheRetention, which is 5m when unset (src/adapters/anthropic.ts:75-79,:1160-1171).Claude Code on a subscription marks its breakpoints
{ "type": "ephemeral", "ttl": "1h" }. The translated lane downgraded those to 5m. Any pause longer than 5 minutes therefore misses the whole cached prefix, and the next turn re-writes it. That is normal human pacing, and it is the gap Claude Code's own 1h TTL is designed to cover.Quota impact. Every pause of more than 5 minutes on a translated route paid a full prefix cache write again: system, tools and history. A 1h hit would have read it instead. On subscription accounts that is quota burned for no benefit. The size of the loss was measured offline only, with an in-process mock upstream and a local model of Anthropic's cache. No live API traffic was used, so the real-world magnitude is unmeasured.
The only workaround so far was
cacheRetention: "long", which forces 1h on every breakpoint for every client.Change. When
cacheRetentionis not set and any of the caller's own breakpoints asks forttl: "1h", that one/v1/messagesrequest runs withcacheRetention: "long".cacheRetention> caller hint > default (5m).src/claude/inbound.ts: newcallerCacheRetention(raw)scans the caller's system, tools and message blocks. It is read out of body, because the Responses body has no field that would not leak to a native Responses forward.src/server/claude-messages.ts: the per-request config handed tohandleResponsesgetscacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody). Only the anthropic adapter readscacheRetention(src/adapters/registry.ts:101). GPT legs of a combo, and the native lane (which forwards the caller's breakpoints verbatim), are unaffected."long"also makes the moving per-turn message breakpoint and the top-level automatic one 1h. This matches Claude Code's own 1h marking, which putsttl: "1h"on its message breakpoint too. It also means those per-turn writes are billed at the 1h write rate, exactly as the caller asked.cacheRetentionrow inreference/configuration/providers.mdnow describes the fallback (English only).Verification
Wire capture of real Claude Code 2.1.281 (
CLAUDE_CODE_PROMPT_CACHE_TTL=1h) against a local capture server: breakpointssystem[1],system[2]and the movingmessages[1].content[0]are all{"type":"ephemeral","ttl":"1h"}— so mapping to"long"(1h on the moving message breakpoint too) mirrors what the caller asked for.Red/green. I reverted only the two
src/files toupstream/devand kept the new test.bun test tests/claude-integration/claude-translated-cache-ttl.test.tsthen gave 2 pass / 1 fail:caller ttl 1h keeps every adapter breakpoint at 1hreceived an untagged (5m) TTL. With the fix restored: 3 pass / 0 fail. The test driveshandleClaudeMessageson a combo route with a mocked upstream and asserts the TTLs on the anthropic wire body in three cases: caller 1h gives 1h, caller with no TTL gives 5m, and caller 1h with operator"short"gives 5m.bun run typecheck: clean.Focused:
bun test tests/claude-integration/claude-inbound.test.ts tests/claude-integration/claude-messages-endpoint.test.ts tests/claude-integration/claude-sidecar-override.test.ts tests/claude-integration/claude-inbound-cache-stabilize.test.ts: 168 pass / 0 fail.bun run test:changed(runner Bun 1.4.3): 10437 pass / 32 skip / 1 fail across 456 files. The one failure istests/cli/cli-connect-readiness.test.ts, "connected-client runtime probe scope > a rejected preferred runtime falls back without rewriting the saved selection". It fails the same way on cleanupstream/devat7640a942b(26 pass / 1 fail in isolation), so it is not caused by this change.bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts: everything passes exceptmove end to end > domain verification launches isolate tests through the pinned runner. That case also fails on cleanupstream/devbecause the local default Bun is not 1.4.3. The new file is registered inscripts/test-layout/layout.jsonandtests/fixtures/test-layout-expected.json.bun run structure:check: passed. The docs-site build (cd docs-site && bun install --frozen-lockfile && bun run build) passed, as diddocs-developer-role-policyanddocs-429-failover-claims(23 pass).Offline harness. This drives
handleClaudeMessagesthrough 6 Claude Code-shaped turns, with the network blocked and the upstream mocked. Wire breakpoints at turn 2:tools[17] system[0] messages[2].user[0] messages[3].user[0]:auto(all 5m)tools[17]:1h system[0]:1h messages[2].user[0]:1h messages[3].user[0]:1h:autosystem[2]:1h messages[2].user[1]:1h.Not run: the full
bun run test. The change is twosrc/files on one request path, andtest:changedcovered its import graph. The full suite is left to CI.Maintainer fixes on top of the contributor commits (
0feecfcbf,33d522318,26c38ea8a,0e73693bf):callerCacheRetentionnow returns"long"only when every caller breakpoint asks for1h, counting the top-level automaticcache_controland breakpoints nested at any depth in block containers (tool_result,search_result,documentsources), while toolinput/input_schemaare skipped. Before this, a caller with a 1h prefix and a 5m tail had its moving per-turn breakpoint upgraded to 1h, so those writes were billed at the 1h rate (2x base input instead of 1.25x). New cases inclaude-translated-cache-ttl.test.tsfail against each previous head and pass now (6 pass / 0 fail).Maintainer local evidence at
0e73693bf:bun test --isolate tests/claude-integration tests/adapters/anthropic3255 pass / 1 fail. The failure isclaude-desktop-remote-hub.test.ts"connected Desktop uses hub IDs across a cold restart", which fails the same way on a dev-equivalent tree.bun run test:changedat0feecfcbfgave 10439 pass / 32 skip / 0 fail across 456 files; later commits only touchcallerCacheRetentionand its test.bun run typecheckclean, and the file-size ratchet passes.Checklist
🤖 Generated with Claude Code
Summary by CodeRabbit
/v1/messagesrequests now use long cache retention when all caller-specified cache breakpoints use a 1-hour TTL.Review readiness checklist
This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:
Required local validation passed; commands, results, and any full-suite exception are documented.
I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
I resolved all correct Codex and CodeRabbit findings.
My PR is ready for review.