Repository navigation
feat(provider): 接入 prompt 缓存(Anthropic 断点 + OpenAI 路由键) - #732
Open
Cai-Tang-www wants to merge 8 commits into
Open
Cai-Tang-www wants to merge 8 commits into
Cai-Tang-www wants to merge 8 commits into
Conversation
公共层只表达"哪一段系统提示是稳定前缀",各 adapter 按自家协议映射: - anthropic:拆稳定块与易变尾部,稳定块与最后一个工具各打一个 cache_control: ephemeral 断点;稳定前缀不足 1024 token 时不打标, 避免支付写入溢价却拿不到命中 - openaicompat:发送 prompt_cache_key(模型 + 稳定前缀 + 工具名集合哈希), 提升自动前缀缓存的同前缀路由命中率 - gemini:依赖隐式缓存,不发送额外参数 两处配套修正必须同时落地,否则接入后行为错误且没有任何报错: - Usage.InputTokens 统一为"未命中缓存的输入"(OpenAI 侧减去 cached 并在 厂商异常数据下收敛为 0),预算链路改用 TotalInputTokens。Anthropic 的 input_tokens 不含缓存,直接取值会让 TokenInputTotal 严重低估,导致 compact 永不触发、预算门禁失效 - prompt_cache_key 按 driver 门控:chatcompletions.Request 是五家自研厂商 共用的中间结构,字段会无条件扩散进请求体,DeepSeek / Qwen / GLM / MiMo / MiniMax 一律不发送 token_usage 与 ledger_reconciled 事件透出 cache_read / cache_write 明细, 供客户端验证命中率。新增 provider 级配置 prompt_cache: auto|off,非法值在 启动阶段校验失败。
memo 是 agent 自身写入的会话记忆,用 memo_remember 记一条就变一次。 原先 NewConfiguredBuilder 把外部 SectionSource 追加进 stable 段,意味着每写 一条记忆都会让整个 prompt 缓存前缀失效:实际命中率趋近于 0,token 成本照烧, 用户完全无感。 改为归入动态段并排在最前。稳定段只保留角色定义与工作区规则文件(AGENTS.md 一类),它们的变化频率足够低,配得上"缓存前缀"这个定位。 原测试 TestDefaultBuilderBuildMemoIsStable 锁定的正是这一错误行为,改名为 TestDefaultBuilderBuildMemoStaysInDynamicSegment 并反转断言,同时补上理由注释。
- configuration.md 增加 prompt_cache 配置项说明、三家 driver 的映射方式, 以及缓存字段进入 usage 后的口径变化 - runtime-provider-event-flow.md 补充 ledger_reconciled 的 cache_read_input_tokens / cache_write_input_tokens 字段,并说明 input_tokens 按"含缓存的总输入"统计
23 of 27 tasks
Contributor
There was a problem hiding this comment.
The review found a configuration-loading defect, a conditional gateway compatibility regression, incorrect accounting for malformed cache counts, and testing/documentation gaps. Provider, context, and runtime tests passed; the configuration suite encountered the unrelated failure below.
Unrelated issues
[P3] /workspace/internal/config/loader_test.go:1719: The unchanged permission test fails when run as root because chmod 000 does not prevent root from reading the directory.
Reviewed head commit: 1280c567bc6a0c6877448897a1a63a9c86d3a40a
- [P1] Add prompt_cache to the provider file load/save path (
internal/config/provider.go:33-34) — see inline comment - [P2] Gate cache keys on endpoint support (
internal/provider/prompt_cache.go:35-38) — see inline comment - [P2] Bound cached tokens before deriving uncached input (
internal/provider/openaicompat/chatcompletions/adapter.go:279-286) — see inline comment - [P2] Test cached usage through generation and reconciliation (
internal/runtime/ledger_cache_payload_test.go:21-29) — see inline comment - [P3] Scope the cache-routing documentation to chat completions (
docs/guides/configuration.md:289) — see inline comment
customProviderFile 使用 KnownFields(true) 解码,但结构体没有 prompt_cache 字段, 按文档在 provider.yaml 里写 `prompt_cache: off` 会直接解析失败;SaveCustomProviderInput 与读写两侧的字段映射也都缺这一项,导致该配置只能通过程序化构造生效。 补全 customProviderFile、SaveCustomProviderInput、加载双向映射、保存构造与 NormalizeCustomProviderInput,并在归一化时统一为小写,非法值由 ProviderConfig.Validate 拦截,保持保存与加载两条路径共用同一套规则。 新增往返测试:保存后重新加载取值一致、手写含 prompt_cache 的 provider.yaml 可加载、 非法值被拒绝且错误信息包含字段名。
两个 review 意见合并处理,都来自"默认开启"这一假设过于乐观: 1. 默认值改为按 driver 区分 openaicompat 服务任意兼容网关,prompt_cache_key 属可选扩展参数。此前默认发送会让 原本可用的网关返回 400,而现有 stream fallback 只处理流式解析失败,不处理 HTTP 参数 拒绝,等于给已工作的部署引入回归。 现在只有 anthropic 默认开启(cache_control 是官方协议字段);openaicompat 默认关闭, 需在 provider 配置里显式写 prompt_cache: auto 声明端点支持。五个自研 driver 由 SupportsPromptCacheKey 硬约束排除,显式 auto 也不发送。 不做基于错误文本的 400 识别重试:识别脆弱且会掩盖真实错误,用能力声明更可控。 2. 缓存命中数按区间收敛 prompt=100、cached=150 时,原来只对减法结果做下限裁剪,cached 保留 150, TotalInputTokens() 得 150,高于厂商上报的总量,而该值会被账本当作已观测输入持久化。 改为 normalizeCachedPromptTokens 把 cached 收敛到 [0, prompt],总输入严格等于上报量。 顺带把 key 计算提到 provider.ComputePromptCacheKey,供 chat completions 与 responses 共用。
此前只有 chat completions 发送 prompt_cache_key,而 chat_api_mode: responses 走 responses.BuildRequest + generateSDKResponses,两条路径都不带该字段。文档里那行 映射因此对 responses 模式并不成立。 Responses API 本身支持 prompt_cache_key,所以补齐实现而不是收紧文档: Request 增加字段,BuildRequest 复用 provider.ComputePromptCacheKey,与 chat completions 共用同一套键计算,保证同一前缀在两种模式下路由一致。默认关闭与显式开启的规则相同。
原测试直接构造已对账结果、只验证 payload 字段拷贝,若 generateStreamingMessage 取错 InputTokens 或对账丢弃缓存明细,测试仍然通过,缓存写入路径也始终为零。 新增两条回归: - 生成链路:假 provider 上报 input=500 / read=1800 / write=1200,断言累计得 3500, 即必须走 TotalInputTokens,否则账本低估会让 compact 永不触发 - 对账链路:已观测时必须用观测值而非估算值,且缓存明细透传到 ledger 与 token_usage 两个事件
- 字段说明从"留空等同 auto"改为按 driver 取默认值:anthropic 默认 auto,其余默认 off - 映射表补充 responses 模式同样发送路由键,并新增五个自研 driver 不发送的说明 - 补一段默认值差异的原因:cache_control 是官方字段不会被拒,prompt_cache_key 是可选 扩展参数,未确认支持前默认发送会让兼容网关返回 400
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #733
这是 #606(Prompt 缓存 Phase 1,已关闭)规划的第二阶段。Phase 1 做完了 provider-neutral 的请求结构优化(stable/dynamic 分离、工具稳定排序),并明确把显式缓存留给后续;本 PR 就是那个后续。设计与边界写在 #733,这里只说代码改了什么、reviewer 该看哪里。
改动概览
按主题分组,共 8 个提交,每个提交可独立编译与测试:
feat(provider)fix(context)fix(config)/fix(provider)feat(provider)test(runtime)docs(config)× 21. 缓存契约
公共层只表达一个语义:哪一段系统提示是稳定前缀。不引入厂商字段。
ResolvedStablePrefix()用strings.HasPrefix校验,不成立返回空串;VolatileSystemSuffix()保证前缀 + 尾部按字节还原原文。设计意图是最坏情况只让缓存不生效,绝不发出错误内容 —— 这是选"单切分点 + 可验证前缀"而不是分块数组的理由。2. 映射与默认值
anthropiccache_control: ephemeral;稳定前缀不足 1024 token 不打标openaicompatchat completions与responses均发送prompt_cache_key(模型 + 稳定前缀 + 工具名集合的 sha256 前 32 hex)prompt_cache: autogeminideepseek/qwen/glm/mimo/minimaxauto也不发)默认值差异的原因:
cache_control是 Anthropic 官方协议字段,端点不会拒绝;prompt_cache_key是 OpenAI 的可选扩展参数,而openaicompat服务任意兼容网关,未确认支持前发送可能让原本可用的部署返回 400,而现有 stream fallback 只处理流式解析失败、不处理 HTTP 参数拒绝。没有做"识别 400 后剥离字段重试":错误文本匹配脆弱,且会把真实的 400 掩盖成无意义重试。代价是 OpenAI 官方用户需显式开一次。
给 tools 单独打标是因为 Anthropic 的缓存前缀顺序是
tools → system → messages:只在 system 上打,规则文件一改 tools 的缓存也跟着废。3. 两处必须同批落地的配套修正
(a) 预算链路必须用含缓存的总输入
Anthropic 的
input_tokens不含缓存,OpenAI 的prompt_tokens含。provider_stream.go原先直接取Usage.InputTokens进账本:Anthropic 返回input_tokens=500, cache_read=18000时账本只累计 500,TokenInputTotal严重低估,compact 永不触发、预算门禁失效,全链路无报错。现在统一为"未命中缓存的输入"(OpenAI 侧减去
cached_tokens,并把它收敛到[0, prompt],保证总输入不超过上报量),账本改用Usage.TotalInputTokens()。(b) memo 索引必须移出稳定段
memo 是 agent 自己写的记忆,记一条就变一次 —— 放进 stable 等于每写一条记忆就摧毁整个缓存前缀。已归入动态段。原测试
TestDefaultBuilderBuildMemoIsStable锁定的正是这个行为,已改名并反转断言。4. 五家自研 driver 为什么不发路由键
chatcompletions.Request是 DeepSeek / Qwen / GLM / MiMo / MiniMax 自研 HTTP 实现共用的中间结构,字段会被直接json.Marshal进请求体。它们有各自的缓存机制(DeepSeek 按前缀全局命中),发路由键要么被忽略、要么被严格网关拒绝。5. 可观测性
token_usage与ledger_reconciled新增cache_read_input_tokens/cache_write_input_tokens。没有这两个字段,缓存是否生效只能靠猜。行为变更(请 reviewer 重点看)
Usage.InputTokens在 OpenAI 兼容 provider 上会变小,因为扣除了缓存命中部分。总输入由TotalInputTokens()表达。若有客户端直接拿input_tokens展示"输入 token",数字会变,需要改用总量或补上缓存明细。这是统一两家口径的必然代价。providers配置新增prompt_cache: auto|off,支持写入provider.yaml并随读写往返保留;非法值在加载阶段报错。留空时按 driver 取默认值(见上表)。openaicompat端点,请求体与改动前完全一致。测试
新增 11 个测试文件。重点覆盖:
auto也不能让自研厂商发送prompt_cache可加载、非法值被拒cached_tokens归一化与区间收敛(全命中 / 超额 / 负数)本地
go build ./...通过;internal/provider/...、internal/config、internal/context/...、internal/runtime全绿。本机(Windows)另有 3 个失败与本次改动无关,已用
git stash在干净树上复现:TestMarshalPayloadJSONSchemaMatchesGeneratedFile、TestBuildFrontendAndReadGatewayToken、TestExecutorRunOverheadWithinThreshold(性能阈值 flaky)。未覆盖
TTL 配置、TUI/Web 命中率展示、评测基线纳入成本指标 —— 都在 #733 的未勾选项里,不阻塞本 PR。