From 29bce46a2a6f86472214427869ad98bbc7c4bf83 Mon Sep 17 00:00:00 2001 From: HPnetwork Date: Mon, 14 Sep 2026 05:56:59 -0400 Subject: [PATCH] fix: keep API key accounts available for unknown models --- .../service/model_not_found_error_test.go | 1 + .../service/openai_gateway_upstream_errors.go | 5 ++++ backend/internal/service/ratelimit_service.go | 3 +++ .../service/ratelimit_service_401_test.go | 23 +++++++++++++++++++ .../ratelimit_service_model_not_found_test.go | 23 +++++++++++++++++++ 5 files changed, 55 insertions(+) diff --git a/backend/internal/service/model_not_found_error_test.go b/backend/internal/service/model_not_found_error_test.go index d76d71d0f1..b83204dc95 100644 --- a/backend/internal/service/model_not_found_error_test.go +++ b/backend/internal/service/model_not_found_error_test.go @@ -122,6 +122,7 @@ func TestIsOpenAICompatibleModelNotFound400(t *testing.T) { {name: "structured code", body: `{"error":{"code":"model_not_found","message":"No such deployment"}}`, want: true}, {name: "unknown provider", body: `{"error":{"message":"Unknown provider for model claude-x"}}`, want: true}, {name: "model not found", body: `{"error":{"message":"Model not found: claude-x"}}`, want: true}, + {name: "unknown model", body: `{"error":{"message":"unknown model: claude-x"}}`, want: true}, {name: "model unsupported", body: `{"error":{"message":"The requested model is not supported"}}`, want: true}, {name: "plain text compatible gateway", body: `unknown provider for model claude-x`, want: true}, {name: "invalid parameter remains terminal", body: `{"error":{"code":"invalid_request_error","message":"Invalid value for temperature"}}`, want: false}, diff --git a/backend/internal/service/openai_gateway_upstream_errors.go b/backend/internal/service/openai_gateway_upstream_errors.go index b5985a226e..2490043284 100644 --- a/backend/internal/service/openai_gateway_upstream_errors.go +++ b/backend/internal/service/openai_gateway_upstream_errors.go @@ -286,6 +286,10 @@ func (s *OpenAIGatewayService) shouldFailoverOpenAIUpstreamResponse(account *Acc } func isOpenAICompatibleModelNotFound400(respBody []byte) bool { + return isOpenAICompatibleModelNotFoundBody(respBody) +} + +func isOpenAICompatibleModelNotFoundBody(respBody []byte) bool { code := strings.TrimSpace(extractUpstreamErrorCode(respBody)) if code != "" { return strings.EqualFold(code, "model_not_found") @@ -296,6 +300,7 @@ func isOpenAICompatibleModelNotFound400(respBody []byte) bool { msg = strings.ToLower(strings.TrimSpace(string(respBody))) } return strings.Contains(msg, "unknown provider for model") || + strings.Contains(msg, "unknown model") || strings.Contains(msg, "model not found") || strings.Contains(msg, "model is not supported") } diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index 4158d0559c..92186dd6ab 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -2430,6 +2430,7 @@ func parseOpenAIImageTryAgainCooldown(body []byte) time.Duration { const upstreamModelNotFoundCooldown = 30 * time.Minute const upstreamModelNotFoundReason = "upstream_404_model_not_found" +const upstreamModelNotFound401Reason = "upstream_401_model_not_found" const upstreamCodexPlanGatedModelCooldown = 30 * time.Minute const upstreamCodexPlanGatedModelReason = "upstream_400_codex_plan_gated_model" const tempUnschedBodyMaxBytes = 64 << 10 @@ -2455,6 +2456,8 @@ func (s *RateLimitService) HandleUpstreamModelNotFound(ctx context.Context, acco switch { case isUpstreamModelNotFoundError(statusCode, responseBody): cooldown, reason = upstreamModelNotFoundCooldown, upstreamModelNotFoundReason + case statusCode == http.StatusUnauthorized && account.Type == AccountTypeAPIKey && account.IsOpenAICompatible() && isOpenAICompatibleModelNotFoundBody(responseBody): + cooldown, reason = upstreamModelNotFoundCooldown, upstreamModelNotFound401Reason case isOpenAIOAuthAccount(account) && isOpenAICodexPlanGatedModelError(statusCode, responseBody): cooldown, reason = upstreamCodexPlanGatedModelCooldown, upstreamCodexPlanGatedModelReason default: diff --git a/backend/internal/service/ratelimit_service_401_test.go b/backend/internal/service/ratelimit_service_401_test.go index c1f9f06954..8804cab702 100644 --- a/backend/internal/service/ratelimit_service_401_test.go +++ b/backend/internal/service/ratelimit_service_401_test.go @@ -236,6 +236,29 @@ func TestRateLimitService_HandleUpstreamError_NonOAuth401(t *testing.T) { require.Empty(t, invalidator.accounts) } +func TestRateLimitService_HandleUpstreamError_NonOAuthModel401StillDisablesCredentials(t *testing.T) { + repo := &rateLimitAccountRepoStub{} + service := NewRateLimitService(repo, nil, &config.Config{}, nil, nil) + account := &Account{ + ID: 104, + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + } + + shouldDisable := service.HandleUpstreamError( + context.Background(), + account, + http.StatusUnauthorized, + http.Header{}, + []byte(`{"error":{"message":"invalid or expired credentials"}}`), + "definitely-not-real", + ) + + require.True(t, shouldDisable) + require.Equal(t, 1, repo.setErrorCalls) + require.Equal(t, 0, repo.tempCalls) +} + // TestRateLimitService_HandleUpstreamError_OAuth401DoesNotOverwriteCredentials // 回归测试:确保 401 handler 不再使用请求开始时的 account 快照写回 credentials。 // 原实现会通过 persistAccountCredentials → UpdateCredentials → SetCredentials diff --git a/backend/internal/service/ratelimit_service_model_not_found_test.go b/backend/internal/service/ratelimit_service_model_not_found_test.go index 5e2fa0f71d..9e2baa3b09 100644 --- a/backend/internal/service/ratelimit_service_model_not_found_test.go +++ b/backend/internal/service/ratelimit_service_model_not_found_test.go @@ -69,6 +69,29 @@ func TestRateLimitService_HandleUpstreamError_ModelNotFoundUsesModelRateLimit(t require.WithinDuration(t, time.Now().Add(upstreamModelNotFoundCooldown), call.resetAt, 5*time.Second) } +func TestRateLimitService_HandleUpstreamError_APIKeyModel401UsesModelRateLimit(t *testing.T) { + repo := &modelNotFoundAccountRepoStub{} + svc := &RateLimitService{accountRepo: repo} + account := openAIModelNotFoundTempAccount() + + handled := svc.HandleUpstreamError( + context.Background(), + account, + http.StatusUnauthorized, + http.Header{}, + []byte(`{"error":{"message":"unknown model definitely-not-real"}}`), + "definitely-not-real", + ) + + require.True(t, handled) + require.Zero(t, repo.tempCalls) + require.Len(t, repo.modelRateLimitCalls, 1) + call := repo.modelRateLimitCalls[0] + require.Equal(t, account.ID, call.accountID) + require.Equal(t, "definitely-not-real", call.scope) + require.Equal(t, upstreamModelNotFound401Reason, call.reason) +} + func TestRateLimitService_HandleUpstreamError_ModelNotFoundWriteFailureDoesNotTempUnschedule(t *testing.T) { repo := &modelNotFoundAccountRepoStub{modelRateLimitErr: errors.New("write failed")} svc := &RateLimitService{accountRepo: repo}