Repository navigation
feat: 客户端发来的协议头没有放行的路(anthropic-version、beta 之类) #23
Description
现象
内核构造上游请求时完全不碰客户端发来的请求头:server/forward.go 只取宿主用
WithUpstreamHeader 设的那一份(headers := s.upstreamHeaders(ctx)),塞进
channel.Request.Header,此外没有任何地方读过进来的请求头。
这一点与方言是否相同无关:同方言透传只是 body 逐字节透传,头一样不带。
进来的 query 串也不带(channel/client.go 先 parsed.RawQuery = "",
之后只为 auth.style: query 拼鉴权参数),所以 Claude Code 的
POST /v1/messages?beta=true 里那个 ?beta=true 也丢了。
设计意图是不让客户端的 Authorization 漏给上游(issue #17 的验收条件,有测试钉住),
这个出发点没问题。代价分三档:
一、无所谓的。 x-stainless-*、accept-encoding、user-agent 这类观测性的头。
Go 自己会补后两个。
二、静态能补,但现在是手工活:anthropic-version。
官方 versioning 页原话:
When making API requests, you must send an
anthropic-versionrequest header. For
example,anthropic-version: 2023-06-01. If you are using the client SDKs, this is
handled for you automatically.
关键在后半句:内核自己就是那个「客户端」,SDK 帮不了它,得它自己发。
而仓库里搜不到任何地方设置它,RelayKit 里也没有,所以接真实 Anthropic 上游只能在
渠道的 headers: 里写死,每个渠道写一遍。
口径冲突要说清:Messages API 参考页把同一个东西标成
"anthropic-version": optional string,与 versioning 页的 must send 不一致。
两处口径都列在这里,不当下结论。
三、按请求变化的,现在补不了:anthropic-beta。
官方参考里多处写「Requires the user-profiles beta header」、
「based on beta headers」—— 功能开关就是靠这个头。Claude Code 每请求可能开不同的
beta,静态配置表达不了。同类还有 Codex 的 Session-Id、Originator、X-Codex-*,
以及 OpenAI 的 OpenAI-Beta。
期望
- 按方言声明一份放行白名单:名单里的头按请求跟到上游。
- 名单外的照旧不转发,尤其
authorization、x-api-key、cookie。 - 落到哪一个上游 workspace 属于计费归属,
anthropic-workspace-id这类
「选上游归属」的头也不从客户端放行,由中继或宿主决定。 - 名单为空时行为与 v0.5.0 逐条相同。
可选形状
- 渠道配置里加放行名单(像上游 new-api 那样,按客户端类型配)。
- 名单写在方言里(代码固定,方言决定协议需要哪些头),配置只用来开关或追加。
- 不做,把「要哪些头就在渠道
headers:里写死」写进docs/config.md。
验收条件
- 名单里的头按请求到达上游;名单外的不到,
authorization、x-api-key、cookie、
anthropic-workspace-id有不带去的用例。 - 名单为空时行为与 v0.5.0 相同(有测试对照)。
- 与宿主用
WithUpstreamHeader设的头同名时的优先级写清并有测试。 make check全绿。
要定的口径
- 名单放哪一层:渠道配置,还是方言(谁维护、谁负责跟进上游新增的协议头)。
- 只能是白名单,不能是黑名单 —— 这条没有余地,写下来是防止后来人图省事反过来。
- 要不要给方言一份内置默认名单(比如 Claude 渠道默认放
anthropic-version与
anthropic-beta)。 - 与
WithUpstreamHeader(issue feat: 宿主按请求给上游加头没有路可走 #17,已随 v0.5.0 发布)的关系:前者是宿主主动加,
这里是客户端本来就有、协议要求带过去。同名时谁赢要定,并写进文档。 anthropic-version到底必需不必需(两处官方口径冲突,要实测)。
上游 new-api 的情况(查证过)
它有两份按客户端挑的透传名单:
claudeCliPassThroughHeaders: X-Stainless-Arch / Lang / Os / Package-Version /
Retry-Count / Runtime / Runtime-Version / Timeout,
User-Agent, X-App, Anthropic-Beta, Anthropic-Version,
Anthropic-Dangerous-Direct-Browser-Access
codexCliPassThroughHeaders: Originator, Session_id, Thread_id, Session-Id, Thread-Id,
X-Client-Request-Id, User-Agent, X-Codex-Beta-Features,
X-Codex-Turn-State, X-Codex-Turn-Metadata, X-Codex-Window-Id,
X-Codex-Parent-Thread-Id, X-OpenAI-Subagent,
X-OpenAI-Memgen-Request, X-ResponsesAPI-Include-Timing-Metrics,
X-OpenAI-Internal-Codex-Responses-Lite
它用 param_override_template 的 pass_headers 加 keep_origin 按请求放行。
说明「一个头都不抄」太绝对了:真实部署需要按请求放行一部分。
没查的
- 缺
anthropic-version时真实上游的确切反应(两处文档口径冲突,没有实测)。 - Claude Code 与 Codex 实际发出的完整头清单(没抓包;上面那份名单来自 new-api)。
- 中转层(LiteLLM、new-api 之类)是否通常替上游补
anthropic-version。
相关
- issue feat: 宿主按请求给上游加头没有路可走 #17:宿主按请求给上游加头(已随 v0.5.0 发布,是「主动加」那条路)。
- issue feat: 渠道配置没法表达「这几条渠道共用上游缓存」(暂缓) #22:渠道配置没法表达「这几条渠道共用上游缓存」。
anthropic-workspace-id
同时关系到它 —— 上游允许用头按请求选 workspace,所以缓存域未必是渠道的静态事实。
环境
core v0.5.0(dd1c7ab)。函数名与行号取自这一版。
Activity
tokenmp commentedon Oct 11, 2026
落地的形状与这条 issue 的「期望」有一处偏离,记在这里。
白名单放在渠道级(
channels[].pass_headers),不放在方言级。 issue 写的是「按方言声明一份放行白名单」。理由是放行的信任边界:要回答的问题是「这条上游渠道可以收到哪些客户端头」,不是「客户端是什么方言」。按客户端方言放行会把 Claude 客户端的特性开关(
anthropic-beta)带给一个它没打算对话的 OpenAI 上游,而那正是这条功能要防的事。渠道级由部署者按上游逐条声明。对应 PR #36(已随 v0.7.0 之后的下一个 tag)。文档里给了两份可直接复制的推荐名单(Claude Code 与 Codex),数据取自这条 issue 里那份 new-api 的透传清单。
两处明确留在范围外:客户端请求的 query 串(
?beta=true仍然丢),以及内核自己补anthropic-version这类「协议必需的头」(仍写在渠道headers里,因为「是否必需」两处官方文档口径冲突、没有实测)。