Skip to content

fix(runtime): keep the Anthropic cache breakpoint on replayed history - #6041

Merged
Astro-Han merged 3 commits into
apache:mainfrom
Colafornia:fix/6040-anthropic-cache-breakpoint
Oct 10, 2026
Merged

Astro-Han merged 3 commits into
apache:mainfrom
Colafornia:fix/6040-anthropic-cache-breakpoint

Conversation

@Colafornia

Copy link
Copy Markdown
Member
English

Summary

On native Anthropic connections, Maka enables automatic prompt caching with the request-level cache_control field (#1420). Anthropic then places the breakpoint on the last cacheable block. Since #5740, every Turn step appends request-only context after the history: the runtime.environment snapshot and plugin contexts. 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. AiSdkTurn passes historyMessageCount to ModelAdapter.startStream. When the resolved provider options carry anthropic.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:

  • Only providerType: 'anthropic' sets anthropic.cacheControl, so MiniMax, Kimi, and custom Anthropic-protocol connections get no marker.
  • A request without request-only context gets no marker, so its body is unchanged.
  • The marker goes on the dispatch copy. Durable message objects, and the memory-extraction positions keyed by them, stay unchanged.
  • Request-level automatic caching stays on. Both markers use the same TTL.

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

  • New test keeps the cache breakpoint on durable history before request-only context drives a two-step tool Turn through getAIModel against a stubbed Anthropic stream. It fails before the change (no block-level marker; the context is the last block) and passes after it.
  • New test adds no cache breakpoint on Anthropic-protocol connections without automatic caching passes before and after. When the marker ignores anthropic.cacheControl, it is the only failure across ai-sdk-backend.test.js and the 323 related tests listed below.
  • With this change applied: npm run build, npm run lint, npm run format:check, and @maka/runtime typecheck passed. ai-sdk-backend.test.js 256/256. model-adapter*, memory-extraction*, mid-turn-capacity-backend, provider-conformance, provider-contract-matrix, and computer-use-provider-protocol 323/323. runtime-host execution-model-composition.test.js 41/41.
  • Before the latest rebase: root npm run typecheck and knip for apps/desktop and packages/ui passed. @maka/runtime test:dist: 3825 passed, 12 skipped, 1 failed. The failure is owned-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.
  • A local model of Anthropic's documented caching rules replayed the request bodies from a two-Turn, six-request session. Estimated cache reads went from 0% to 76% of input tokens.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

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-by trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No
中文

摘要

对原生 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 协议连接都不会被加标记。
  • 没有仅用于本次请求的上下文时不加标记,请求体保持不变。
  • 标记加在发送用的副本上。持久消息对象,以及以它们为键的记忆提取位置,都保持不变。
  • 请求级自动缓存继续开启。两个标记使用相同的 TTL。

上下文有意不放进系统提示词。它的时间戳每个 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.js 256/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.js 41/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 次,并且不引用本次修改的模块。
  • 用本地实现的 Anthropic 文档缓存规则模型,回放了一个两个 Turn、六个请求的会话的请求体。估算的缓存读取占输入 token 的比例从 0% 提高到 76%。

AI 使用

只选一项:

  • 没有生成式工具做出实质性贡献
  • 生成式工具做出了实质性贡献

工具及范围:Devin(Claude Opus 5.5)完成了问题诊断、代码实现、测试编写,以及本地验证和缓存模型。提交带有 Generated-by 尾注。

检查清单

  • 测试覆盖了这次修改,并且没有这次修改时会失败
  • 本地的 lint、格式、类型检查和受影响的测试套件均通过

这个 PR 是否带来行为变化?

  • 是 —— 已在上方摘要中说明
  • 否

Colafornia and others added 2 commits October 10, 2026 14:09
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>
@github-actions github-actions Bot added the effort/M Under 500 readable lines label Oct 10, 2026

@Astro-Han Astro-Han 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.

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 cacheControl source is the request-level field at packages/runtime/src/model-factory.ts:594, and it is set only for providerType === '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/anthropic 4.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.cacheControl on message historyMessageCount - 1 of a copy (model-adapter.ts:1270-1295). The SDK puts a message-level marker on the message's last part. For a tool message that is the last tool_result, even when the SDK merges that message with the trailing context user message into one Anthropic user turn, so the marker lands on the tool_result block 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.length skips 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_thinking without cache_control. If it is a tool-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. historyMessageCount is read per attempt from attemptMessages (ai-sdk-turn.ts:1632). Overflow recovery reassigns that variable (ai-sdk-turn.ts:2018), and dispatchMessages is 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 and memorySourceMessages keep the unmarked dispatchMessages.
  • Other providers. MiniMax, Kimi, custom Anthropic-protocol connections and every non-Anthropic wire have no anthropic.cacheControl in their resolved provider options, so the helper returns the input array unchanged. The second new test pins this for a custom + anthropic-messages relay.

P3 (non-blocking):

  1. 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.
  2. 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.

Comment thread packages/runtime/src/model-adapter.ts
@Colafornia

Copy link
Copy Markdown
Member Author

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 dispatchMessages, so the automatic breakpoint on the context block is the one entry a same-step retry can read. It is also the sole breakpoint for startStream callers that pass no historyMessageCount, and the graceful fallback when the SDK drops the history marker. The extra write is bounded by the context block (a few hundred tokens at 1.25x).

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 Astro-Han 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.

Approved at @Astro-Han's explicit request: the automated review of this exact head found no blocking (P0–P2) issues, and CI is green.

@Astro-Han
Astro-Han merged commit be68f29 into apache:main Oct 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(runtime): Anthropic automatic cache breakpoint lands on request-only Turn context

2 participants