跳到正文

Field entry

混合模型网关中的 Claude 配置:一次 Prompt cache 失效排查

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 的输入 token95,196328
cache write token019,216
cache read token075,627
全部输入 token(含 cache)95,19695,171
输出 token1919
网关「原价金额」增量¥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 配置下的测试模型添加这一项,保留其他设置,再执行第三组测试。

三组配置的变量对照

组apisession affinity headercacheControlFormatcache hit
1openai-completions无无无
2anthropic-messages有不适用有
3openai-completions无anthropic无
4openai-completions有anthropic未测

Pi 返回的实际模型配置确认新设置已经加载。结果却没有改善:

5 轮合计原 OpenAI 配置添加 cacheControlFormat 后的 OpenAI 配置
未命中 cache 的输入 token95,19695,181
cache write token00
cache read token00
输出 token1919
网关「原价金额」增量¥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 配置在这几个方面都有一致证据。

文章声明

除特别注明外,本博客文章均为个人原创。转载、摘录、总结、改编或引用相关内容时,请注明作者及来源,并附原文链接:https://blog.harumonia.moe/posts/2026-11-15-multipie-model-gateway-in-pi。未经许可,不得用于商业用途或歪曲原意。