Merge pull request #5352 from feeeei/main

fix(gemini): 修复Gemini池模式时,依然被 429 response 触发账户限流问题
This commit is contained in:
Wesley Liddick
2026-08-09 14:55:02 +08:00
committed by GitHub
3 changed files with 107 additions and 1 deletions
@@ -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)
@@ -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
@@ -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()