Merge pull request #7139 from HP-network/fix/api-key-model-401-failover

fix: keep API-key accounts available for unknown models
This commit is contained in:
Wesley Liddick
2026-09-23 10:05:42 +08:00
committed by GitHub
5 changed files with 55 additions and 0 deletions
@@ -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},
@@ -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")
}
@@ -2468,6 +2468,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
@@ -2493,6 +2494,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:
@@ -246,6 +246,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
@@ -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}