OrzLee
OrzLee
EMBER GLASS · 2026
0%
正在点燃余烬…
OrzLee
首页
留言
动态
归档
推荐
音乐
工具
皮肤
神经虚空
VOID
熔岩玻璃
EMBER
随机
RANDOM
Search
1
Emby公益服-上万部电影电视剧免费看
80,574 阅读
2
openwrt-docker部署lxk0301京东自动签到脚本
14,224 阅读
3
QuantumultX-京东签到撸京东豆
12,166 阅读
4
Nginx SNI 分流(端口复用)使用Xray+VLESS+XTLS
10,898 阅读
5
LXK0301京东签到脚本-自动提交互助码
10,734 阅读
随便写写
科学上网
Web开发
瞎折腾
Search
标签搜索
quantumultx
laravel
openwrt
laravel nova
laradock
telegram
DDC/CL
薅羊毛
google adsense
提示缓存
jd_scripts
openCode
京东签到
ubuntu
oh-my-zsh
web开发环境
nginx
shadowsocks shadowsocksR
RBAC
billion-context
orzlee
累计撰写
49
篇文章
累计收到
604
条评论
首页
栏目
随便写写
科学上网
Web开发
瞎折腾
页面
留言
动态
归档
推荐
音乐
工具
搜索到
1
篇与
的结果
2026-11-13
CPA 中转Claude:openCode主会话 1h 缓存保活、子代理 5m 缓存
CPA 中转:主会话 1h 缓存保活、子代理 5m 缓存 OpenCode v2 + 自建 Claude 中转(CPA)上,缓存 TTL 到底谁说了算:思路、验证方法、配置和踩坑,留给以后的自己查 前言 先放结论:在 OpenCode v2 + CPA 这条链上,「主会话缓存 1 小时并保活、子代理缓存 5 分钟」我已经配出来了,配置层和中转层都实测过。但还没开一个新会话完整跑一遍真实对话,保活在真实环境里也只触发过一次,所以这篇是思路和步骤的笔记,不是「已稳定跑通」的战报。没验证的我单独放在「没验证 / 没做完」那一节。 我的目的很朴素:Claude Sonnet 5.5 当主会话,想让缓存尽量多被「读」到、少被重新「写」。办法是让主会话的提示缓存(把前面发过的长前缀存起来,下次只读不重算)活够久,并且快过期前有人帮它续一下;子代理活儿短,5 分钟的缓存就够。 先把几个简称说清楚,后面不再解释: CPA:CLIProxyAPI,我自建的 Claude 订阅中转,开了 Claude Code CLI 指纹模式。 bili:billion-context,OpenCode 的上下文管理插件。 omo slim:oh-my-opencode-slim,给 orchestrator、oracle 这些角色分别指定模型的插件。 CPA 中转:主会话 1h 缓存保活、子代理 5m 缓存 前言 先看结论:最终配置要点 Warming 是什么:先读文档和源码,别靠猜 协议路径:用 OpenAI 包设 TTL,等于没设 CPA 指纹:1 小时是中转「送」的,5 分钟要靠请求头换 分流方案:同一个中转,配两个 provider V2 原生写法:顺手把配置整体搬过来 验证:配置层看什么,保活怎么盯 停止保活:退出 TUI 不管用 踩过的小坑 旧会话报 Model unavailable /model 选择器里分不清哪个是主会话那一路 缓存匹配规则:这是理解,没有现场验证 没验证 / 没做完 结语 参考与外链 先看结论:最终配置要点 项目 主会话那一路 子代理那一路 provider 名(本文示例) claude_orz claude_orz_sub 协议包 @opencode/ai/providers/anthropic(走 /v1/messages) 同左 settings.cache.ttl 1h 5m 保活 interval: auto,duration: 2 hours 不配 请求头 不加 固定加 X-Claude-Code-Agent-Id omo slim 里给谁用 orchestrator oracle、designer、fixer 缓存写入倍率(相对普通输入) 2× 1.25× 计费倍率按 Anthropic 官方口径(我没另外核对):5 分钟缓存写入 1.25×,1 小时缓存写入 2×,读取 0.1×(Sonnet/Opus 5.5 是 0.05×,Fable 是 0.025×)。所以 1 小时档写一次更贵,要靠后面反复「读」把它摊薄,这也是为什么它要配保活:写得贵,就得让它活久一点、读得多一点。 下面按我折腾的顺序写:先搞懂 Warming,再踩协议路径的坑,再发现 1 小时其实是中转「送」的,然后分流、改 V2 写法、验证、学会怎么停。 Warming 是什么:先读文档和源码,别靠猜 官方文档在这里:OpenCode Warming。我是对着文档和源码一起看的,下面是我整理出来的规则。 保活不会让缓存「不过期」,它是在缓存快过期之前自动再发一次请求,让计时从头开始。几条要记住的规则: 只作用于顶层会话,子代理会话被跳过。这是官方设计,源码里按 parentID 判断。所以子代理本来就不会被保活,5 分钟就是它的自然状态,不用管。注意这个「自然状态」依赖子代理那一路固定带 X-Claude-Code-Agent-Id;不带头的话,在我这台中转上会被抬成 1h(见下文「CPA 指纹」一节)。 duration 是总窗口,从「最近一次真实请求发出时」开始算,保活本身不延长窗口。 interval: auto 取缓存 TTL 的 0.9 倍,1 小时缓存就是 54 分钟一次。如果 0.9×ttl 已经 ≥ duration,auto 会直接跳过。 保活响应里缓存读取 < 缓存写入,就判定未命中,立刻停止后续保活。 配置优先级:模型的 settings.cache.warming > provider 级 > 顶层 warming。 variant 里写的 cache 设置会被丢弃,所以没法用 variant 区分 TTL,只能靠不同的 provider。 每次真实请求、压缩、generate 都会重置窗口;保活本身、子代理请求、Esc 不会。 关闭要显式写 cache.warming: false。删掉字段会回退到上层默认,"0" 也不是有效的关闭写法。 我用 duration: 2 hours、interval: auto(54 分钟)算了一下:第 54、108 分钟各触发一次,第 162 分钟已经超出窗口,不触发。缓存最长撑到最后一次保活后再过 1 小时,即约 168 分钟(2 小时 48 分)。如果写成 duration: 1 hour,只会保 1 次。这是按规则推算的,不是我等满 168 分钟看到的。 协议路径:用 OpenAI 包设 TTL,等于没设 我最初的 provider 用的是 OpenAI 包(Responses 协议)。这条路径上,缓存设置只会变成请求里的 prompt_cache_options 字段,实测怎么写 TTL 都设不出 1 小时;翻日志,这一路清一色是 /v1/responses。这个字段具体在哪一层被丢弃,我没完全查清(读中转源码时看到它会剥掉该字段,但没有逐层验证)。 解决办法是换成 Anthropic 包(@opencode/ai/providers/anthropic),请求走 /v1/messages。 想控制 TTL,先确认请求实际走的是哪个协议端点,再去纠结配置写没写对。这一步我白白多绕了一圈。 CPA 指纹:1 小时是中转「送」的,5 分钟要靠请求头换 换成 Anthropic 包以后我又冒出一个疑问:设的 TTL 真的生效了吗?我干脆绕过 OpenCode,直接拿 Anthropic Messages 请求打中转,看响应 usage 里 cache_creation 的 5m/1h 拆分,一项一项试。 请求写法 响应里记成 显式写 ttl 1h 1h 显式写 ttl 5m 1h 不写 ttl 1h 请求里加 prompt_cache_options 字段 1h(不报错但没用,原因没查清) 换 User-Agent、换 beta 头、带不带 tools 1h 加请求头 X-Claude-Code-Agent-Id + 显式 5m 5m 加请求头 X-Claude-Code-Agent-Id + 不写 ttl 5m 加请求头 + 显式 1h 没测 只有加上 X-Claude-Code-Agent-Id(Claude Code 用来标记「这是子代理」的头)以后,5m 才真的记成 5m。我接着读了 CPA 的源码印证:它在 CLI 指纹模式下,对「非子代理」请求,会给没写 ttl 的缓存标记补成 1h,还会重写首条用户消息、丢掉显式写的 TTL;带了子代理头就不补。 所以结论有点反直觉: 主会话 1h、子代理 5m 这套行为,是 CPA 的指纹逻辑给的。 不是 bili 干的,也不是 OpenCode 里 ttl 配置直接决定的。至少在我这台中转上,显式写 5m 也会被抬成 1h。 想要 5m,得靠加子代理头得到。 bili 这边我也查了配置:只开了 promptCache.routing(一个让缓存键保持稳定的开关),它不改 TTL,它自动打的缓存断点也不带 ttl。bili 自带的 cacheKeepalive 默认是关的,别和 OpenCode 的 Warming 同时开:两套机制互相不知情,不会去重。 自己复查可以照下面的思路做。这只是示意,地址和 key 换成你自己的,不是我当时的原命令: # 示意:向中转的 /v1/messages 发一个带 cache_control 的长系统提示请求 curl -s https://<你的中转地址>/v1/messages \ -H "x-api-key: <你的中转 key>" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-5-5", "max_tokens": 16, "system": [{ "type": "text", "text": "<一段足够长的系统提示>", "cache_control": {"type": "ephemeral", "ttl": "5m"} }], "messages": [{"role": "user", "content": "hi"}] }' 同样的请求连发两次,看 usage 里: cache_creation.ephemeral_5m_input_tokens / ephemeral_1h_input_tokens:第一次写入记在了哪一档; cache_read_input_tokens:第二次有没有读到。 想验证「子代理头」的效果,就在第二组请求里多加一行 -H "X-Claude-Code-Agent-Id: opencode-subagent",对比 5m/1h 两栏。 分流方案:同一个中转,配两个 provider 既然 5m 要靠请求头,主会话不能带头,子代理必须带头,那就同一个中转地址配两个 provider: 主会话那一路:缓存 ttl: 1h + 保活(interval: auto、duration: 2 hours),不加子代理头。 子代理那一路:缓存 ttl: 5m,不配保活,固定加请求头 X-Claude-Code-Agent-Id。 两个 provider 的模型集一致:sonnet-5-5、opus-5-5、fable-5-2、haiku-5-5。另外我在 Claude 模型上补了上下文/输出上限(context 1,000,000 / output 128,000)和能力声明(文本+图片输入,推理档位 low/medium/high/xhigh/max),否则缺省会回退到 200,000/32,000。 opencode.json(V2 写法,只摘缓存和分流相关的片段,其余省略;配置带注释,所以是 JSONC): { "providers": { // 主会话那一路:1h 缓存 + 保活,不带子代理头 "claude_orz": { "package": "@opencode/ai/providers/anthropic", "name": "Claude 主会话 (1h)", "settings": { "baseURL": "https://<你的中转地址>/v1", "apiKey": "<你的中转 key>", "cache": { "ttl": "1h", "warming": { "interval": "auto", "duration": "2 hours" } } }, "models": { "claude-sonnet-5-5": { "name": "Sonnet 5.5 · 主会话", "limit": { "context": 1000000, "output": 128000 }, "capabilities": { "tools": true, "input": ["text", "image"], "output": ["text"], "reasoning": { "supported": true, "effort": ["low", "medium", "high", "xhigh", "max"] } } } // opus-5-5 / fable-5-2 / haiku-5-5 写法相同,省略 } }, // 子代理那一路:5m 缓存,无保活,固定带子代理头 "claude_orz_sub": { "package": "@opencode/ai/providers/anthropic", "name": "Claude 子代理 (5m)", "headers": { "X-Claude-Code-Agent-Id": "opencode-subagent" }, "settings": { "baseURL": "https://<你的中转地址>/v1", "apiKey": "<你的中转 key>", "cache": { "ttl": "5m" } }, "models": { "claude-sonnet-5-5": { "name": "Sonnet 5.5 · 子代理", "limit": { "context": 1000000, "output": 128000 }, "capabilities": { /* 同上,省略 */ } } // opus-5-5 / fable-5-2 / haiku-5-5 写法相同,省略 } } // 另一个走 OpenAI 协议的 provider(explorer 等用),不参与缓存方案,省略 } } oh-my-opencode-slim.json(只摘角色到模型的指向,skills、mcps、prompt 等省略): { "preset": "claude", "presets": { "claude": { "orchestrator": { "model": "claude_orz/claude-sonnet-5-5", "variant": "high" }, "oracle": { "model": "claude_orz_sub/claude-opus-5-5", "variant": "high" }, "designer": { "model": "claude_orz_sub/claude-sonnet-5-5", "variant": "high" }, "fixer": { "model": "claude_orz_sub/claude-sonnet-5-5", "variant": "high" }, // explorer 这类轻量角色用另一个走 OpenAI 协议的中转,不参与缓存方案 "explorer": { "model": "openai_orz/gpt-6.1-sol", "variant": "low" } } }, "agents": { // fast-generic 同理,保持用 OpenAI 协议那一路 "fast-generic": { "model": "openai_orz/gpt-6.1-sol", "variant": "low" } } } V2 原生写法:顺手把配置整体搬过来 分流的时候我顺手把整份配置改成了 OpenCode V2 原生写法。V2 会在内存里把 V1 写法规范化,不会改写你的源文件,也没有 opencode migrate 这类子命令,所以只能手改。对应关系: V1 V2 provider providers npm package @ai-sdk/anthropic @opencode/ai/providers/anthropic @ai-sdk/openai @opencode/ai/providers/openai(默认就是 Responses,别改成 chat) options.baseURL / apiKey / cache settings.* options.headers provider 同级的 headers model 的 options model 的 settings variants(对象) variants(数组,每项 id + settings) plugin plugins(别和 V1 的 plugin 并存) 顶层 server 块 V2 直接忽略,已删 我的流程是:改之前先备份;改完用能解析 JSONC 的解析器校验(配置里有注释,普通 JSON 解析会被注释卡住);再做两项验收: opencode debug config:看有没有警告; opencode api model.list:看模型和它们的 settings。注意这个输出带凭据,要先用程序过滤,只留你要看的字段,别直接打在屏幕上。 验证:配置层看什么,保活怎么盯 模型层面我核对过:model.list 里主会话那一路 settings.cache 是 ttl 1h + warming auto / 2 hours,没有额外请求头;子代理那一路是 cache 5m + 头 X-Claude-Code-Agent-Id。 实时观察保活,去 OpenCode 日志目录下的 opencode.log,tail -F 并过滤关键字: tail -F <OpenCode 日志目录>/opencode.log | grep -E "warming|failed to warm session" 关键句有四个: scheduled session warming:给某个会话排上了保活; warming session:这一次保活发出去了; stopped warming session after a cache miss:判定未命中,停止后续保活; failed to warm session:保活本身失败。 保活请求的特征:末尾带一条临时用户文本「保活,仅回复 OK」,没有专用请求头。另外,没有「列出当前活跃保活循环 / 剩余窗口」这样的接口,想知道还剩多久,只能自己按规则算。 停止保活:退出 TUI 不管用 这一条我经常忘。退出 TUI 停不了保活,定时器在后台服务里。只有删会话才会取消该会话的保活循环;Esc、归档、切模型、移目录、fork 都不会取消。 opencode session export <id> > backup.json # 先导出备份 opencode session delete <id> # 删会话才会取消该会话的保活循环 opencode service status # 全局后台服务:status / restart / stop opencode service restart opencode service stop 配置里关闭则显式写 cache.warming: false。要注意,热加载后旧循环仍然带着旧参数,要等下次请求,或者 service restart 才会清掉。 如果想让某个会话按新配置重新判定,可以 opencode run --session <id> "…" 发一次请求,但必须先把 warming 关掉,否则这次请求本身会重置、延长窗口,多出一次真实请求。 踩过的小坑 旧会话报 Model unavailable 改完配置后,旧会话报 Model unavailable: <被我删掉的模型>。原因是会话里保存的模型优先于 agent 配置,而旧会话存的正好是我已经删掉的模型。处理:开新会话,或者把这个会话的模型改回现有的。 /model 选择器里分不清哪个是主会话那一路 选择器标题只显示模型的 name,provider 名只出现在分组标题或搜索态下,两个 provider 的同名模型很难区分。我的处理是:给 8 个 Claude 模型的 name 都加上「主会话 / 子代理」字样,provider 也起了带「1h」「5m」的 name。provider 没法在选择器里隐藏,把模型设成 disabled 会让 omo slim 引用不到,所以这条路走不通。 缓存匹配规则:这是理解,没有现场验证 以下是我对 Anthropic 官方规则的理解,没有现场验证,当「排查线索」用: 缓存按「前缀内容」匹配,不按会话。键是从头到某个断点的内容(工具定义 → 系统提示 → 历史消息);同账号体系下,前缀逐字相同,别的会话也能命中。 一个会话的每个断点各留一份缓存。短前缀读不到长缓存;长请求可以往回找,命中更短的已缓存前缀(往回约 20 个内容块)。 工具、系统提示、历史(折叠、压缩、改写)或模型变了,变化位置之后就读不到。 缓存按账号隔离。中转如果在多个账号间轮换,换账号就读不到。我没查我的中转有几个账号。 没验证 / 没做完 还没开一个新会话完整跑一遍「主会话写入 1h、子代理 5m、请求走 /v1/messages」的真实对话。目前验证到的是配置层和中转层的实测。 保活在真实环境只触发过一次,且那一次被判为未命中、随即停止,原因还没查清。手上只有一条未证实的线索,属于推测:保活走的可能是另一条生成路径,omo slim 的系统提示/历史转换看起来只注册在正常请求的钩子上,保活不经过它;而闲置之后第一笔请求,折叠标识和系统提示似乎会被重新构造。这些都是读源码得到的印象,没有拿两次请求逐字对比过。保活能否稳定命中,我还在观察。 CPA 对「带子代理头 + 显式 1h」会落到哪一档,没实测(我测的是带头时显式 5m 和不写 ttl,都是 5m)。 prompt_cache_options 字段在中转上无效的原因,没查清。 中转有几个 Claude 账号、是否轮换,没查。 结语 回头看,这次实打实做成的是分流:主会话 1h 缓存加保活、子代理 5m 缓存,配置层和中转层都对上了。保活在真实对话里能不能稳定命中,我还在观察,详细没验证的清单不重复了,都在上面的「没验证 / 没做完」里。 下一步就是补第一条:开个新会话,主会话和子代理各跑几轮,对着 usage 看 5m/1h 两栏和读取量,再盯着日志看保活那一次到底会不会命中。如果你也在 CPA 上试过「带子代理头 + 显式 1h」,结果是哪一档,欢迎告诉我。 保活这东西最气人的一点,是它在后台悄悄转,而我连「现在有几个循环在转」都查不到。 参考与外链 OpenCode Warming 官方文档:保活的窗口、间隔、未命中判定。 billion-context(bili):上下文管理插件与它自带的缓存保活。
2026年11月13日
1 阅读
0 评论
0 点赞