Diagnose and fix prompt-cache failures when Pi talks to an OpenAI-compatible relay API. Three commands:
/cache-check,/cache-fix,/cache-restore.
用第三方中转 API(聚合站、网关、自建代理)跑 Pi 时,账单经常比预期高好几倍, 原因几乎总是同一个:prompt cache 没生效,每一轮都在按全价重算前缀。
难点不在于修,在于定位是谁的问题——配置写错了?Pi 没发缓存标记?还是上游不认? 这三件事的修法完全不同,靠猜会绕很久。这个包把它们拆成三层,一条命令一次答完。
── L1 有效配置 ───────────────────────────
✓ api = openai-responses(自动前缀缓存 + prompt_cache_key)
✓ 缓存 TTL long (ttl:"1h")
── L2 线上请求 ───────────────────────────
✓ 请求里有 3 个 cache_control 断点(pi 原生): system[0], user[4], tools[3]
── L3 本会话实际用量 ─────────────────────
input 485 cacheRead 17,920 命中 97.4%
✓ 缓存生效
pi install git:github.com/Song-ic/pi-cache-check先试不装(临时加载,只对本次运行生效):
pi -e git:github.com/Song-ic/pi-cache-check/cache-check → L1 ✗ 配置不对
/cache-fix → 看改动 → 确认 → 自动备份 → 热生效(不用重启 pi)
(说句话,触发一次请求)
/cache-check → L2 出现断点 / L3 的 cacheRead > 0 → ✓ 缓存生效
同一个 OpenAI 兼容上游,缓存有两条完全不同的路,别把其中一条当成唯一答案:
A. openai-responses(先试这条) |
B. openai-completions + 断点 |
|
|---|---|---|
| 机制 | 服务端自动前缀缓存 | 显式 cache_control 断点 |
prompt_cache_key |
Pi 恒发 | 默认不发(仅官方直连或 TTL=long) |
需要 compat |
不需要 | 需要 cacheControlFormat: "anthropic" |
| 多后端账号池下 | key 把同一会话粘到同一后端 | 每轮随机分发,命中率随池子大小塌方 |
prompt_cache_key 是决定性差异:聚合站/中转商背后往往是多个后端账号轮询,
缓存写在 A 号、下一轮被派到 B 号就 miss。实测同一上游同一时段:
A 路 93.9%(6 轮全中,82 秒空闲后仍命中),B 路 16~39%(间歇性 miss/hit)。
排查顺序:上游支持 responses 就走 A;上游不支持 responses 或不对它做自动缓存时,
才落到 B(/cache-fix 修的就是 B 路的配置)。/cache-check 认得这两条路,
不会在 A 路上要求断点,/cache-fix 也绝不会把 A 路降级成 B 路。
不发额外请求、不花额外 token,搭你正在跑的请求的车。三层各自指向不同责任方:
| 层 | 看什么 | 红了代表 | 该找谁 |
|---|---|---|---|
| L1 有效配置 | api / compat / TTL 档位 / 磁盘与会话是否一致 |
配置不对、被外部工具覆写、或改了还没重启 | 配置 → /cache-fix |
| L2 线上请求 | 真实 payload 里的 cache_control 断点(数量、位置、谁打的) |
配置对但 Pi 没发 | Pi 变了 → 查版本/源码 |
| L3 实际用量 | 本会话每轮 cacheRead / cacheWrite |
请求发对了但上游不认 | 上游 → 找中转商 |
L2 是外部脚本做不到的一层:它直接看线上请求长什么样,而不是从结果反推
(靠 Pi 的 before_provider_request 钩子;诊断本身只读,不改 payload)。
判定是四态的,两条设计原则都是从真实翻车里学来的:
✓ 生效/✗ 未生效/⚠ 能用但易失效(如 TTL 太短)/? 尚未验证- 没有证据 ≠ 通过:L2/L3 没数据时报"尚未验证",不假绿
- 会失效 ≠ 没生效:TTL 短报"易失效",不假红;第 1 轮 cacheRead=0 是建缓存,不算失败
失败时的提示按层级给方向:配置有问题 → /cache-fix;配置全对仍不命中 →
指向换策略或找上游,不会让你去"修"一个没毛病的配置。
L2/L3 需要本会话发过至少一次请求。刚进 pi 就跑的话,说句话再跑一次。
/cache-check 的 L1 红了就跑它。修两样东西,全部先展示改动 → 等确认 → 先备份 → 热生效:
① provider 配置(B 路时)—— api 设为 openai-completions、
compat.cacheControlFormat 设为 anthropic。之后 Pi 用官方机制在
system prompt / 最后一个 tool / 最后一条对话消息三处打 cache_control 断点。
走 A 路(responses)的 provider 本身就是对的,不做任何改动。
② 缓存 TTL 档位 —— 写进 cache-check.json,立即生效且每次启动自动应用。
TTL 为什么和 compat 一样要紧。 实测某中转上游默认 TTL 只有约 30 秒, 且从写入起算、读取不续期。背靠背连打能看到 97% 命中,但真实写码时你会看输出、 会思考,两轮间隔一两分钟是常态——每次都跨过 TTL,命中率塌回 30% 上下。
PI_CACHE_RETENTION=long让断点带上ttl:"1h";对照实验(同 provider、同协议、 同样静默 45 秒,只差这一个变量):不开 → 全量重发;开 → 97.1% 命中。代价:长 TTL 的缓存写入通常更贵(Anthropic 官方口径 2x vs 1.25x 默认档)。 读取折扣一般能盖过它,但你的中转商如何计价请核对账单——所以这一项会单独告知并等你确认。
不碰 apiKey、不碰 models、不碰其他 provider。幂等——已经对了就说"无需修改"退出。
需要交互确认,非 TUI 模式下拒绝执行而不是默默改文件。
/cache-fix # 修当前 provider
/cache-fix "My Provider" # 修指定 provider
用最近一次 /cache-fix 的备份覆盖当前 models.json,同样要确认,同样热生效。
配置修好了也可能被外部工具(cc-switch 这类 API 切换器)重写回去;而且 Pi 的 compat
是 per-provider 的,每新增一个 provider 都得重配一次。
所以本包在 before_provider_request 里兜底:走 chat/completions 且请求里一个断点
都没有时,自动补上(位置与 Pi 原生一致)。新 provider 不配也能命中。三条硬约束:
- 只处理 chat/completions——responses 的
input[]里放cache_control会被上游 400 拒绝(实测),宁可不缓存也不能把请求打挂 - 已有断点就不碰,绝不重复打
- 熔断——注入过的请求收到 400 → 立即停止注入并告知
兜底不掩盖根因:L2 会明确区分断点是 pi 原生还是本插件补的,靠补的会被记成
"尚未验证"并提醒去修配置。关掉:PI_CACHE_CHECK_ENFORCE=0。
| 文件 | 内容 | 谁写 |
|---|---|---|
~/.pi/agent/models.json |
provider 的 api / compat |
/cache-fix(改前备份) |
~/.pi/agent/models.json.bak-cachefix-* |
备份 | /cache-fix |
~/.pi/agent/cache-check.json |
{"cacheRetention":"long"} |
/cache-fix |
TTL 改回默认:删掉 cache-check.json。回滚 models.json:/cache-restore。
显式设过 PI_CACHE_RETENTION 环境变量的,优先级高于偏好文件,本包不覆盖。
- 直连 OpenAI 官方 API——自动前缀缓存本来就工作,
prompt_cache_key也自动带 - 上游对
cache_control字段直接报 400 且不支持 responses——客户端改不了,得找中转商 (/cache-check仍能帮你出一份 L1/L2 绿 + L3 红的证据)
改了配置,/cache-check 还是红。
配置是会话启动时解析的,/reload 只重载扩展、换不掉已选定的模型对象;
resume 恢复旧会话同样带着当时解析的配置。L1 的"磁盘配置与本会话不一致"
提示就是在抓这个。解法:会话内 /model 重选一次同款模型(触发重新解析,不丢会话),
或者开新会话。
敲了 /cache-check 没反应,或模型自己回了一份"缓存报告"。
说明命令没注册,斜杠命令被当普通文本发给了 LLM——它可能凭空编一份像模像样的报告,
甚至用它的文件工具替你"执行"命令(实测发生过:模型收到 /cache-restore 后
自己找到备份把 models.json 回滚了)。两种常见原因:
① 包没装上(检查 pi config / settings.json 的 packages);
② 装了两份(比如 pi install 过本地路径又用 -e 加载了一份)——pi 会给重名命令
加 :1/:2 后缀,裸名消失。删掉一份即可。
在 API 切换器(如 cc-switch)里切分组后配置被打回原形。 这类工具给每个 provider 存的是完整快照,切换时整个写回。把它存的快照也更新成 正确配置才能根治;在那之前,本包的兜底注入能保住 B 路的命中(L2 会标注是谁打的断点)。
模型自称 GPT-5 / 别的型号。 模型不知道自己的型号,自我介绍来自训练数据或宿主的 system prompt,不是证据—— 既不能证明中转商换了模型,也不能证明没换。验货看行为,别问名字。
给这类上游做过合成 HTTP 探针的都知道:探针结果可能和真实客户端完全相反 (同一路径探针 0% 命中、真 Pi 97%——TLS 指纹、路由、请求形状都可能不同)。 本包因此坚持只用两种证据:真实请求的 payload(L2)和真实会话的用量(L3)。 如果你打算自己排查,别信 curl 出来的数字。
npm test # 45 个用例,Node ≥22.18 原生剥类型,无构建步骤src/analyze.ts 是纯函数层(payload 解析、配置检查、四态判定、修复计划、过期检测、
断点注入),不 import 任何 Pi 运行时模块,测试直接跑。
extensions/cache-check.ts 只负责注册命令、事件钩子和读写文件。
MIT