先把问题缩小

很多人看到 cch,第一反应是:这是不是某种随机请求头?不是 Anthropic 在请求里塞了一个用户看不懂的跟踪码?

这两个说法都不够准。公开复盘里展示的形态更像这样:Claude Code 在请求的 system 内容里放入一段归因文本,格式里包含 x-anthropic-billing-header、客户端版本、入口信息,以及一个会随请求变化的 cch 值。它是在一个长得像 header 的文本块,被放进了模型请求体里。

这件事值得写,不是因为 cch 这个名字神秘。问题在于,现代 AI 客户端已经不只是「把 prompt 发给模型」。它会带上版本、入口、运行时、会话、缓存、工具、计费、路由等一堆元信息。只要这些元信息没有被清楚地放在 API 合约里,就会从供应商内部实现细节,变成第三方 API 用户的生产事故来源。

对直连官方服务的人来说,这类字段可能只是「客户端与服务端之间的内部协议」。但对使用第三方 API、反代、LiteLLM、claude-code-router、企业 LLM gateway、Amazon Bedrock 或自建兼容层的人来说,它会穿过一串并不属于原厂控制的系统。每多一层,隐藏字段就多一次出错机会。

它为什么不是小事

一个额外字段是否危险,取决于它改变了什么。

如果它只是一个稳定的、公开的、可忽略的客户端版本号,大多数网关都能处理。真正麻烦的是三点叠加:它是动态的,它跟请求体绑定,它又带有归因或计量含义。

动态字段会让同样的业务请求看起来不再一样。请求体绑定会让它进入缓存键、签名、日志、审计、重放和兼容转换。归因含义会让下游不敢随便删,因为没人知道删了会不会影响计费、功能开关或服务端判断。

这就是第三方 API 用户最容易被坑的地方。他们在维护一条链路:

  • 客户端生成请求。
  • 代理或兼容层转换请求。
  • 网关决定路由、缓存、限流和审计。
  • 云厂商或模型供应商执行推理。
  • 企业侧再把响应、成本和日志汇总回内部系统。

任何一个「看起来只是元数据」的字段,只要进入请求体,就会被这条链路当成业务输入的一部分。它不再是供应商内部字段,而是整个链路都必须理解的协议片段。

最先被打穿的是缓存

大模型 API 的缓存通常不是锦上添花。对长上下文、代码仓库、Agent 任务和企业知识库来说,缓存命中直接关系到延迟和成本。

问题在于,缓存依赖稳定边界。服务端或代理层要判断「这次请求和上次是否共享同一段前缀」,就必须比较请求里的某些内容。比较范围可以不同,有的看消息前缀,有的看系统提示,有的看工具定义,有的还会把模型、参数、header 或用户标识纳入键。实现各有差异,但原则一样:参与缓存键的部分越动态,命中越难。

cch 这类值的风险就在这里。如果它每次请求都会变,而且被放进系统内容,那么下游缓存看到的就可能是「每次都有一点不同的新请求」。即使业务文本没变,缓存键也可能变。

这不是一个纯理论问题。TurboAI 的复盘明确把 CLAUDE_CODE_ATTRIBUTION_HEADER 和代理场景下的 prompt caching 问题放在一起讨论,并给出关闭该归因字段的处理建议。它不是官方规范,但它说明了真实用户在第三方路径里遇到的现象:动态归因字段会让代理和缓存层难以保持稳定判断。

这里需要保持边界:不同服务商的缓存算法不一样,不能简单说「所有缓存都会被 cch 打穿」。更稳的说法是,只要某个缓存层把系统内容、请求体或完整序列化请求纳入缓存键,动态归因字段就会降低命中概率。对第三方 API 用户来说,这已经足够麻烦,因为他们经常不知道哪一层到底用了什么缓存键。

Bedrock 报错暴露了更硬的兼容问题

缓存变差还算温水煮人。Bedrock 这类云厂商网关报 400,就更直接。

GitHub 上有用户在 anthropics/claude-code 仓库提交 issue,标题直指 x-anthropic-billing-header 文本被注入 system prompt 后,在 Amazon Bedrock 上导致 400。issue 描述里的报错信息是:x-anthropic-billing-header 是保留关键字,不能出现在 system prompt 中。作者还强调,这是 Claude Code 内部生成并放入请求体的文本内容。

这个细节很关键。Bedrock 的 InvokeModel API 文档把请求分成 URI 参数、HTTP header 和 body。body 是模型输入与推理参数所在的位置。云厂商网关会对 body 做自己的校验,尤其是面向 Anthropic 模型的兼容格式时,它不只是在转发字节流。

如果一个字段作为 HTTP header 出现,网关可以选择透传、过滤或拒绝。如果它作为系统提示里的文本出现,网关就可能把它当成模型输入的一部分,又因为名字像内部保留字段而触发校验。这种错位会让用户很难排障:从客户端看,它不是用户写的 prompt;从网关看,它已经在请求体里;从模型供应商看,它可能只是客户端归因机制。

这类问题最伤第三方用户。直连官方 API 时,客户端和服务端可能彼此理解这个字段。换成 Bedrock、Vertex、企业代理或 OpenAI-compatible 转换层,原来的默契就没了。

隐藏字段会让代理层两头不是人

代理层最怕的不知道自己该相信谁。

一个 LLM gateway 通常要做几件事:兼容不同供应商格式,隐藏密钥,做配额和计费,记录审计日志,改写模型名,必要时做缓存和重试。它处理的是工程合同,不是聊天体验。

当客户端塞进一个未充分公开的动态字段,代理层马上遇到四个问题。

第一,它该不该透传?透传可能导致下游报错,也可能破坏缓存。不透传又担心破坏官方客户端特性。

第二,它该不该参与缓存键?参与会降低命中;不参与则要确认这个字段不会改变模型输出或服务端行为。

第三,它该不该进入审计日志?进入日志会增加噪声,还可能让内部归因字段扩散到企业日志系统。不进日志则会降低排障可见性。

第四,它该不该被签名?如果某些链路要对请求体做签名、重放保护或幂等判断,动态字段会让「同一业务请求」的判定变得不稳定。

这就是为什么很多兼容层问题看起来像小 bug,实际很难修。它们没有单点责任人。客户端说这是归因和计量,代理说我只是转发,云厂商说请求体不合法,用户只看到同一套 prompt 直连能跑、换个 endpoint 就炸。

cch 争议背后是 Agent 客户端的边界变化

早期 API 客户端比较简单:用户给 prompt,客户端发请求,服务端回结果。现在的 Agent 客户端复杂得多。

它会自动读文件、开工具、做多轮计划、维护 session、调用子任务、注入系统提示、追加工具定义、切换模型、记录遥测,还可能针对不同入口做计量归因。客户端本身已经是运行时,不只是一个 HTTP wrapper。

运行时一复杂,就会自然产生「内部元数据」。供应商想知道请求来自 CLI 还是 IDE,来自哪个版本,是否开启某个能力,是否应该计入某类产品成本,这些诉求并不奇怪。问题是,这些诉求一旦通过请求体里的动态文本实现,就会把内部治理需求转嫁给外部生态。

对第三方 API 用户来说,最危险的不是「供应商做归因」。归因可以合理。危险的是归因字段没有明确边界:

  • 它是不是公开协议的一部分?
  • 它会不会影响模型输出?
  • 它会不会影响缓存?
  • 它能不能关闭?
  • 关闭后是否影响计费、功能、限流或支持?
  • 第三方网关应该透传、改写还是剥离?

这些问题没有答案时,用户只能靠抓包、读 issue、看 debug log 和反复试错。API 越贵、任务越长,这种试错越不可接受。

供应商该怎么做

这里不需要把问题道德化。更好的判断标准是工程合约。

如果一个字段服务于客户端归因、计量或能力开关,它应该有清楚的归属。能放 HTTP header 的,不要伪装成系统提示文本。必须放请求体的,也应该有明确 schema,而不是混在自然语言系统块里。

如果字段会变化,它就应该说明变化频率和影响范围。每请求变化、每会话变化、每版本变化,是完全不同的风险等级。每请求变化的字段,默认不应该进入第三方缓存键,除非它确实改变语义。

如果字段是为了官方客户端内部能力,第三方路径就应该有兼容策略。Bedrock、Vertex、企业代理和 OpenAI-compatible endpoint 都不是边缘场景。今天很多公司使用 Claude Code 或类似 Agent 工具,并不是都直连同一个官方 endpoint。供应商文档里应该明确哪些环境变量、settings、网关路径会改变请求形状。

更实际的做法包括:

  • 把归因字段从模型输入里移出去,优先使用标准 header 或受控 metadata。
  • 给动态字段提供稳定开关,并说明关闭后影响什么、不影响什么。
  • 在官方文档里列出 Bedrock、Vertex、代理、企业网关的兼容矩阵。
  • 明确告诉网关开发者:哪些字段可剥离,哪些字段必须保留,哪些字段不能进入缓存键。
  • 对缓存相关字段给出最小稳定前缀,而不是让用户猜。

这些要求不苛刻。任何进入生产链路的客户端,只要改变请求体,就应该把这个改变当成 API 合约,而不是实现细节。

第三方 API 用户该怎么自保

用户侧也不能只等供应商修。只要你在公司里维护 API 代理、模型网关或统一调用层,这类问题以后会越来越多。

第一件事是打开请求形状观测。不要只记录模型名、token 和状态码,要能在安全脱敏后看到系统内容、工具定义、metadata、非标准 header、客户端版本和代理改写结果。没有这层日志,所有兼容问题都会变成玄学。

第二件事是把缓存键做白名单。不要偷懒把完整请求体 hash 一下就当缓存键,除非你明确接受任何动态字段都会打穿缓存。更好的做法是只纳入会影响语义的稳定部分,并把客户端归因、trace id、session id、随机 nonce 这类字段排除。

第三件事是在网关边界做归一化。进入下游供应商之前,哪些字段可以透传,哪些字段要剥离,哪些字段要转换成云厂商接受的格式,都应该写成规则,而不是散落在业务代码里。

第四件事是做兼容回归测试。至少要覆盖三类请求:普通对话、长 system prompt、带工具的 Agent 调用。每类都分别跑直连、代理、Bedrock 或其他云厂商路径。只测「能不能返回一句话」不够,必须看缓存命中、错误码、请求体差异和成本统计。

第五件事是把客户端版本纳入变更管理。Claude Code、Codex、Cursor、Cline 这类工具已经不是普通本地软件。它们升级后可能改变请求体、工具协议和运行时元数据。企业如果把它们接进统一网关,就应该像升级 SDK 一样做灰度。

不要把这件事看成单个字段的八卦

cch 会不会继续以当前形式存在,不是最重要的问题。重要的是,它提醒了一件更大的事:Agent 客户端正在变成 API 协议的一部分。

过去我们以为协议在服务端文档里。现在协议的一半在客户端运行时里:它怎么拼 system prompt,怎么加工具,怎么写 metadata,怎么做归因,怎么处理缓存控制,怎么选择模型。用户看见的只是一条 API 调用,决定调用形状的却是一整套客户端逻辑。

这对生态是个提醒。模型供应商不能再假设「官方客户端的内部字段只有官方服务会看见」。第三方 API 用户也不能再假设「兼容 Anthropic API」就等于兼容所有 Anthropic 客户端。兼容的是 HTTP 路径,还是请求体语义,还是客户端运行时协议,这是三件事。

所以,cch 最坑第三方 API 用户的地方,不是那几个随机字符。坑在它把一个本该明确分层的问题混到了一起:客户端归因想进来,服务端计量想识别,代理层想缓存,云厂商网关想校验,用户只想稳定调用。只要边界没说清,成本就会落到最下游的人身上。

对工程团队来说,最稳的结论很简单:凡是会随请求变化、又不属于业务语义的字段,都应该默认被审计、隔离和显式处理。不要等它已经打穿缓存、触发 400、污染日志或改变账单之后,才发现自己一直在把供应商内部协议当成透明空气。