mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user