From 77ae2bcccb1f38d3be586d33ab613d0a06bb8179 Mon Sep 17 00:00:00 2001 From: erio Date: Fri, 6 Mar 2026 18:32:57 +0800 Subject: [PATCH] fix: exclude Antigravity from OAuth 401 temp-unschedulable and escalation logic PR #723 changed all OAuth 401 to use SetTempUnschedulable instead of SetError, and PR #761 added 401 escalation logic in tryTempUnschedulable. Both caused non-OpenAI accounts to frequently become temporarily unschedulable. - HandleUpstreamError case 401: limit temp-unschedulable to OpenAI OAuth only, other platforms (Anthropic/Gemini/Antigravity) revert to original SetError - tryTempUnschedulable: skip 401 escalation for Antigravity, let its applyErrorPolicy temp_unschedulable_rules handle 401 naturally Co-Authored-By: Claude Opus 4.6 --- backend/internal/service/error_policy_test.go | 24 +++++++++- backend/internal/service/ratelimit_service.go | 8 ++-- .../ratelimit_service_401_db_fallback_test.go | 3 +- .../service/ratelimit_service_401_test.go | 47 ++++++++++++------- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/backend/internal/service/error_policy_test.go b/backend/internal/service/error_policy_test.go index 59375cf50a..62aba88fdd 100644 --- a/backend/internal/service/error_policy_test.go +++ b/backend/internal/service/error_policy_test.go @@ -114,7 +114,7 @@ func TestCheckErrorPolicy(t *testing.T) { account: &Account{ ID: 15, Type: AccountTypeOAuth, - Platform: PlatformAntigravity, + Platform: PlatformGemini, // 非 Antigravity 平台才有 401 升级逻辑 TempUnschedulableReason: `{"status_code":401,"until_unix":1735689600}`, Credentials: map[string]any{ "temp_unschedulable_enabled": true, @@ -131,6 +131,28 @@ func TestCheckErrorPolicy(t *testing.T) { body: []byte(`unauthorized`), expected: ErrorPolicyNone, }, + { + name: "temp_unschedulable_401_antigravity_no_escalation", + account: &Account{ + ID: 16, + Type: AccountTypeOAuth, + Platform: PlatformAntigravity, // Antigravity 跳过 401 升级,由 rules 正常处理 + TempUnschedulableReason: `{"status_code":401,"until_unix":1735689600}`, + Credentials: map[string]any{ + "temp_unschedulable_enabled": true, + "temp_unschedulable_rules": []any{ + map[string]any{ + "error_code": float64(401), + "keywords": []any{"unauthorized"}, + "duration_minutes": float64(10), + }, + }, + }, + }, + statusCode: 401, + body: []byte(`unauthorized`), + expected: ErrorPolicyTempUnscheduled, // Antigravity 不升级,继续走规则匹配 + }, { name: "temp_unschedulable_body_miss_returns_none", account: &Account{ diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index 9f16fb2b50..a888a2eeeb 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -128,8 +128,9 @@ func (s *RateLimitService) HandleUpstreamError(ctx context.Context, account *Acc } // 其他 400 错误(如参数问题)不处理,不禁用账号 case 401: - // 对所有 OAuth 账号在 401 错误时调用缓存失效并强制下次刷新 - if account.Type == AccountTypeOAuth { + // OpenAI OAuth 账号在 401 错误时临时不可调度(保持 active 让刷新服务恢复); + // 其他平台 OAuth 账号保持原有 SetError 行为(Antigravity 主流程不走此路径)。 + if account.Type == AccountTypeOAuth && account.Platform == PlatformOpenAI { // 1. 失效缓存 if s.tokenCacheInvalidator != nil { if err := s.tokenCacheInvalidator.InvalidateToken(ctx, account); err != nil { @@ -1093,7 +1094,8 @@ func (s *RateLimitService) tryTempUnschedulable(ctx context.Context, account *Ac } // 401 首次命中可临时不可调度(给 token 刷新窗口); // 若历史上已因 401 进入过临时不可调度,则本次应升级为 error(返回 false 交由默认错误逻辑处理)。 - if statusCode == http.StatusUnauthorized { + // Antigravity 跳过:其 401 由 applyErrorPolicy 的 temp_unschedulable_rules 自行控制,无需升级逻辑。 + if statusCode == http.StatusUnauthorized && account.Platform != PlatformAntigravity { reason := account.TempUnschedulableReason // 缓存可能没有 reason,从 DB 回退读取 if reason == "" { diff --git a/backend/internal/service/ratelimit_service_401_db_fallback_test.go b/backend/internal/service/ratelimit_service_401_db_fallback_test.go index e1611425f5..ad2c9c81e7 100644 --- a/backend/internal/service/ratelimit_service_401_db_fallback_test.go +++ b/backend/internal/service/ratelimit_service_401_db_fallback_test.go @@ -28,6 +28,7 @@ func (r *dbFallbackRepoStub) GetByID(ctx context.Context, id int64) (*Account, e func TestCheckErrorPolicy_401_DBFallback_Escalates(t *testing.T) { // Scenario: cache account has empty TempUnschedulableReason (cache miss), // but DB account has a previous 401 record → should escalate to ErrorPolicyNone. + // 注意:Antigravity 跳过 401 升级,此测试使用 Gemini 平台验证升级逻辑。 repo := &dbFallbackRepoStub{ dbAccount: &Account{ ID: 20, @@ -39,7 +40,7 @@ func TestCheckErrorPolicy_401_DBFallback_Escalates(t *testing.T) { account := &Account{ ID: 20, Type: AccountTypeOAuth, - Platform: PlatformAntigravity, + Platform: PlatformGemini, TempUnschedulableReason: "", // cache miss — reason is empty Credentials: map[string]any{ "temp_unschedulable_enabled": true, diff --git a/backend/internal/service/ratelimit_service_401_test.go b/backend/internal/service/ratelimit_service_401_test.go index 7bced46f4f..653e2fba4c 100644 --- a/backend/internal/service/ratelimit_service_401_test.go +++ b/backend/internal/service/ratelimit_service_401_test.go @@ -41,13 +41,37 @@ func (r *tokenCacheInvalidatorRecorder) InvalidateToken(ctx context.Context, acc return r.err } -func TestRateLimitService_HandleUpstreamError_OAuth401SetsTempUnschedulable(t *testing.T) { +// TestRateLimitService_HandleUpstreamError_OpenAIOAuth401SetsTempUnschedulable +// 仅 OpenAI OAuth 账号 401 使用 temp_unschedulable +func TestRateLimitService_HandleUpstreamError_OpenAIOAuth401SetsTempUnschedulable(t *testing.T) { + repo := &rateLimitAccountRepoStub{} + invalidator := &tokenCacheInvalidatorRecorder{} + service := NewRateLimitService(repo, nil, &config.Config{}, nil, nil) + service.SetTokenCacheInvalidator(invalidator) + account := &Account{ + ID: 100, + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + } + + shouldDisable := service.HandleUpstreamError(context.Background(), account, 401, http.Header{}, []byte("unauthorized")) + + require.True(t, shouldDisable) + require.Equal(t, 0, repo.setErrorCalls) + require.Equal(t, 1, repo.tempCalls) + require.Len(t, invalidator.accounts, 1) +} + +// TestRateLimitService_HandleUpstreamError_NonOpenAIOAuth401SetsError +// 非 OpenAI 的 OAuth 账号 401 走原有 SetError 行为 +func TestRateLimitService_HandleUpstreamError_NonOpenAIOAuth401SetsError(t *testing.T) { tests := []struct { name string platform string }{ {name: "gemini", platform: PlatformGemini}, {name: "antigravity", platform: PlatformAntigravity}, + {name: "anthropic", platform: PlatformAnthropic}, } for _, tt := range tests { @@ -60,29 +84,20 @@ func TestRateLimitService_HandleUpstreamError_OAuth401SetsTempUnschedulable(t *t ID: 100, Platform: tt.platform, Type: AccountTypeOAuth, - Credentials: map[string]any{ - "temp_unschedulable_enabled": true, - "temp_unschedulable_rules": []any{ - map[string]any{ - "error_code": 401, - "keywords": []any{"unauthorized"}, - "duration_minutes": 30, - "description": "custom rule", - }, - }, - }, } shouldDisable := service.HandleUpstreamError(context.Background(), account, 401, http.Header{}, []byte("unauthorized")) require.True(t, shouldDisable) - require.Equal(t, 0, repo.setErrorCalls) - require.Equal(t, 1, repo.tempCalls) - require.Len(t, invalidator.accounts, 1) + require.Equal(t, 1, repo.setErrorCalls) + require.Equal(t, 0, repo.tempCalls) + require.Contains(t, repo.lastErrorMsg, "Authentication failed (401)") }) } } +// TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError +// OpenAI OAuth 401 缓存失效出错时仍走 temp_unschedulable func TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError(t *testing.T) { repo := &rateLimitAccountRepoStub{} invalidator := &tokenCacheInvalidatorRecorder{err: errors.New("boom")} @@ -90,7 +105,7 @@ func TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError(t *testin service.SetTokenCacheInvalidator(invalidator) account := &Account{ ID: 101, - Platform: PlatformGemini, + Platform: PlatformOpenAI, Type: AccountTypeOAuth, }