"backend/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "ff6fa0203d5c1660ad73a51651481a781b168f8b"
Commit 266179a3 authored by shaw's avatar shaw Committed by 陈曦
Browse files

fix(gateway): skip body mimicry for real Claude Code clients to restore prompt caching

PR #1914 unconditionally applied the full mimicry pipeline to all OAuth
accounts, including real Claude Code CLI clients. This replaced the
client's long system prompt (~10K+ tokens with stable cache_control
breakpoints) with a short ~45 token [billing, CC prompt] pair, which
falls below Anthropic's 1024-token minimum cacheable prefix threshold.
The result: every request created a new cache but never hit an existing
one.

Fix: restore the Claude Code client detection gate so that real CC
clients bypass body-level mimicry (system rewrite, message cache
management, tool name obfuscation). Non-CC third-party clients
(opencode, etc.) continue to receive full mimicry.

Also harden the detection logic:
- Make UA regex case-insensitive (align with claude_code_validator.go)
- Validate metadata.user_id format via ParseMetadataUserID() instead of
  just checking non-empty, preventing third-party tools from spoofing
  a claude-cli/* UA with an arbitrary user_id string to bypass mimicry
parent 9abd3fb9
......@@ -9,6 +9,11 @@ import (
)
func TestIsClaudeCodeClient(t *testing.T) {
// 合法的 legacy 格式 metadata.user_id(64位 hex + account uuid + session uuid)
legacyUserID := "user_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2_account_550e8400-e29b-41d4-a716-446655440000_session_123e4567-e89b-12d3-a456-426614174000"
// 合法的 JSON 格式 metadata.user_id(2.1.78+ 版本)
jsonUserID := `{"device_id":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2","account_uuid":"550e8400-e29b-41d4-a716-446655440000","session_id":"123e4567-e89b-12d3-a456-426614174000"}`
tests := []struct {
name string
userAgent string
......@@ -16,15 +21,21 @@ func TestIsClaudeCodeClient(t *testing.T) {
want bool
}{
{
name: "Claude Code client",
name: "Claude Code client with legacy user_id",
userAgent: "claude-cli/1.0.62 (darwin; arm64)",
metadataUserID: "session_123e4567-e89b-12d3-a456-426614174000",
metadataUserID: legacyUserID,
want: true,
},
{
name: "Claude Code without version suffix",
userAgent: "claude-cli/2.0.0",
metadataUserID: "session_abc",
name: "Claude Code client with JSON user_id",
userAgent: "claude-cli/2.1.92 (external, cli)",
metadataUserID: jsonUserID,
want: true,
},
{
name: "Claude Code case insensitive UA",
userAgent: "Claude-CLI/2.0.0",
metadataUserID: legacyUserID,
want: true,
},
{
......@@ -34,21 +45,33 @@ func TestIsClaudeCodeClient(t *testing.T) {
want: false,
},
{
name: "Different user agent",
name: "Claude CLI UA with invalid user_id format",
userAgent: "claude-cli/2.0.0",
metadataUserID: "fake-user-id-12345",
want: false,
},
{
name: "Different user agent with valid user_id",
userAgent: "curl/7.68.0",
metadataUserID: "user123",
metadataUserID: legacyUserID,
want: false,
},
{
name: "Empty user agent",
userAgent: "",
metadataUserID: "user123",
metadataUserID: legacyUserID,
want: false,
},
{
name: "Similar but not Claude CLI",
userAgent: "claude-api/1.0.0",
metadataUserID: "user123",
metadataUserID: legacyUserID,
want: false,
},
{
name: "Opencode spoofing UA with arbitrary user_id",
userAgent: "claude-cli/2.1.92",
metadataUserID: "session_abc",
want: false,
},
}
......
......@@ -329,7 +329,7 @@ func isClaudeCodeCredentialScopeError(msg string) bool {
// Some upstream APIs return non-standard "data:" without space (should be "data: ").
var (
sseDataRe = regexp.MustCompile(`^data:\s*`)
claudeCliUserAgentRe = regexp.MustCompile(`^claude-cli/\d+\.\d+\.\d+`)
claudeCliUserAgentRe = regexp.MustCompile(`(?i)^claude-cli/\d+\.\d+\.\d+`)
// claudeCodePromptPrefixes 用于检测 Claude Code 系统提示词的前缀列表
// 支持多种变体:标准版、Agent SDK 版、Explore Agent 版、Compact 版等
......@@ -3709,13 +3709,19 @@ func sleepWithContext(ctx context.Context, d time.Duration) error {
}
}
// isClaudeCodeClient 判断请求是否来自 Claude Code 客户端
// 简化判断:User-Agent 匹配 + metadata.user_id 存在
// isClaudeCodeClient 判断请求是否来自真正的 Claude Code 客户端。
// 判定条件:
// 1. User-Agent 匹配 claude-cli/X.Y.Z(大小写不敏感)
// 2. metadata.user_id 符合 Claude Code 格式(legacy 或 JSON 格式)
//
// 只检查 metadata.user_id 非空不够严格:第三方工具(opencode 等)可能伪造 UA
// 并附带任意 metadata.user_id 字符串,从而绕过 mimicry。必须通过 ParseMetadataUserID
// 验证格式才能确认是真正的 Claude Code 客户端。
func isClaudeCodeClient(userAgent string, metadataUserID string) bool {
if metadataUserID == "" {
if !claudeCliUserAgentRe.MatchString(userAgent) {
return false
}
return claudeCliUserAgentRe.MatchString(userAgent)
return ParseMetadataUserID(metadataUserID) != nil
}
// normalizeSystemParam 将 json.RawMessage 类型的 system 参数转为标准 Go 类型(string / []any / nil),
......@@ -4144,12 +4150,15 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
})
}
// OAuth 账号无条件走完整 mimicry,与 Parrot 对齐。
// 不再检查 isClaudeCodeRequest —— 即使客户端自称 Claude Code(opencode 等
// 第三方工具会伪装 UA / X-App / system prompt),它的伪装往往不完整(缺 billing
// block / 工具名混淆 / cache 策略等),被 Anthropic 判为 third-party。
// 无条件覆盖不会对真正的 Claude Code 造成问题,因为我们的伪装更完整。
shouldMimicClaudeCode := account.IsOAuth()
// Claude Code 客户端判定:UA 匹配 claude-cli/* 且携带 metadata.user_id。
// 真正的 Claude Code 客户端自带完整的 system prompt、cache_control 断点和 header,
// 不需要代理做任何 body 级别的 mimicry;强行替换反而会破坏客户端的缓存策略
// (长 system prompt 被替换为 ~45 tokens 的短 prompt,低于 Anthropic 1024 token
// 最低缓存门槛,导致系统级缓存失效)。
//
// 对于非 Claude Code 的第三方客户端(opencode 等),仍然走完整 mimicry。
isClaudeCode := IsClaudeCodeClient(ctx) || isClaudeCodeClient(c.GetHeader("User-Agent"), parsed.MetadataUserID)
shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCode
if shouldMimicClaudeCode {
// 与 Parrot 对齐:OAuth 账号无条件重写 system(即使客户端已发了 Claude Code
......@@ -8387,7 +8396,8 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
// Pre-filter: strip empty text blocks to prevent upstream 400.
body = StripEmptyTextBlocks(body)
shouldMimicClaudeCode := account.IsOAuth()
isClaudeCodeCT := IsClaudeCodeClient(ctx) || isClaudeCodeClient(c.GetHeader("User-Agent"), parsed.MetadataUserID)
shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCodeCT
if shouldMimicClaudeCode {
normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: true}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment