alfadb
|
ddf91e9a7f
|
fix(gateway): 按最终 anthropic-beta header 对 body.context_management 做能力维度 sanitize
上游 Anthropic 在 body 含 `context_management` 但最终发出去的
`anthropic-beta` header 不含 `context-management-2025-06-27` 时会拒收:
{
"type": "invalid_request_error",
"message": "context_management: Extra inputs are not permitted"
}
(HTTP 400, request_id 形如 req_011C...)
该 400 在 haiku 路径上触发,因为三个 beta header 构造器有意排除了
context-management beta:
- HaikuBetaHeader (messages, OAuth / mimic CC)
- APIKeyHaikuBetaHeader (messages, API-key)
- CountTokensBetaHeader (count_tokens, 所有认证类型)
但 body 中仍然带着 `context_management` 字段,原因有二:
1. normalizeClaudeOAuthRequestBody 在 thinking_enabled / thinking_adaptive
打开时为 `clear_thinking_20251015` 主动注入;
2. 客户端 (Claude Code CLI >= 2.1.87) 原样发送, 网关透传时一并转发。
修复方案: 能力维度对称约束
==========================
对齐已有的 Bedrock 模式
(`backend/internal/service/bedrock_request.go` 中的
`sanitizeBedrockFieldsForBetaTokens`):
根据 **最终** 发出的 `anthropic-beta` header 决定是否保留
`body.context_management`, 而不是按 model 名或路由分类来决定。
新增纯函数:
sanitizeAnthropicBodyForBetaTokens(body, betaHeader) (body, changed)
如果 `betaHeader` 不含 `context-management-2025-06-27`, 用 sjson 把 body
字段 strip 掉; 否则原样返回。
在所有 Anthropic / Anthropic-兼容 上游出口都接入:
| 路径 | sanitize 接入点 |
|--------------------------------------------|-------------------------------------------------------|
| /v1/messages OAuth mimic CC | buildUpstreamRequest |
| /v1/messages OAuth 真 CC 透传 | buildUpstreamRequest |
| /v1/messages API-key | buildUpstreamRequest |
| /v1/messages API-key passthrough | buildUpstreamRequestAnthropicAPIKeyPassthrough |
| /v1/messages Vertex / service-account | buildUpstreamRequestAnthropicVertex |
| /v1/messages/count_tokens (全部 4 条路径) | buildCountTokensRequest, |
| | buildCountTokensRequestAnthropicAPIKeyPassthrough |
| Antigravity Anthropic-兼容 上游 | AntigravityGatewayService.ForwardUpstream |
| Bedrock | (已由 sanitizeBedrockFieldsForBetaTokens 处理) |
为什么要重排 (而不是加一行调用)
================================
sanitize 必须 **在** `signBillingHeaderCCH` 之前运行。CCH 对整个 body
取 xxHash64 摘要后写入 billing header 里 5 位十六进制的 `cch` 字段;
如果先签名再 strip, 上游对发出去的 body 重算 hash 会和 `cch` 不一致,
请求被判为 third-party。这就要求在 `http.NewRequest` 之前算出最终的
`anthropic-beta` header, 所以把原本内联在 builder 里的 beta 计算逻辑
抽成了两个纯函数:
- computeFinalAnthropicBeta (messages 路径: mimic 不透传
客户端 beta)
- computeFinalCountTokensAnthropicBeta (count_tokens 路径: mimic 不
跳过白名单透传)
两者逐位保留原行为:
- mimic 路径在 messages 上跳过客户端 beta, 在 count_tokens 上合并
- API-key 路径尊重 `InjectBetaForAPIKey` 开关
- dropSet (`defaultDroppedBetasSet` + BetaPolicy filter) 应用在主路径,
passthrough / Vertex 路径有意不应用 —— 这条原有的不对称行为本 PR
不动。
一条语义测试 (`TestSanitizeMustBeBeforeCCHSigning_HashConsistency`) 把
顺序约束文档化并强制守住: 它证明 `sanitize -> signBillingHeaderCCH`
产生的 `cch` 与最终 body 一致, 而 `signBillingHeaderCCH -> sanitize`
产生的 `cch` 会被上游 hash 重算判失败。
为什么是能力维度 (而不是 haiku 模型名匹配)
==========================================
最朴素的"按 model 名 strip"方案
(`strings.Contains(modelID, "haiku") -> DeleteBytes "context_management"`)
有四个真实失败模式:
1. 过度删除。CLI >= 2.1.87 的真 Claude Code 客户端在 haiku 上同时
发送 body 字段 **和** `anthropic-beta: context-management-2025-06-27`。
一律 strip 会让该用户的 `clear_thinking_20251015` 静默失效。
2. 别名漂移。未来的 haiku 别名 (`claude-3-haiku-...`,
`claude-haiku-...` 等) 改变匹配面; 任何新别名都会悄悄绕过 strip。
3. count_tokens 漏覆盖。count_tokens 有自己的 builder 和不同的 beta
header 集合; 在一个地方做 model 名检查会漏掉这条路径。
4. API-key passthrough 早退。passthrough builder 在 model 名 strip
之前就 return 了, strip 根本不执行。
能力维度沿着 header 端到端走, 上述 4 个 case 都由构造方式保证正确,
不依赖任何 modelID 匹配。
防御项
======
- 当 `sjson.DeleteBytes` 在 `gjson` 刚验证过字段存在的 body 上失败时,
`sanitizeAnthropicBodyForBetaTokens` 会记 warning 日志 —— 这种情况
现实中仅在请求中途被破坏时发生, 日志把此前会静默发生的 body / header
不一致暴露出来。
- `header_util.go` 新增 `deleteHeaderAllForms`: 在白名单透传已经写入
canonical 大小写的 `Anthropic-Beta` 之后再覆盖, 否则会同时留下两条。
测试
====
`backend/internal/service` 下新增 44 个测试:
- 纯函数: anthropicBetaTokensContains x 5, sanitize keep/strip x 6,
computeFinal{Anthropic,CountTokens}AnthropicBeta x 12
- normalize 回归 x 5
- buildUpstreamRequest 端到端 x 4
(OAuth mimic haiku strip / mimic sonnet preserve /
真 CC haiku 带客户端 beta preserve / API-key haiku strip)
- buildCountTokensRequest 端到端 x 2
- buildUpstreamRequestAnthropicAPIKeyPassthrough x 2 (strip / preserve)
- buildCountTokensRequestAnthropicAPIKeyPassthrough x 2 (strip / preserve)
- buildUpstreamRequestAnthropicVertex x 2 (strip / preserve, 含 outgoing
`anthropic-beta` header 对称断言)
- CCH 顺序语义测试 x 1
unit 套件全过 (本机 88s), `golangci-lint` 0 issues。
已知局限 (本 PR 范围外)
========================
- Vertex 路径用透传过来的客户端 `anthropic-beta` header 作为 sanitize
依据, 而不是 Vertex 侧的能力矩阵。最坏情况是过度 strip (= 当前 main
的行为, 主路径本来什么都不 strip); 不是 regression。完整的 Vertex
能力模型属于单独的 PR。
- Vertex builder 仍然不应用 BetaPolicy filter / dropSet。这是该 builder
早 return 的既有架构决策, 本 PR 不动。
- count_tokens mimic 在 haiku 上仍然注入 `context-management-2025-06-27`
(因为原 count_tokens mimic 逻辑并不像 messages mimic 那样排除它)。
本 PR 逐位保留 main 的行为; 是否要让它与 messages mimic 的排除策略
统一是另一个问题。
- `sanitizeAnthropicBodyForBetaTokens` 目前只处理
`context_management <-> context-management-2025-06-27` 这一对。如果
Anthropic 后续推出更多 beta-gated body 字段, 可以在后续 PR 重构为
`{body 路径 -> required beta token}` 注册表的形式。
|
2026-05-28 00:02:50 +08:00 |
|