From 7b3ed2a961bb2f10507eda16f4dd5a83bc7af44f Mon Sep 17 00:00:00 2001 From: eyre Date: Sun, 26 Jul 2026 17:54:56 +0000 Subject: [PATCH] fix(gateway): detect proxied Claude Code traffic by body to preserve prompt cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an upstream API gateway (e.g. new-api) relays real Claude Code requests, the User-Agent becomes Go-http-client while the body retains the full Claude Code fingerprint (billing attribution block + metadata.user_id + cache_control breakpoints). Previously, the OAuth mimicry path relied solely on UA matching to detect Claude Code clients. Without a matching UA, the gateway would rewrite the system prompt — replacing the client's carefully structured system blocks and cache_control breakpoints with its own injection. This breaks Anthropic's prefix-based prompt cache: since the cache key evaluates tools → system → messages in order, a changed system invalidates all downstream message caching. Symptoms observed: - cache_read permanently locked at ~25K (only system prompt cached) - cache_creation growing monotonically every turn (full messages rewrite) - Single-request costs $17-27 instead of normal $1-2 Fix: when UA does not match but the body contains a valid billing attribution block (x-anthropic-billing-header with cc_entrypoint=), treat the request as proxied Claude Code traffic and skip mimicry. This preserves the client's original system structure and cache_control breakpoints, allowing Anthropic's prompt cache to function correctly. --- .../service/gateway_claude_oauth_body.go | 25 +++++++++++++++++++ backend/internal/service/gateway_forward.go | 11 ++++++++ 2 files changed, 36 insertions(+) diff --git a/backend/internal/service/gateway_claude_oauth_body.go b/backend/internal/service/gateway_claude_oauth_body.go index 9fbbc674d..9450ddbb1 100644 --- a/backend/internal/service/gateway_claude_oauth_body.go +++ b/backend/internal/service/gateway_claude_oauth_body.go @@ -1208,3 +1208,28 @@ func (s *GatewayService) claudeOAuthSystemPromptInjectionSettings(ctx context.Co } return s.settingService.GetClaudeOAuthSystemPromptInjectionSettings(ctx) } + +// systemHasBillingAttributionBlock 检查请求体的 system 字段中是否包含真实 Claude Code +// 客户端注入的 billing attribution block。该 block 格式稳定(见 gateway_billing_block.go), +// 仅由真实 Claude Code CLI 生成;第三方客户端(opencode 等)不会生成此 block。 +// +// 用于识别被上游 API 网关代理的真实 Claude Code 流量:此类请求的 User-Agent 被网关替换 +// 为 Go-http-client,但 body 保留了完整的客户端特征。如果不识别这类请求而走 mimicry +// 重写 system,会破坏 Anthropic prompt cache 的前缀一致性,导致 messages 级缓存永不命中。 +func systemHasBillingAttributionBlock(body []byte) bool { + system := gjson.GetBytes(body, "system") + if !system.IsArray() { + return false + } + found := false + system.ForEach(func(_, item gjson.Result) bool { + text := item.Get("text").String() + if strings.HasPrefix(text, claudeCodeBillingHeaderPrefix) && + strings.Contains(text, claudeCodeEntrypointMarker) { + found = true + return false + } + return true + }) + return found +} diff --git a/backend/internal/service/gateway_forward.go b/backend/internal/service/gateway_forward.go index 3005d93fa..5a78a1981 100644 --- a/backend/internal/service/gateway_forward.go +++ b/backend/internal/service/gateway_forward.go @@ -170,6 +170,17 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A clientUserAgent = c.GetHeader("User-Agent") } isClaudeCode := IsClaudeCodeClient(ctx) || isClaudeCodeClient(clientUserAgent, parsed.MetadataUserID) + + // 补充判定:上游 API 网关(如 new-api)转发真实 Claude Code 流量时, + // UA 会变成 Go-http-client 但 body 保留了完整的 Claude Code 特征 + // (billing attribution block + metadata.user_id)。此时如果仍走 mimicry + // 重写 system prompt,会破坏 Anthropic prompt cache 的前缀匹配—— + // 导致 messages 级缓存永远 miss、cache_creation 每轮全量重写。 + // 通过检查 body 中的 billing attribution block 来识别被代理的真实 CC 流量。 + if !isClaudeCode && parsed.MetadataUserID != "" { + isClaudeCode = systemHasBillingAttributionBlock(body) + } + shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCode if shouldMimicClaudeCode {