fix(gateway): filter anthropic-beta on the Vertex Anthropic path (#3358)

Vertex AI's Anthropic endpoint rejects unknown anthropic-beta tokens with
HTTP 400. buildUpstreamRequestAnthropicVertex forwarded the client header
verbatim via the allowedHeaders whitelist, so recent Claude Code CLIs that
send advisor-tool-2026-03-01, prompt-caching-scope-2026-01-05,
redact-thinking-2026-02-12 and thinking-token-count-2026-05-13 broke every
Vertex service_account request, even though plain account-test requests passed.

This is the only upstream builder that bypassed beta filtering: the
OAuth/API-key path uses computeFinalAnthropicBeta and the Bedrock path uses
filterBedrockBetaTokens. Close the gap with a Vertex-specific whitelist
(vertexSupportedBetaTokens) mirroring bedrockSupportedBetaTokens, plus the
existing BetaPolicy block check:

- evaluateBetaPolicy block check (symmetric to resolveBedrockBetaTokensForRequest)
- filterVertexBetaTokens strips policy-filtered + non-whitelisted tokens
- body context_management sanitize now keys on the final beta, not the raw client value
- overwrite the anthropic-beta header after the whitelist copy loop with the final value

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
haruka
2026-06-19 07:51:24 -07:00
co-authored by Claude Opus 4.8
parent 4a5665da5b
commit 40e1cc14b3
+69 -7
View File
@@ -6830,6 +6830,48 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
return req, body, nil
}
// vertexSupportedBetaTokens 是 Vertex AI 的 Anthropic 端点接受的 anthropic-beta
// 白名单。Vertex 对任何未知 token 直接 HTTP 400,故采用白名单(与 Bedrock 的
// bedrockSupportedBetaTokens 同思路)而非黑名单:未来 Claude Code 新增的、Vertex 尚未
// 支持的 token 天然被剥离。当 Vertex 新增支持某 beta 时在此补充。
//
// 明确排除(issue #3358 中 Vertex 报 400 的 token):advisor-tool-2026-03-01、
// prompt-caching-scope-2026-01-05、redact-thinking-2026-02-12、
// thinking-token-count-2026-05-13;以及 claude-code-20250219 / oauth-2025-04-20 等
// 客户端身份 beta——Vertex service_account 走 Bearer 鉴权,不需要它们。
var vertexSupportedBetaTokens = map[string]bool{
"context-1m-2025-08-07": true,
"context-management-2025-06-27": true,
"fine-grained-tool-streaming-2025-05-14": true,
"interleaved-thinking-2025-05-14": true,
}
// filterVertexBetaTokens 解析 client 的 anthropic-beta header,先剔除 drop 集合中的
// token(BetaPolicy filter + 默认 drop),再只保留 Vertex 支持的 token,去重后逗号拼接。
// 返回最终 header(可能为空字符串)。
func filterVertexBetaTokens(header string, drop map[string]struct{}) string {
tokens := parseAnthropicBetaHeader(header)
if len(tokens) == 0 {
return ""
}
out := make([]string, 0, len(tokens))
seen := make(map[string]bool, len(tokens))
for _, t := range tokens {
if _, dropped := drop[t]; dropped {
continue
}
if !vertexSupportedBetaTokens[t] {
continue
}
if seen[t] {
continue
}
seen[t] = true
out = append(out, t)
}
return strings.Join(out, ",")
}
func (s *GatewayService) buildUpstreamRequestAnthropicVertex(
ctx context.Context,
c *gin.Context,
@@ -6844,14 +6886,27 @@ func (s *GatewayService) buildUpstreamRequestAnthropicVertex(
return nil, err
}
// 能力维度 sanitize:Vertex 路径上 anthropic-beta header 原样透传客户端值
// (下面白名单跳过 anthropic-version 但保留 anthropic-beta),依此决定是否
// 保留 body 中的 context_management,与 Anthropic 直连 / Bedrock 路径对称。
// 计算最终 outgoing anthropic-beta。Vertex AI 的 Anthropic 端点只接受一小撮
// beta token,未知 token 会直接 HTTP 400——近期 Claude Code CLI 透传的
// advisor-tool-2026-03-01 / prompt-caching-scope-2026-01-05 /
// redact-thinking-2026-02-12 / thinking-token-count-2026-05-13 都不被 Vertex 接受
// (issue #3358)。这里复用 BetaPolicy 的 block 检查(与 Bedrock 的
// resolveBedrockBetaTokensForRequest 对称),再按 vertexSupportedBetaTokens 白名单
// 剥离其余 token,使该路径与 Anthropic 直连 / Bedrock 路径行为一致。
clientBeta := ""
if c != nil && c.Request != nil {
clientBeta := getHeaderRaw(c.Request.Header, "anthropic-beta")
if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(vertexBody, clientBeta); changed {
vertexBody = sanitized
}
clientBeta = getHeaderRaw(c.Request.Header, "anthropic-beta")
}
policy := s.evaluateBetaPolicy(ctx, clientBeta, account, modelID)
if policy.blockErr != nil {
return nil, policy.blockErr
}
finalBeta := filterVertexBetaTokens(clientBeta, mergeDropSets(policy.filterSet))
// 能力维度 sanitize:基于最终 beta(而非原始 client 值)决定是否保留 body 中的
// context_management,与 Anthropic 直连 / Bedrock 路径对称。
if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(vertexBody, finalBeta); changed {
vertexBody = sanitized
}
fullURL, err := buildVertexAnthropicURL(account.VertexProjectID(), account.VertexLocation(modelID), modelID, reqStream)
if err != nil {
@@ -6883,6 +6938,13 @@ func (s *GatewayService) buildUpstreamRequestAnthropicVertex(
setHeaderRaw(req.Header, "authorization", "Bearer "+token)
setHeaderRaw(req.Header, "content-type", "application/json")
// 覆盖上面白名单 loop 写入的原始 client anthropic-beta,使用过滤后的最终值。
// finalBeta 为空(全部被剥离)时不下发该 header,与 Vertex 无 beta 请求一致。
deleteHeaderAllForms(req.Header, "anthropic-beta")
if finalBeta != "" {
setHeaderRaw(req.Header, "anthropic-beta", finalBeta)
}
s.debugLogGatewaySnapshot("UPSTREAM_FORWARD_VERTEX_ANTHROPIC", req.Header, vertexBody, map[string]string{
"url": req.URL.String(),
"token_type": "service_account",