@@ -25,6 +25,10 @@ const claudeToolPrefix = "proxy_"
2525// userIDPattern matches Claude Code format: user_[64-hex]_account__session_[uuid-v4]
2626var userIDPattern = regexp .MustCompile (`^user_[a-fA-F0-9]{64}_account__session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` )
2727
28+ // claudeCLIUserAgentPattern matches official Claude CLI user agent pattern.
29+ // Aligns with sub2api/claude-relay-service detection: claude-cli/x.y.z
30+ var claudeCLIUserAgentPattern = regexp .MustCompile (`(?i)^claude-cli/\d+\.\d+\.\d+` )
31+
2832// processClaudeRequestBody processes Claude request body before sending to upstream.
2933// Following CLIProxyAPI order:
3034// 1. applyCloaking (system prompt injection, fake user_id, sensitive word obfuscation)
@@ -74,10 +78,9 @@ func applyCloaking(body []byte, clientUserAgent string, model string, cloakCfg *
7478 return body
7579 }
7680
77- // Skip system instructions for claude-3-5-haiku models (CLIProxyAPI behavior)
78- if ! strings .HasPrefix (model , "claude-3-5-haiku" ) {
79- body = checkSystemInstructionsWithMode (body , strictMode )
80- }
81+ // Always ensure Claude Code system prompt for cloaked requests.
82+ // This keeps messages-path requests compatible with strict Claude client validators.
83+ body = checkSystemInstructionsWithMode (body , strictMode )
8184
8285 // Inject fake user_id
8386 body = injectFakeUserID (body )
@@ -93,7 +96,7 @@ func applyCloaking(body []byte, clientUserAgent string, model string, cloakCfg *
9396
9497// isClaudeCodeClient checks if the User-Agent indicates a Claude Code client.
9598func isClaudeCodeClient (userAgent string ) bool {
96- return strings .HasPrefix (userAgent , "claude-cli" )
99+ return claudeCLIUserAgentPattern . MatchString ( strings .TrimSpace (userAgent ) )
97100}
98101
99102func isClaudeOAuthToken (apiKey string ) bool {
@@ -273,7 +276,7 @@ func shouldCloak(cloakMode string, userAgent string) bool {
273276 case "never" :
274277 return false
275278 default : // "auto" or empty
276- return ! strings . HasPrefix (userAgent , "claude-cli" )
279+ return ! isClaudeCodeClient (userAgent )
277280 }
278281}
279282
@@ -336,30 +339,70 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) {
336339// In strict mode, it replaces all user system messages.
337340// In non-strict mode (default), it prepends to existing system messages.
338341func checkSystemInstructionsWithMode (body []byte , strictMode bool ) []byte {
339- system := gjson .GetBytes (body , "system" )
342+ if hasClaudeCodeSystemPrompt (body ) {
343+ return body
344+ }
345+
340346 claudeCodeInstructions := `[{"type":"text","text":"` + claudeCodeSystemPrompt + `"}]`
341347
342348 if strictMode {
343349 body , _ = sjson .SetRawBytes (body , "system" , []byte (claudeCodeInstructions ))
344350 return body
345351 }
346352
353+ system := gjson .GetBytes (body , "system" )
347354 if system .IsArray () {
348- if gjson .GetBytes (body , "system.0.text" ).String () != claudeCodeSystemPrompt {
349- system .ForEach (func (_ , part gjson.Result ) bool {
350- if part .Get ("type" ).String () == "text" {
351- claudeCodeInstructions , _ = sjson .SetRaw (claudeCodeInstructions , "-1" , part .Raw )
352- }
353- return true
354- })
355- body , _ = sjson .SetRawBytes (body , "system" , []byte (claudeCodeInstructions ))
356- }
357- } else {
355+ system .ForEach (func (_ , part gjson.Result ) bool {
356+ if part .Get ("type" ).String () == "text" {
357+ claudeCodeInstructions , _ = sjson .SetRaw (claudeCodeInstructions , "-1" , part .Raw )
358+ }
359+ return true
360+ })
358361 body , _ = sjson .SetRawBytes (body , "system" , []byte (claudeCodeInstructions ))
362+ return body
359363 }
364+
365+ if system .Type == gjson .String && strings .TrimSpace (system .String ()) != "" {
366+ existingBlock := `{"type":"text","text":` + system .Raw + `}`
367+ claudeCodeInstructions , _ = sjson .SetRaw (claudeCodeInstructions , "-1" , existingBlock )
368+ }
369+ body , _ = sjson .SetRawBytes (body , "system" , []byte (claudeCodeInstructions ))
360370 return body
361371}
362372
373+ func hasClaudeCodeSystemPrompt (body []byte ) bool {
374+ system := gjson .GetBytes (body , "system" )
375+ if ! system .Exists () {
376+ return false
377+ }
378+
379+ if system .IsArray () {
380+ found := false
381+ system .ForEach (func (_ , part gjson.Result ) bool {
382+ if strings .TrimSpace (part .Get ("text" ).String ()) == claudeCodeSystemPrompt {
383+ found = true
384+ return false
385+ }
386+ if part .Type == gjson .String && strings .TrimSpace (part .String ()) == claudeCodeSystemPrompt {
387+ found = true
388+ return false
389+ }
390+ return true
391+ })
392+ return found
393+ }
394+
395+ if system .Type == gjson .String {
396+ return strings .TrimSpace (system .String ()) == claudeCodeSystemPrompt
397+ }
398+
399+ if system .IsObject () {
400+ return strings .TrimSpace (system .Get ("text" ).String ()) == claudeCodeSystemPrompt
401+ }
402+
403+ return false
404+ }
405+
363406// ===== Sensitive word obfuscation (CLIProxyAPI-aligned) =====
364407
365408// zeroWidthSpace is the Unicode zero-width space character used for obfuscation.
0 commit comments