mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:38:27 +08:00
fix(anthropic): recognize auto mode classifier
This commit is contained in:
@@ -48,9 +48,14 @@ var claudeCodeSystemPrompts = []string{
|
||||
}
|
||||
|
||||
const (
|
||||
// These markers identify Claude Code's official security-monitor classifier
|
||||
// request without coupling validation to every wording change in the prompt.
|
||||
claudeCodeSecurityMonitorPromptPrefix = "You are a security monitor for autonomous AI coding agents."
|
||||
claudeCodeSecurityMonitorPromptMinLen = 10_000
|
||||
|
||||
// claudeCodeBillingHeaderPrefix 是 Claude Code 在 system 数组首块注入的计费归因块前缀。
|
||||
// 该块存在于所有真实 Claude Code CLI 请求中(含安全监视器等无身份 prose 的子请求),
|
||||
// 格式固定、不随提示词改版漂移,是比身份 prose 更稳定的客户端标识。
|
||||
// 大多数真实 CLI 请求(含部分无身份 prose 的子请求)会携带该块;不携带该块的
|
||||
// 固定官方辅助请求由独立规则识别。该格式比身份 prose 更稳定。
|
||||
// 生成见 gateway_billing_block.go;同类识别见 pkg/apicompat/anthropic_to_responses.go。
|
||||
claudeCodeBillingHeaderPrefix = "x-anthropic-billing-header"
|
||||
// claudeCodeEntrypointMarker 标识计费块携带入口归因字段。不绑定具体入口值
|
||||
@@ -167,6 +172,10 @@ func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) boo
|
||||
return false
|
||||
}
|
||||
|
||||
if isClaudeCodeSecurityMonitorPrompt(systemEntries) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查每个 system entry
|
||||
for _, entry := range systemEntries {
|
||||
entryMap, ok := entry.(map[string]any)
|
||||
@@ -196,6 +205,46 @@ func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) boo
|
||||
return false
|
||||
}
|
||||
|
||||
func isClaudeCodeSecurityMonitorPrompt(systemEntries []any) bool {
|
||||
if len(systemEntries) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
entry, ok := systemEntries[0].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
entryType, ok := entry["type"].(string)
|
||||
if !ok || entryType != "text" {
|
||||
return false
|
||||
}
|
||||
|
||||
text, ok := entry["text"].(string)
|
||||
if !ok || len(text) < claudeCodeSecurityMonitorPromptMinLen ||
|
||||
!strings.HasPrefix(text, claudeCodeSecurityMonitorPromptPrefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
markers := []string{
|
||||
"## Threat Model",
|
||||
"- `<transcript>`:",
|
||||
"## HARD BLOCK",
|
||||
"## SOFT BLOCK",
|
||||
"## Classification Process",
|
||||
"## Output Format",
|
||||
"<block>yes</block><reason>",
|
||||
"<block>no</block>",
|
||||
}
|
||||
for _, marker := range markers {
|
||||
if !strings.Contains(text, marker) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// bestSimilarityScore 计算文本与所有 Claude Code 模板的最佳相似度
|
||||
func (v *ClaudeCodeValidator) bestSimilarityScore(text string) float64 {
|
||||
normalizedText := normalizePrompt(text)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||
@@ -135,6 +136,139 @@ func TestClaudeCodeValidator_BillingBlockRecognizedWithoutIdentityPrompt(t *test
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestClaudeCodeValidator_SecurityMonitorWithoutBillingBlock(t *testing.T) {
|
||||
monitorPrompt, err := os.ReadFile("testdata/security_monitor_system_prompt.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
validHeaders := map[string]string{
|
||||
"User-Agent": "claude-cli/2.1.220 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"anthropic-beta": "claude-code-20250219",
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
validBody := func(prompt string) map[string]any {
|
||||
return map[string]any{
|
||||
"model": "claude-haiku-4-5-20251001",
|
||||
"system": []any{
|
||||
map[string]any{"type": "text", "text": prompt},
|
||||
},
|
||||
"metadata": map[string]any{"user_id": claudeCodeMetadataUserIDJSON},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
headers map[string]string
|
||||
body map[string]any
|
||||
wantAccept bool
|
||||
}{
|
||||
{
|
||||
name: "official classifier request",
|
||||
headers: validHeaders,
|
||||
body: validBody(string(monitorPrompt)),
|
||||
wantAccept: true,
|
||||
},
|
||||
{
|
||||
name: "non-Claude user agent",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "curl/8.0.0",
|
||||
"X-App": "cli",
|
||||
"anthropic-beta": "claude-code-20250219",
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: validBody(string(monitorPrompt)),
|
||||
},
|
||||
{
|
||||
name: "missing X-App",
|
||||
headers: map[string]string{
|
||||
"User-Agent": validHeaders["User-Agent"],
|
||||
"anthropic-beta": validHeaders["anthropic-beta"],
|
||||
"anthropic-version": validHeaders["anthropic-version"],
|
||||
},
|
||||
body: validBody(string(monitorPrompt)),
|
||||
},
|
||||
{
|
||||
name: "missing anthropic-beta",
|
||||
headers: map[string]string{
|
||||
"User-Agent": validHeaders["User-Agent"],
|
||||
"X-App": validHeaders["X-App"],
|
||||
"anthropic-version": validHeaders["anthropic-version"],
|
||||
},
|
||||
body: validBody(string(monitorPrompt)),
|
||||
},
|
||||
{
|
||||
name: "missing anthropic-version",
|
||||
headers: map[string]string{
|
||||
"User-Agent": validHeaders["User-Agent"],
|
||||
"X-App": validHeaders["X-App"],
|
||||
"anthropic-beta": validHeaders["anthropic-beta"],
|
||||
},
|
||||
body: validBody(string(monitorPrompt)),
|
||||
},
|
||||
{
|
||||
name: "missing metadata",
|
||||
headers: validHeaders,
|
||||
body: map[string]any{
|
||||
"model": "claude-haiku-4-5-20251001",
|
||||
"system": []any{map[string]any{"type": "text", "text": string(monitorPrompt)}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata user ID",
|
||||
headers: validHeaders,
|
||||
body: func() map[string]any {
|
||||
body := validBody(string(monitorPrompt))
|
||||
body["metadata"] = map[string]any{"user_id": "invalid"}
|
||||
return body
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "unrelated prompt",
|
||||
headers: validHeaders,
|
||||
body: validBody("You are a different security classifier for coding agents."),
|
||||
wantAccept: false,
|
||||
},
|
||||
{
|
||||
name: "opening sentence alone",
|
||||
headers: validHeaders,
|
||||
body: validBody(claudeCodeSecurityMonitorPromptPrefix),
|
||||
wantAccept: false,
|
||||
},
|
||||
{
|
||||
name: "opening sentence plus arbitrary altered suffix",
|
||||
headers: validHeaders,
|
||||
body: validBody(claudeCodeSecurityMonitorPromptPrefix + "\n\n" +
|
||||
strings.Repeat("This is arbitrary altered classifier content. ", 300)),
|
||||
wantAccept: false,
|
||||
},
|
||||
{
|
||||
name: "multiple system entries without billing block",
|
||||
headers: validHeaders,
|
||||
body: func() map[string]any {
|
||||
body := validBody(string(monitorPrompt))
|
||||
body["system"] = append(body["system"].([]any), map[string]any{
|
||||
"type": "text",
|
||||
"text": "Additional unrelated system content.",
|
||||
})
|
||||
return body
|
||||
}(),
|
||||
wantAccept: false,
|
||||
},
|
||||
}
|
||||
|
||||
validator := NewClaudeCodeValidator()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
|
||||
for name, value := range tt.headers {
|
||||
req.Header.Set(name, value)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.wantAccept, validator.Validate(req, tt.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCodeValidator_BillingBlockVSCodeEntrypointRecognized(t *testing.T) {
|
||||
// 回归:Claude Code 在 VSCode 扩展内运行时,计费块入口为 cc_entrypoint=claude-vscode
|
||||
// 而非 cli。其安全监视器子请求同样不携带身份 prose,此前写死 cc_entrypoint=cli 的
|
||||
|
||||
Reference in New Issue
Block a user