Repository navigation
fix(runtime): keep the Anthropic cache breakpoint on replayed history - #6041
Conversation
Native Anthropic requests use request-level automatic caching, which places the breakpoint on the last cacheable block. Since apache#5740 that block is the request-only Turn context appended after history, which no later request repeats at that position. Each request therefore paid a cache write that the next step and the next Turn could not read. When the resolved provider options enable Anthropic automatic caching, the model adapter now marks the last replayed history message with the same cache control, so the entry lands on a prefix the next request repeats. The Turn context stays request-only, connections without automatic caching get no marker, and durable message identities are unchanged because the marker is applied to the dispatch copy. Refs apache#6040 Generated-by: Devin Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…reakpoint Ablation showed the non-positive count guard and the intermediate locals change no behavior: a count of zero matches no message index. The test now checks that the block marker uses the request-level cache control instead of repeating the request-level assertion that provider conformance already covers. Refs apache#6040 Generated-by: Devin Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin Refs apache#6040
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Reviewed range: 2463d86c..bf12f012 (3 commits, full review, first pass; merge-base is current main). No prior findings.
Verdict: no P0-P2 findings. The placement is correct under Anthropic's prompt-caching rules, and other providers are unaffected.
What I checked:
- Breakpoint budget. The only other
cacheControlsource is the request-level field atpackages/runtime/src/model-factory.ts:594, and it is set only forproviderType === 'anthropic'. With this PR a request carries at most 2 of the 4 allowed breakpoints: the automatic one plus one explicit block marker.@ai-sdk/anthropic4.0.68 also counts breakpoints (CacheControlValidator,MAX_CACHE_BREAKPOINTS = 4) and drops any extra with a warning instead of sending it. - Placement. The adapter sets message-level
providerOptions.anthropic.cacheControlon messagehistoryMessageCount - 1of a copy (model-adapter.ts:1270-1295). The SDK puts a message-level marker on the message's last part. For atoolmessage that is the lasttool_result, even when the SDK merges that message with the trailing contextusermessage into one Anthropic user turn, so the marker lands on thetool_resultblock just before the context text. That is the last block the next step replays unchanged. - Mixing with automatic caching. Anthropic returns a 400 when explicit markers take all 4 slots, or when an explicit marker on the last block has a different TTL from the top-level field. Neither can happen here: the explicit marker is never on the last block (the guard
historyMessageCount >= messages.lengthskips requests with no context tail), and it copies the top-level value, so the TTL ordering rule also holds. - Non-cacheable tails. If the last history part is a reasoning block, the SDK emits
thinking/redacted_thinkingwithoutcache_control. If it is atool-approval-response, the SDK skips it. In both cases the marker is dropped and the request falls back to today's behavior, with no 400. - Replayed and compacted history.
historyMessageCountis read per attempt fromattemptMessages(ai-sdk-turn.ts:1632). Overflow recovery reassigns that variable (ai-sdk-turn.ts:2018), anddispatchMessagesis rebuilt from it on every loop, so after mid-turn compaction or overflow recovery the marker moves to the last message of the rebuilt history. The first request after a compaction writes, and later requests read. Durable messages andmemorySourceMessageskeep the unmarkeddispatchMessages. - Other providers. MiniMax, Kimi, custom Anthropic-protocol connections and every non-Anthropic wire have no
anthropic.cacheControlin their resolved provider options, so the helper returns the input array unchanged. The second new test pins this for acustom+anthropic-messagesrelay.
P3 (non-blocking):
- The request-level automatic breakpoint still lands on the request-only context block, so each request still pays a 1.25x cache write on the context tokens after the history breakpoint, and no later request reads it. The amount is small (the context is short), but if those tokens matter, this PR could drop the top-level field and rely on explicit markers when a context tail exists.
- When the last history part is a reasoning or approval part, the history marker is silently lost (see above). This is no worse than before. A test for the reasoning-last case would document that.
CI: test passes on bf12f012. Mergeable: MERGEABLE (state BLOCKED only on required review). No protocol files touched, so no epoch impact.
|
On the review's P3 about the request-level automatic breakpoint still landing on the request-only context block: keeping the top-level field is deliberate. The tail write is not strictly unread — a retried step resends identical Dropping it conditionally when a context tail exists would add a branch and remove the retry-read path, so we are keeping the current shape. |
Astro-Han
left a comment
There was a problem hiding this comment.
Approved at @Astro-Han's explicit request: the automated review of this exact head found no blocking (P0–P2) issues, and CI is green.
English
Summary
On native Anthropic connections, Maka enables automatic prompt caching with the request-level
cache_controlfield (#1420). Anthropic then places the breakpoint on the last cacheable block. Since #5740, every Turn step appends request-only context after the history: theruntime.environmentsnapshot and plugincontexts. The AI SDK merges that text into the last user message, so the breakpoint lands on it. Later requests never repeat that block at that position, so each request paid a cache write that no later request could read.This PR keeps the context request-only and moves the cache entry onto the history.
AiSdkTurnpasseshistoryMessageCounttoModelAdapter.startStream. When the resolved provider options carryanthropic.cacheControl, the adapter marks the last history message with the same value. Step N+1 and the first request of the next Turn then read the prefix that step N wrote.Unchanged behavior:
providerType: 'anthropic'setsanthropic.cacheControl, so MiniMax, Kimi, and custom Anthropic-protocol connections get no marker.The context stays out of the system prompt on purpose. Its timestamp changes on every Turn, so that placement would invalidate the full history each Turn.
Fixes #6040
Verification
keeps the cache breakpoint on durable history before request-only contextdrives a two-step tool Turn throughgetAIModelagainst a stubbed Anthropic stream. It fails before the change (no block-level marker; the context is the last block) and passes after it.adds no cache breakpoint on Anthropic-protocol connections without automatic cachingpasses before and after. When the marker ignoresanthropic.cacheControl, it is the only failure acrossai-sdk-backend.test.jsand the 323 related tests listed below.npm run build,npm run lint,npm run format:check, and@maka/runtimetypecheck passed.ai-sdk-backend.test.js256/256.model-adapter*,memory-extraction*,mid-turn-capacity-backend,provider-conformance,provider-contract-matrix, andcomputer-use-provider-protocol323/323.runtime-hostexecution-model-composition.test.js41/41.npm run typecheckandknipforapps/desktopandpackages/uipassed.@maka/runtimetest:dist: 3825 passed, 12 skipped, 1 failed. The failure isowned-child-process.test.js› "a supervisor fault is reported as a failure before its tree is stopped". It also failed in 4 of 6 isolated runs and does not import the changed modules.AI use
Select exactly one:
Tool(s) and scope: Devin (Claude Opus 5.5) diagnosed the issue, implemented the change, wrote the tests, and ran the local verification and caching model. The commits carry a
Generated-bytrailer.Checklist
Does this PR entail a change in behavior?
中文
摘要
对原生 Anthropic 连接,Maka 通过请求级的
cache_control字段开启自动提示缓存(#1420)。Anthropic 会把断点放在最后一个可缓存块上。自 #5740 起,每个 Turn 的每一步都会在历史之后追加仅用于本次请求的上下文:runtime.environment快照和插件的contexts。AI SDK 会把这段文本并入最后一条用户消息,于是断点落在它上面。后续请求在同一位置不会重复这个块,因此每个请求都支付了缓存写入费用,却没有任何后续请求能读到。这个 PR 保持上下文只用于本次请求,把缓存条目移到历史上。
AiSdkTurn向ModelAdapter.startStream传入historyMessageCount。当解析后的服务商选项包含anthropic.cacheControl时,适配层用同一个值标记最后一条历史消息。这样第 N+1 步和下一个 Turn 的第一个请求都能读取第 N 步写入的前缀。保持不变的行为:
providerType: 'anthropic'会设置anthropic.cacheControl,所以 MiniMax、Kimi 和自定义的 Anthropic 协议连接都不会被加标记。上下文有意不放进系统提示词。它的时间戳每个 Turn 都会变化,放在那里会让整段历史的缓存在每个 Turn 失效。
Fixes #6040
验证
keeps the cache breakpoint on durable history before request-only context通过getAIModel对接一个模拟的 Anthropic 流,跑一个两步的工具调用 Turn。修改前失败(没有块级标记,上下文是最后一个块),修改后通过。adds no cache breakpoint on Anthropic-protocol connections without automatic caching在修改前后都通过。当标记不再检查anthropic.cacheControl时,在ai-sdk-backend.test.js和下面所列的 323 个相关测试中,只有它失败。npm run build、npm run lint、npm run format:check和@maka/runtime的类型检查均通过。ai-sdk-backend.test.js256/256。model-adapter*、memory-extraction*、mid-turn-capacity-backend、provider-conformance、provider-contract-matrix和computer-use-provider-protocol共 323/323。runtime-host的execution-model-composition.test.js41/41。npm run typecheck以及apps/desktop、packages/ui的knip均通过。@maka/runtime的test:dist:3825 个通过,12 个跳过,1 个失败。失败的是owned-child-process.test.js› "a supervisor fault is reported as a failure before its tree is stopped"。它单独运行 6 次中也失败 4 次,并且不引用本次修改的模块。AI 使用
只选一项:
工具及范围:Devin(Claude Opus 5.5)完成了问题诊断、代码实现、测试编写,以及本地验证和缓存模型。提交带有
Generated-by尾注。检查清单
这个 PR 是否带来行为变化?