mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:48:43 +08:00
Merge pull request #5352 from feeeei/main
fix(gemini): 修复Gemini池模式时,依然被 429 response 触发账户限流问题
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user