mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-12 17:24:13 +00:00
fix(executor): fix OAuth extra usage detection by Anthropic API
Three changes to avoid Anthropic's content-based system prompt validation: 1. Fix identity prefix: Use 'You are Claude Code, Anthropic's official CLI for Claude.' instead of the SDK agent prefix, matching real Claude Code. 2. Move user system instructions to user message: Only keep billing header + identity prefix in system[] array. User system instructions are prepended to the first user message as <system-reminder> blocks. 3. Enable cch signing for OAuth tokens by default: The xxHash64 cch integrity check was previously gated behind experimentalCCHSigning config flag. Now automatically enabled when using OAuth tokens. Related: router-for-me/CLIProxyAPI#2599
This commit is contained in:
@@ -157,10 +157,13 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
extraBetas, body = extractAndRemoveBetas(body)
|
extraBetas, body = extractAndRemoveBetas(body)
|
||||||
bodyForTranslation := body
|
bodyForTranslation := body
|
||||||
bodyForUpstream := body
|
bodyForUpstream := body
|
||||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
oauthToken := isClaudeOAuthToken(apiKey)
|
||||||
|
if oauthToken && !auth.ToolPrefixDisabled() {
|
||||||
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
||||||
}
|
}
|
||||||
if experimentalCCHSigningEnabled(e.cfg, auth) {
|
// Enable cch signing by default for OAuth tokens (not just experimental flag).
|
||||||
|
// Claude Code always computes cch; missing or invalid cch is a detectable fingerprint.
|
||||||
|
if oauthToken || experimentalCCHSigningEnabled(e.cfg, auth) {
|
||||||
bodyForUpstream = signAnthropicMessagesBody(bodyForUpstream)
|
bodyForUpstream = signAnthropicMessagesBody(bodyForUpstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,10 +328,12 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
extraBetas, body = extractAndRemoveBetas(body)
|
extraBetas, body = extractAndRemoveBetas(body)
|
||||||
bodyForTranslation := body
|
bodyForTranslation := body
|
||||||
bodyForUpstream := body
|
bodyForUpstream := body
|
||||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
oauthToken := isClaudeOAuthToken(apiKey)
|
||||||
|
if oauthToken && !auth.ToolPrefixDisabled() {
|
||||||
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
||||||
}
|
}
|
||||||
if experimentalCCHSigningEnabled(e.cfg, auth) {
|
// Enable cch signing by default for OAuth tokens (not just experimental flag).
|
||||||
|
if oauthToken || experimentalCCHSigningEnabled(e.cfg, auth) {
|
||||||
bodyForUpstream = signAnthropicMessagesBody(bodyForUpstream)
|
bodyForUpstream = signAnthropicMessagesBody(bodyForUpstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,47 +1296,91 @@ func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, exp
|
|||||||
// Including any cache_control here creates an intra-system TTL ordering violation
|
// Including any cache_control here creates an intra-system TTL ordering violation
|
||||||
// when the client's system blocks use ttl='1h' (prompt-caching-scope-2026-01-05 beta
|
// when the client's system blocks use ttl='1h' (prompt-caching-scope-2026-01-05 beta
|
||||||
// forbids 1h blocks after 5m blocks, and a no-TTL block defaults to 5m).
|
// forbids 1h blocks after 5m blocks, and a no-TTL block defaults to 5m).
|
||||||
agentBlock := `{"type":"text","text":"You are a Claude agent, built on Anthropic's Claude Agent SDK."}`
|
// Use Claude Code identity prefix for interactive CLI mode.
|
||||||
|
// Real Claude Code uses "You are Claude Code, Anthropic's official CLI for Claude."
|
||||||
|
// when running in interactive mode (the most common case).
|
||||||
|
agentBlock := `{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude."}`
|
||||||
|
|
||||||
if strictMode {
|
|
||||||
// Strict mode: billing header + agent identifier only
|
|
||||||
result := "[" + billingBlock + "," + agentBlock + "]"
|
|
||||||
payload, _ = sjson.SetRawBytes(payload, "system", []byte(result))
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-strict mode: billing header + agent identifier + user system messages
|
|
||||||
// Skip if already injected
|
// Skip if already injected
|
||||||
firstText := gjson.GetBytes(payload, "system.0.text").String()
|
firstText := gjson.GetBytes(payload, "system.0.text").String()
|
||||||
if strings.HasPrefix(firstText, "x-anthropic-billing-header:") {
|
if strings.HasPrefix(firstText, "x-anthropic-billing-header:") {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
result := "[" + billingBlock + "," + agentBlock
|
// system[] only keeps billing header + agent identifier.
|
||||||
|
// User system instructions are moved to the first user message to avoid
|
||||||
|
// Anthropic's content-based system prompt validation (extra usage detection).
|
||||||
|
systemResult := "[" + billingBlock + "," + agentBlock + "]"
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, "system", []byte(systemResult))
|
||||||
|
|
||||||
|
// Collect user system instructions and prepend to first user message
|
||||||
|
var userSystemParts []string
|
||||||
if system.IsArray() {
|
if system.IsArray() {
|
||||||
system.ForEach(func(_, part gjson.Result) bool {
|
system.ForEach(func(_, part gjson.Result) bool {
|
||||||
if part.Get("type").String() == "text" {
|
if part.Get("type").String() == "text" {
|
||||||
// Add cache_control to user system messages if not present.
|
txt := strings.TrimSpace(part.Get("text").String())
|
||||||
// Do NOT add ttl — let it inherit the default (5m) to avoid
|
if txt != "" {
|
||||||
// TTL ordering violations with the prompt-caching-scope-2026-01-05 beta.
|
userSystemParts = append(userSystemParts, txt)
|
||||||
partJSON := part.Raw
|
|
||||||
if !part.Get("cache_control").Exists() {
|
|
||||||
updated, _ := sjson.SetBytes([]byte(partJSON), "cache_control.type", "ephemeral")
|
|
||||||
partJSON = string(updated)
|
|
||||||
}
|
}
|
||||||
result += "," + partJSON
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else if system.Type == gjson.String && system.String() != "" {
|
} else if system.Type == gjson.String && strings.TrimSpace(system.String()) != "" {
|
||||||
partJSON := `{"type":"text","cache_control":{"type":"ephemeral"}}`
|
userSystemParts = append(userSystemParts, strings.TrimSpace(system.String()))
|
||||||
updated, _ := sjson.SetBytes([]byte(partJSON), "text", system.String())
|
}
|
||||||
partJSON = string(updated)
|
|
||||||
result += "," + partJSON
|
if !strictMode && len(userSystemParts) > 0 {
|
||||||
|
combined := strings.Join(userSystemParts, "\n\n")
|
||||||
|
payload = prependToFirstUserMessage(payload, combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// prependToFirstUserMessage prepends text content to the first user message.
|
||||||
|
// This avoids putting non-Claude-Code system instructions in system[] which
|
||||||
|
// triggers Anthropic's extra usage billing for OAuth-proxied requests.
|
||||||
|
func prependToFirstUserMessage(payload []byte, text string) []byte {
|
||||||
|
messages := gjson.GetBytes(payload, "messages")
|
||||||
|
if !messages.Exists() || !messages.IsArray() {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first user message index
|
||||||
|
firstUserIdx := -1
|
||||||
|
messages.ForEach(func(idx, msg gjson.Result) bool {
|
||||||
|
if msg.Get("role").String() == "user" {
|
||||||
|
firstUserIdx = int(idx.Int())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if firstUserIdx < 0 {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixBlock := fmt.Sprintf(`<system-reminder>
|
||||||
|
As you answer the user's questions, you can use the following context from the system:
|
||||||
|
%s
|
||||||
|
|
||||||
|
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
|
||||||
|
</system-reminder>
|
||||||
|
`, text)
|
||||||
|
|
||||||
|
contentPath := fmt.Sprintf("messages.%d.content", firstUserIdx)
|
||||||
|
content := gjson.GetBytes(payload, contentPath)
|
||||||
|
|
||||||
|
if content.IsArray() {
|
||||||
|
newBlock := fmt.Sprintf(`{"type":"text","text":%q}`, prefixBlock)
|
||||||
|
existing := content.Raw
|
||||||
|
newArray := "[" + newBlock + "," + existing[1:]
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray))
|
||||||
|
} else if content.Type == gjson.String {
|
||||||
|
newText := prefixBlock + content.String()
|
||||||
|
payload, _ = sjson.SetBytes(payload, contentPath, newText)
|
||||||
}
|
}
|
||||||
result += "]"
|
|
||||||
|
|
||||||
payload, _ = sjson.SetRawBytes(payload, "system", []byte(result))
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,7 +1388,9 @@ func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, exp
|
|||||||
// Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation.
|
// Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation.
|
||||||
func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string, apiKey string) []byte {
|
func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string, apiKey string) []byte {
|
||||||
clientUserAgent := getClientUserAgent(ctx)
|
clientUserAgent := getClientUserAgent(ctx)
|
||||||
useExperimentalCCHSigning := experimentalCCHSigningEnabled(cfg, auth)
|
// Enable cch signing for OAuth tokens by default (not just experimental flag).
|
||||||
|
oauthToken := isClaudeOAuthToken(apiKey)
|
||||||
|
useCCHSigning := oauthToken || experimentalCCHSigningEnabled(cfg, auth)
|
||||||
|
|
||||||
// Get cloak config from ClaudeKey configuration
|
// Get cloak config from ClaudeKey configuration
|
||||||
cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth)
|
cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth)
|
||||||
@@ -1376,7 +1427,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
|
|||||||
billingVersion := helps.DefaultClaudeVersion(cfg)
|
billingVersion := helps.DefaultClaudeVersion(cfg)
|
||||||
entrypoint := parseEntrypointFromUA(clientUserAgent)
|
entrypoint := parseEntrypointFromUA(clientUserAgent)
|
||||||
workload := getWorkloadFromContext(ctx)
|
workload := getWorkloadFromContext(ctx)
|
||||||
payload = checkSystemInstructionsWithSigningMode(payload, strictMode, useExperimentalCCHSigning, billingVersion, entrypoint, workload)
|
payload = checkSystemInstructionsWithSigningMode(payload, strictMode, useCCHSigning, billingVersion, entrypoint, workload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject fake user ID
|
// Inject fake user ID
|
||||||
|
|||||||
Reference in New Issue
Block a user