TL;DR
同一个网关、同一个 Claude Sonnet 模型,换一种接入方式,账单低了 70%。5 轮对照中,openai-completions 全程零 cache read / write;anthropic-messages 合计 cache write 19,216 token、cache read 75,627 token,后四轮的输入几乎全部来自 cache。结论一句话:Claude 走 anthropic-messages,不要挂在 OpenAI 兼容口上。
补测没有推翻这个结论:给原 OpenAI 配置补上 cacheControlFormat: "anthropic" 后,仍是零 cache read / write。但该组未开启 session affinity header,所以只能说「缺少 session affinity 时,仅补客户端配置项无效」——cache_control 是否被网关丢弃、请求前缀是否被重排,根因尚未定位。本文结论仅限这条链路:Pi 1.5.1 + 该网关 + 该模型。
处理方式很简单:为 Claude 单独建立一个使用 anthropic-messages 的 provider,配置见第五节。
一、引入:Pi 上 Claude 从未命中 cache
混合模型网关(multi-model gateway)通常会把多个厂商的模型整合到同一个服务中。接入时,一种常见做法是统一使用 OpenAI 兼容接口,再通过 model ID 区分 GPT、Claude 等模型。我目前使用的就是这样的第三方网关。在 Claude Code 上运行没遇到什么问题,最近准备试试 Pi Agent,但实际切换之后发现,同样的任务在 Pi 上计费明显偏高,细查后确认:Pi 上的 Claude 模型从未命中 prompt cache。
环境版本:
- Pi 1.5.1
- pi-models-discovery 1.6.1
同一网关、同一个 Claude Sonnet 模型,两种接入方式的差异在哪?为此我在 Pi 中配置了 openai-completions 与 anthropic-messages 两条路径。两者都能正常多轮对话,测试中的答案也都正确,但 cache 统计和费用出现了明显差异。
本文记录这次排查过程,重点讨论混合网关中 Anthropic 模型的配置注意事项。文中的网关地址和 provider 名称均为示例。
二、问题分析:回答正常,cache 为什么可能没有生效
1. 模型 ID 与请求协议是两个配置维度
model ID 决定请求选择哪个模型,api 则决定 Pi 使用哪一种 protocol adapter 组织请求、解析响应。
例如,下面两项配置即使填写相同的 model ID,也会走不同的适配路径:
| Pi 配置 | 协议口径 |
|---|---|
api: "openai-completions" | OpenAI Chat Completions 兼容协议 |
api: "anthropic-messages" | Anthropic Messages 协议 |
网关可以完成消息格式转换,让模型正常回答。至于 cache 控制、cache 用量字段等能力是否同时保留,不能仅凭「支持 OpenAI 兼容接口」判断。
2. 要区分三种「生效」
排查 cache 时,需要分别确认:客户端是否加载配置并生成预期请求,服务端是否创建和读取 cache,以及账单是否体现 cache 计价。
这三个环节需要不同证据。模型回答正确,无法证明 cache hit;配置已经加载,也无法证明网关转发给上游的请求保留了对应字段。
对于 Anthropic Messages,官方提供了 cache_creation_input_tokens 和 cache_read_input_tokens 等用量字段。Pi 将其归一化为 cacheWrite、cacheRead,可以用来观察 cache 创建和读取情况。Anthropic 提示缓存文档
但经过网关后,字段可能缺失或被转换。因此,客户端日志和网关计费记录需要一起看。
三、对照实验:把 cache 差异从其他变量中分离出来
为了降低日常 Agent 工具调用、重试和上下文变化的干扰,我设计了一个固定的多轮测试:
- 两组使用同一个 Claude Sonnet model ID,以及相同的测试资料和用户问题。
- 每组启动一个持续运行的 Pi RPC 进程,在同一个会话中连续完成 5 轮对话。
- system prompt 包含约 64 KiB 的英文合成资料,单轮实际输入约 1.9 万 token。
- 第一轮让模型记住一个词,后四轮从历史中回忆,回答保持简短。
- 每轮完成后等待 2 秒,再发送下一轮。
- 关闭 thinking、tools、extensions、auto-retry、context compaction 和后台 cache prewarm。
每次运行还会在 system prompt 开头加入一个独立随机标记。标记在本组内保持不变,用来降低不同组、不同次测试之间的 cache 复用干扰。它不能保证清空服务端已有 cache,首轮状态仍要看实际用量。
每组同时保存实际选择的 provider、model、API 类型,逐轮 token 用量及 RPC 事件。网关侧则记录运行前后的请求数、cache 计数和费用增量。
这里有一个实验边界:两套配置并不对等——Anthropic 配置额外开启了 session affinity header,OpenAI 配置没有。因此,这次首先比较的是两套配置的实际效果,尚不能把所有差异单独归因于协议。
第一组与第二组:差异同时出现在日志和账单中
| 5 轮合计 | OpenAI 兼容配置 | Anthropic Messages 配置 |
|---|---|---|
| 未命中 cache 的输入 token | 95,196 | 328 |
| cache write token | 0 | 19,216 |
| cache read token | 0 | 75,627 |
| 全部输入 token(含 cache) | 95,196 | 95,171 |
| 输出 token | 19 | 19 |
| 网关「原价金额」增量 | ¥1.36 | ¥0.41 |
两组的全部输入规模几乎相同,输出量也完全一致。Anthropic 组的未命中输入很少,是因为大部分输入计入了 cache read / write,并不意味着它少发送了上下文。
Anthropic 组首轮 cache write 18,859 token,后续轮次因新增消息又写入 357 token,合计 19,216 token。后四轮的 cache read 分别为 18,829、18,864、18,933、19,001 token,合计 75,627 token,占该组全部输入 token(95,171)的 79.5%;全部输入中未命中 cache 的只有 328 token。
OpenAI 组则连续五轮都没有观察到 cache read / write。
网关侧的请求数各自增加 5 次:OpenAI 组的 cache 统计显示值不变;Anthropic 组的 cache write 计数增加约 0.02M、cache read 计数增加约 0.08M,与本地日志相符。
按网关展示的「原价金额」(按官方牌价折算、不含折扣)增量计算,Anthropic 组本次约低 70%。这是一个包含首轮 cache write 成本的五轮短测试结果,不应直接当作其他业务的固定节省比例。网关累计数字存在舍入,费用增量也不能替代精确的逐笔账单。
四、进一步排查:只补 cache_control 是否足够
看到差异后,一个值得验证的假设是:OpenAI 兼容配置没有生成 Anthropic 格式的 cache 标记。
核对 Pi 的实现可以看到,OpenAI Completions adapter 支持通过以下兼容项生成 Anthropic 格式的 cache 控制:
{
"compat": {
"supportsDeveloperRole": false,
"cacheControlFormat": "anthropic"
}
}
其中,cacheControlFormat 是 Pi 的客户端配置项,adapter 据此在请求中构造 cache breakpoint(即 cache_control 字段);它不是让所有网关自动支持 cache 的通用开关。Pi OpenAI Completions 适配源码
于是,我只给原 OpenAI 配置下的测试模型添加这一项,保留其他设置,再执行第三组测试。
三组配置的变量对照
| 组 | api | session affinity header | cacheControlFormat | cache hit |
|---|---|---|---|---|
| 1 | openai-completions | 无 | 无 | 无 |
| 2 | anthropic-messages | 有 | 不适用 | 有 |
| 3 | openai-completions | 无 | anthropic | 无 |
| 4 | openai-completions | 有 | anthropic | 未测 |
Pi 返回的实际模型配置确认新设置已经加载。结果却没有改善:
| 5 轮合计 | 原 OpenAI 配置 | 添加 cacheControlFormat 后的 OpenAI 配置 |
|---|---|---|
| 未命中 cache 的输入 token | 95,196 | 95,181 |
| cache write token | 0 | 0 |
| cache read token | 0 | 0 |
| 输出 token | 19 | 19 |
| 网关「原价金额」增量 | ¥1.36 | ¥1.36 |
第三组的网关请求数同样增加了 5 次,未命中 cache 的输入增加约 0.10M,cache 统计显示值不变。
这次补测说明:在缺少 session affinity 的前提下,仅添加该客户端配置项,对当前链路仍不足以获得 cache 收益。
原因是哪一个,现有数据还区分不了:一是网关丢弃或转换了 cache_control 字段;二是网关把请求路由到了不同上游,而 Anthropic 的 cache 按 organization / workspace 隔离,不同上游之间本就不共享。第三组没开 session affinity header,第二组开了,这正好是两组结果的差别所在。要坐实根因,需要一个同时开启 session affinity header 与 cacheControlFormat 的第四组,并抓取原始 HTTP 请求。
现有日志是 Pi RPC 事件记录,并非原始 HTTP 抓包,所以还不能确认 cache 标记在线上请求中如何被处理。
五、问题解决:采用已经验证有效的 Anthropic Messages 配置
这次实践中,验证有效的处理方式,是为 Claude 模型单独建立使用 anthropic-messages 的 provider,而不是继续沿用当前网关的 OpenAI 兼容配置。
在 Pi 的 ~/.pi/agent/models.json 中,可以使用如下结构:
{
"providers": {
"gateway-anthropic": {
"baseUrl": "https://gateway.example.com",
"api": "anthropic-messages",
"apiKey": "$GATEWAY_API_KEY",
"models": [
{
"id": "YOUR_CLAUDE_MODEL_ID"
}
]
}
}
}
这里展示的是需要合并进现有文件的配置结构,不应直接覆盖已有 providers。运行前,将 model ID 和示例地址替换成网关实际提供的值,并设置环境变量 GATEWAY_API_KEY。
baseUrl 要与客户端的路径拼接规则匹配。本次测试中,OpenAI adapter 使用带 /v1 的基础地址,Anthropic adapter 使用网关根地址;其他网关可能存在额外路径前缀,应以它的接入文档和最终请求 URL 为准。
调用时显式选择 provider 和模型,避免同一个 model ID 同时存在于多套配置中时选错入口:
pi --provider gateway-anthropic --model YOUR_CLAUDE_MODEL_ID
Pi 的 Anthropic adapter 在默认 short cache 策略下会生成 cache breakpoint,但服务端是否命中,仍然要通过请求结果验证。Pi Anthropic Messages 适配源码
验收时,观察首轮是否产生 cache write、后续轮次是否出现 cache read,再核对网关统计和费用。就本次案例而言,Anthropic 配置在这几个方面都有一致证据。

