---
title: "混合模型网关中的 Claude 配置：一次 Prompt cache 失效排查"
date: 2026-11-15 12:30:00
updated: 2026-11-15 12:30:00
draft: false
author: harumonia
categories:
  - 源流清泉
tags:
  - Claude
  - Pi
  - Prompt Caching
  - Gateway
  - 排查
description: 同一网关、同一个 Claude Sonnet，两种接入方式的 5 轮对照：openai-completions 全程零 cache 读写，anthropic-messages 账单低约 70%。附 session affinity header 这一未受控变量的说明。
---

## 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 + 该网关 + 该模型。

<!-- more -->

处理方式很简单：为 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 提示缓存文档](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)

但经过网关后，字段可能缺失或被转换。因此，客户端日志和网关计费记录需要一起看。

## 三、对照实验：把 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 控制：

```json
{
  "compat": {
    "supportsDeveloperRole": false,
    "cacheControlFormat": "anthropic"
  }
}
```

其中，`cacheControlFormat` 是 Pi 的客户端配置项，adapter 据此在请求中构造 cache breakpoint（即 `cache_control` 字段）；它不是让所有网关自动支持 cache 的通用开关。[Pi OpenAI Completions 适配源码](https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/openai-completions.ts)

于是，我只给原 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` 中，可以使用如下结构：

```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 同时存在于多套配置中时选错入口：

```bash
pi --provider gateway-anthropic --model YOUR_CLAUDE_MODEL_ID
```

Pi 的 Anthropic adapter 在默认 short cache 策略下会生成 cache breakpoint，但服务端是否命中，仍然要通过请求结果验证。[Pi Anthropic Messages 适配源码](https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/anthropic-messages.ts)

验收时，观察首轮是否产生 cache write、后续轮次是否出现 cache read，再核对网关统计和费用。就本次案例而言，Anthropic 配置在这几个方面都有一致证据。

## 文章声明

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