Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions backend/internal/service/claude_code_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewClaudeCodeValidator() *ClaudeCodeValidator {
// 采用与 claude-relay-service 完全一致的验证策略:
//
// Step 1: User-Agent 检查 (必需) - 必须是 claude-cli/x.x.x
// Step 2: 对于非 messages 路径,只要 UA 匹配就通过
// Step 2: 对于非 messages 路径和 /messages/count_tokens,只要 UA 匹配就通过
// Step 3: 检查 max_tokens=1 + haiku 探测请求绕过(UA 已验证)
// Step 4: 对于 messages 路径,进行严格验证:
// - System prompt 相似度检查
Expand All @@ -71,12 +71,17 @@ func (v *ClaudeCodeValidator) Validate(r *http.Request, body map[string]any) boo
return false
}

// Step 2: 非 messages 路径,只要 UA 匹配就通过
// Step 2: 非 messages 路径只要 UA 匹配就通过
path := r.URL.Path
if !strings.Contains(path, "messages") {
return true
}

// count_tokens 是 Claude Code 官方辅助请求,通常不携带完整 messages system prompt。
if isMessagesCountTokensPath(path) {
return true
}

// Step 3: 检查 max_tokens=1 + haiku 探测请求绕过
// 这类请求用于 Claude Code 验证 API 连通性,不携带 system prompt
if isMaxTokensOneHaiku, ok := IsMaxTokensOneHaikuRequestFromContext(r.Context()); ok && isMaxTokensOneHaiku {
Expand Down Expand Up @@ -128,6 +133,10 @@ func (v *ClaudeCodeValidator) Validate(r *http.Request, body map[string]any) boo
return true
}

func isMessagesCountTokensPath(path string) bool {
return strings.HasSuffix(path, "/messages/count_tokens")
}

// hasClaudeCodeSystemPrompt 检查请求是否包含 Claude Code 系统提示词
// 使用字符串相似度匹配(Dice coefficient)
func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) bool {
Expand Down
91 changes: 91 additions & 0 deletions backend/internal/service/claude_code_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,97 @@ func TestClaudeCodeValidator_MessagesWithoutProbeStillNeedStrictValidation(t *te
require.False(t, ok)
}

func TestClaudeCodeValidator_CountTokensPathUAOnly(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages/count_tokens", nil)
req.Header.Set("User-Agent", "claude-cli/2.1.156 (Claude Code)")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
})
require.True(t, ok)
}

func TestClaudeCodeValidator_CountTokensPathRequiresUA(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages/count_tokens", nil)
req.Header.Set("User-Agent", "curl/8.0.0")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
})
require.False(t, ok)
}

func TestClaudeCodeValidator_MessagesPathFullValid(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/2.1.156 (Claude Code)")
req.Header.Set("X-App", "claude-code")
req.Header.Set("anthropic-beta", "claude-code-20250219")
req.Header.Set("anthropic-version", "2023-06-01")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
"stream": true,
"system": []any{
map[string]any{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
},
},
"metadata": map[string]any{
"user_id": "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
},
})
require.True(t, ok)
}

func TestClaudeCodeValidator_MessagesPathRejectsNonClaudeCodeUA(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "curl/8.0.0")
req.Header.Set("X-App", "claude-code")
req.Header.Set("anthropic-beta", "claude-code-20250219")
req.Header.Set("anthropic-version", "2023-06-01")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
"stream": true,
"system": []any{
map[string]any{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
},
},
"metadata": map[string]any{
"user_id": "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
},
})
require.False(t, ok)
}

func TestClaudeCodeValidator_MessagesPathWithoutSystemPromptStillRejected(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/2.1.156 (Claude Code)")
req.Header.Set("X-App", "claude-code")
req.Header.Set("anthropic-beta", "claude-code-20250219")
req.Header.Set("anthropic-version", "2023-06-01")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
"stream": true,
"messages": []any{
map[string]any{"role": "user", "content": "hello"},
},
"metadata": map[string]any{
"user_id": "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
},
})
require.False(t, ok)
}

func TestClaudeCodeValidator_NonMessagesPathUAOnly(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/models", nil)
Expand Down
Loading