Commit b717956c authored by shaw's avatar shaw Committed by 陈曦
Browse files

fix: 非Claude Code客户端system prompt迁移至messages以绕过第三方应用检测

Anthropic近期引入基于system参数内容的第三方应用检测机制,原有的前置追加
Claude Code提示词策略无法通过检测(后续内容仍为非Claude Code格式触发429)。

新策略:对非Claude Code客户端的OAuth/SetupToken账号请求,将system字段
完整替换为Claude Code标识提示词,原始system内容作为user/assistant消息对
注入messages开头,模型仍接收完整指令。

仅影响/v1/messages路径,chat_completions和responses路径保持原有逻辑不变。
真正的Claude Code客户端请求完全不受影响(原样透传)。
parent 4fee20ec
...@@ -3955,7 +3955,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A ...@@ -3955,7 +3955,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
// Beta policy: evaluate once; block check + cache filter set for buildUpstreamRequest. // Beta policy: evaluate once; block check + cache filter set for buildUpstreamRequest.
// Always overwrite the cache to prevent stale values from a previous retry with a different account. // Always overwrite the cache to prevent stale values from a previous retry with a different account.
if account.Platform == PlatformAnthropic && c != nil { if account.Platform == PlatformAnthropic && c != nil {
policy := s.evaluateBetaPolicy(ctx, c.GetHeader("anthropic-beta"), account, parsed.Model) policy := s.evaluateBetaPolicy(ctx, c.GetHeader("anthropic-beta"), account)
if policy.blockErr != nil { if policy.blockErr != nil {
return nil, policy.blockErr return nil, policy.blockErr
} }
...@@ -5617,8 +5617,9 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex ...@@ -5617,8 +5617,9 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
} }
// Build effective drop set: merge static defaults with dynamic beta policy filter rules // Build effective drop set: merge static defaults with dynamic beta policy filter rules
policyFilterSet := s.getBetaPolicyFilterSet(ctx, c, account, modelID) policyFilterSet := s.getBetaPolicyFilterSet(ctx, c, account)
effectiveDropSet := mergeDropSets(policyFilterSet) effectiveDropSet := mergeDropSets(policyFilterSet)
effectiveDropWithClaudeCodeSet := mergeDropSets(policyFilterSet, claude.BetaClaudeCode)
// 处理 anthropic-beta header(OAuth 账号需要包含 oauth beta) // 处理 anthropic-beta header(OAuth 账号需要包含 oauth beta)
if tokenType == "oauth" { if tokenType == "oauth" {
...@@ -5629,16 +5630,11 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex ...@@ -5629,16 +5630,11 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
applyClaudeCodeMimicHeaders(req, reqStream) applyClaudeCodeMimicHeaders(req, reqStream)
incomingBeta := getHeaderRaw(req.Header, "anthropic-beta") incomingBeta := getHeaderRaw(req.Header, "anthropic-beta")
// Claude Code OAuth credentials are scoped to Claude Code. // Match real Claude CLI traffic (per mitmproxy reports):
// Non-haiku models MUST include claude-code beta for Anthropic to recognize // messages requests typically use only oauth + interleaved-thinking.
// this as a legitimate Claude Code request; without it, the request is // Also drop claude-code beta if a downstream client added it.
// rejected as third-party ("out of extra usage").
// Haiku models are exempt from third-party detection and don't need it.
requiredBetas := []string{claude.BetaOAuth, claude.BetaInterleavedThinking} requiredBetas := []string{claude.BetaOAuth, claude.BetaInterleavedThinking}
if !strings.Contains(strings.ToLower(modelID), "haiku") { setHeaderRaw(req.Header, "anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, effectiveDropWithClaudeCodeSet))
requiredBetas = []string{claude.BetaClaudeCode, claude.BetaOAuth, claude.BetaInterleavedThinking}
}
setHeaderRaw(req.Header, "anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, effectiveDropSet))
} else { } else {
// Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta // Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta
clientBetaHeader := getHeaderRaw(req.Header, "anthropic-beta") clientBetaHeader := getHeaderRaw(req.Header, "anthropic-beta")
...@@ -5861,7 +5857,7 @@ type betaPolicyResult struct { ...@@ -5861,7 +5857,7 @@ type betaPolicyResult struct {
} }
// evaluateBetaPolicy loads settings once and evaluates all rules against the given request. // evaluateBetaPolicy loads settings once and evaluates all rules against the given request.
func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader string, account *Account, model string) betaPolicyResult { func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader string, account *Account) betaPolicyResult {
if s.settingService == nil { if s.settingService == nil {
return betaPolicyResult{} return betaPolicyResult{}
} }
...@@ -5876,11 +5872,10 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri ...@@ -5876,11 +5872,10 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model) switch rule.Action {
switch effectiveAction {
case BetaPolicyActionBlock: case BetaPolicyActionBlock:
if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) { if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) {
msg := effectiveErrMsg msg := rule.ErrorMessage
if msg == "" { if msg == "" {
msg = "beta feature " + rule.BetaToken + " is not allowed" msg = "beta feature " + rule.BetaToken + " is not allowed"
} }
...@@ -5922,7 +5917,7 @@ const betaPolicyFilterSetKey = "betaPolicyFilterSet" ...@@ -5922,7 +5917,7 @@ const betaPolicyFilterSetKey = "betaPolicyFilterSet"
// In the /v1/messages path, Forward() evaluates the policy first and caches the result; // In the /v1/messages path, Forward() evaluates the policy first and caches the result;
// buildUpstreamRequest reuses it (zero extra DB calls). In the count_tokens path, this // buildUpstreamRequest reuses it (zero extra DB calls). In the count_tokens path, this
// evaluates on demand (one DB call). // evaluates on demand (one DB call).
func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Context, account *Account, model string) map[string]struct{} { func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Context, account *Account) map[string]struct{} {
if c != nil { if c != nil {
if v, ok := c.Get(betaPolicyFilterSetKey); ok { if v, ok := c.Get(betaPolicyFilterSetKey); ok {
if fs, ok := v.(map[string]struct{}); ok { if fs, ok := v.(map[string]struct{}); ok {
...@@ -5930,7 +5925,7 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont ...@@ -5930,7 +5925,7 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont
} }
} }
} }
return s.evaluateBetaPolicy(ctx, "", account, model).filterSet return s.evaluateBetaPolicy(ctx, "", account).filterSet
} }
// betaPolicyScopeMatches checks whether a rule's scope matches the current account type. // betaPolicyScopeMatches checks whether a rule's scope matches the current account type.
...@@ -5949,33 +5944,6 @@ func betaPolicyScopeMatches(scope string, isOAuth bool, isBedrock bool) bool { ...@@ -5949,33 +5944,6 @@ func betaPolicyScopeMatches(scope string, isOAuth bool, isBedrock bool) bool {
} }
} }
// matchModelWhitelist checks if a model matches any pattern in the whitelist.
// Reuses matchModelPattern from group.go which supports exact and wildcard prefix matching.
func matchModelWhitelist(model string, whitelist []string) bool {
for _, pattern := range whitelist {
if matchModelPattern(pattern, model) {
return true
}
}
return false
}
// resolveRuleAction determines the effective action and error message for a rule given the request model.
// When ModelWhitelist is empty, the rule's primary Action/ErrorMessage applies unconditionally.
// When non-empty, Action applies to matching models; FallbackAction/FallbackErrorMessage applies to others.
func resolveRuleAction(rule BetaPolicyRule, model string) (action, errorMessage string) {
if len(rule.ModelWhitelist) == 0 {
return rule.Action, rule.ErrorMessage
}
if matchModelWhitelist(model, rule.ModelWhitelist) {
return rule.Action, rule.ErrorMessage
}
if rule.FallbackAction != "" {
return rule.FallbackAction, rule.FallbackErrorMessage
}
return BetaPolicyActionPass, "" // default fallback: pass (fail-open)
}
// droppedBetaSet returns claude.DroppedBetas as a set, with optional extra tokens. // droppedBetaSet returns claude.DroppedBetas as a set, with optional extra tokens.
func droppedBetaSet(extra ...string) map[string]struct{} { func droppedBetaSet(extra ...string) map[string]struct{} {
m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(extra)) m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(extra))
...@@ -6022,7 +5990,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -6022,7 +5990,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
modelID string, modelID string,
) ([]string, error) { ) ([]string, error) {
// 1. 对原始 header 中的 beta token 做 block 检查(快速失败) // 1. 对原始 header 中的 beta token 做 block 检查(快速失败)
policy := s.evaluateBetaPolicy(ctx, betaHeader, account, modelID) policy := s.evaluateBetaPolicy(ctx, betaHeader, account)
if policy.blockErr != nil { if policy.blockErr != nil {
return nil, policy.blockErr return nil, policy.blockErr
} }
...@@ -6034,7 +6002,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -6034,7 +6002,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
// 例如:管理员 block 了 interleaved-thinking,客户端不在 header 中带该 token, // 例如:管理员 block 了 interleaved-thinking,客户端不在 header 中带该 token,
// 但请求体中包含 thinking 字段 → autoInjectBedrockBetaTokens 会自动补齐 → // 但请求体中包含 thinking 字段 → autoInjectBedrockBetaTokens 会自动补齐 →
// 如果不做此检查,block 规则会被绕过。 // 如果不做此检查,block 规则会被绕过。
if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account, modelID); blockErr != nil { if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account); blockErr != nil {
return nil, blockErr return nil, blockErr
} }
...@@ -6043,7 +6011,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -6043,7 +6011,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
// checkBetaPolicyBlockForTokens 检查 token 列表中是否有被管理员 block 规则命中的 token。 // checkBetaPolicyBlockForTokens 检查 token 列表中是否有被管理员 block 规则命中的 token。
// 用于补充 evaluateBetaPolicy 对 header 的检查,覆盖 body 自动注入的 token。 // 用于补充 evaluateBetaPolicy 对 header 的检查,覆盖 body 自动注入的 token。
func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, tokens []string, account *Account, model string) *BetaBlockedError { func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, tokens []string, account *Account) *BetaBlockedError {
if s.settingService == nil || len(tokens) == 0 { if s.settingService == nil || len(tokens) == 0 {
return nil return nil
} }
...@@ -6055,15 +6023,14 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke ...@@ -6055,15 +6023,14 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke
isBedrock := account.IsBedrock() isBedrock := account.IsBedrock()
tokenSet := buildBetaTokenSet(tokens) tokenSet := buildBetaTokenSet(tokens)
for _, rule := range settings.Rules { for _, rule := range settings.Rules {
effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model) if rule.Action != BetaPolicyActionBlock {
if effectiveAction != BetaPolicyActionBlock {
continue continue
} }
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
if _, present := tokenSet[rule.BetaToken]; present { if _, present := tokenSet[rule.BetaToken]; present {
msg := effectiveErrMsg msg := rule.ErrorMessage
if msg == "" { if msg == "" {
msg = "beta feature " + rule.BetaToken + " is not allowed" msg = "beta feature " + rule.BetaToken + " is not allowed"
} }
...@@ -8521,7 +8488,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con ...@@ -8521,7 +8488,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
} }
// Build effective drop set for count_tokens: merge static defaults with dynamic beta policy filter rules // Build effective drop set for count_tokens: merge static defaults with dynamic beta policy filter rules
ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account, modelID)) ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account))
// OAuth 账号:处理 anthropic-beta header // OAuth 账号:处理 anthropic-beta header
if tokenType == "oauth" { if tokenType == "oauth" {
......
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