From cbc2a3dd46ded3dce0c25e8be6fd58d2aa209e6e Mon Sep 17 00:00:00 2001 From: feeeei Date: Fri, 7 Aug 2026 09:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B1=A0=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=20Gemini=20=E8=B4=A6=E5=8F=B7=E8=A2=AB=20429=20=E6=89=93?= =?UTF-8?q?=E4=B8=8A=E8=B4=A6=E5=8F=B7=E7=BA=A7=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 429 的标记点在重试循环内,先于 CheckErrorPolicy 执行,池模式豁免只能落在 handleGeminiUpstreamError 自身;否则一次上游 429 会把账号锁到 PST 午夜, 即便重试已经成功返回客户端 - 判定条件与 HandleUpstreamError 对齐:自定义错误码优先级高于池模式; 401/403/529 仍委派给 RateLimitService,临时不可调度规则不受影响 - chat completions 路径的策略分发改为只有 None / Matched 才处理账号状态, 与 messages 兼容层的 switch 一致,ErrorPolicySkipped 不再漏进来 --- .../gemini_chat_completions_compat_service.go | 4 +- .../service/gemini_error_policy_test.go | 99 +++++++++++++++++++ .../service/gemini_messages_compat_service.go | 5 + 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/gemini_chat_completions_compat_service.go b/backend/internal/service/gemini_chat_completions_compat_service.go index 228f0153e..e76fbcee1 100644 --- a/backend/internal/service/gemini_chat_completions_compat_service.go +++ b/backend/internal/service/gemini_chat_completions_compat_service.go @@ -215,7 +215,9 @@ func (s *GeminiMessagesCompatService) forwardClaudeBodyAsChatCompletions( if s.rateLimitService != nil { policy = s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, respBody, mappedModel) } - if policy != ErrorPolicyTempUnscheduled { + // 与 messages 兼容层一致:只有 None / Matched 才走账号状态处理。 + // Skipped(池模式、或自定义错误码未命中)与 TempUnscheduled 已由策略层裁决完毕。 + if policy == ErrorPolicyNone || policy == ErrorPolicyMatched { s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody) } evBody := unwrapIfNeeded(account.Type == AccountTypeOAuth, respBody) diff --git a/backend/internal/service/gemini_error_policy_test.go b/backend/internal/service/gemini_error_policy_test.go index e71bd141b..46024077a 100644 --- a/backend/internal/service/gemini_error_policy_test.go +++ b/backend/internal/service/gemini_error_policy_test.go @@ -489,6 +489,105 @@ func TestHandleGeminiUpstreamError_GoogleOneCapacityExhaustedUsesTierCooldown(t require.True(t, repo.lastRateLimitReset.Before(after.Add(5*time.Minute).Add(2*time.Second))) } +// --------------------------------------------------------------------------- +// TestHandleGeminiUpstreamError_PoolMode429 — 池模式账号的 429 不写账号级限流。 +// +// 429 的标记点在重试循环内(handleClaudeCompat / forwardNativeGemini / +// chat completions 三条路径),先于 CheckErrorPolicy 执行,池模式豁免只能落在 +// handleGeminiUpstreamError 自身;否则一次上游 429 会把账号锁到 PST 午夜, +// 即便重试已经成功返回客户端。 +// --------------------------------------------------------------------------- + +func TestHandleGeminiUpstreamError_PoolMode429(t *testing.T) { + // 中转上游的真实 429 文案:不含 "per day",也没有 quotaResetDelay, + // 解析失败后 apikey 账号会落到 PST 午夜兜底。 + body := []byte(`{"error":{"code":429,"message":"You have exhausted your capacity on this model. Your quota will reset after 6h53m10s."}}`) + + tests := []struct { + name string + account *Account + expectRateLimited bool + }{ + { + name: "pool_mode_apikey_stays_in_pool", + account: &Account{ + ID: 600, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{"pool_mode": true}, + }, + expectRateLimited: false, + }, + { + name: "custom_error_codes_hit_overrides_pool_mode", + account: &Account{ + ID: 601, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "pool_mode": true, + "custom_error_codes_enabled": true, + "custom_error_codes": []any{float64(429)}, + }, + }, + expectRateLimited: true, + }, + { + name: "custom_error_codes_miss_skips", + account: &Account{ + ID: 602, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "pool_mode": true, + "custom_error_codes_enabled": true, + "custom_error_codes": []any{float64(500)}, + }, + }, + expectRateLimited: false, + }, + { + name: "non_pool_apikey_still_rate_limited", + account: &Account{ + ID: 603, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + }, + expectRateLimited: true, + }, + { + name: "oauth_account_ignores_pool_mode_flag", + account: &Account{ + ID: 604, + Platform: PlatformGemini, + Type: AccountTypeOAuth, + Credentials: map[string]any{"pool_mode": true}, + }, + expectRateLimited: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &rateLimit429AccountRepoStub{} + svc := &GeminiMessagesCompatService{ + accountRepo: repo, + rateLimitService: NewRateLimitService(repo, nil, &config.Config{}, nil, nil), + } + + svc.handleGeminiUpstreamError(context.Background(), tt.account, http.StatusTooManyRequests, http.Header{}, body) + + if !tt.expectRateLimited { + require.Zero(t, repo.rateLimitCalls, "池模式账号不应被标记账号级限流") + return + } + require.Equal(t, 1, repo.rateLimitCalls) + require.Equal(t, tt.account.ID, repo.lastRateLimitID) + require.True(t, repo.lastRateLimitReset.After(time.Now())) + }) + } +} + type geminiErrorPolicyRepo struct { mockAccountRepoForGemini setErrorCalls int diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index fcedf9a21..e62a24223 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -2959,6 +2959,11 @@ func (s *GeminiMessagesCompatService) handleGeminiUpstreamError(ctx context.Cont if statusCode != 429 { return } + // 池模式账号不写账号级限流:账号留在池内,由 failover / 同号重试消化 429。 + // 自定义错误码优先级高于池模式,开启后仍按其命中结果标记。 + if account.IsPoolMode() && !account.IsCustomErrorCodesEnabled() { + return + } oauthType := account.GeminiOAuthType() tierID := account.GeminiTierID()