fix(ratelimit): 图片模型被 Codex 文本端点拒绝时不再写模型冷却

gpt-image-* 被误发到 /v1/chat/completions 或 /v1/responses 时,Codex 会稳定
返回 400 codex_plan_gated_model。HandleUpstreamModelNotFound 把它当成账号能力
缺失,给每个账号写 30 分钟 model_rate_limits 冷却。failover 走完整个号池后,
池内每个账号都冷却了该图片模型,于是后续走对端点的 /v1/images/generations
请求也一并 503 no available accounts。

在 codex plan-gated 分支里对图片模型提前返回:仍然 failover 掉本次尝试,
但跳过 SetModelRateLimit 写入,不把号池对正确的生图请求整体下线。

Fixes #4828
This commit is contained in:
li
2026-08-07 20:41:34 +08:00
parent b8c0c00091
commit b5d9fd21b0
2 changed files with 51 additions and 0 deletions
@@ -2047,6 +2047,14 @@ func (s *RateLimitService) HandleUpstreamModelNotFound(ctx context.Context, acco
case isUpstreamModelNotFoundError(statusCode, responseBody):
cooldown, reason = upstreamModelNotFoundCooldown, upstreamModelNotFoundReason
case isOpenAIOAuthAccount(account) && isOpenAICodexPlanGatedModelError(statusCode, responseBody):
// Image models rejected by the Codex text endpoints are a deterministic
// endpoint mismatch, not an account capability gap: the same account still
// serves them over /v1/images/*. Cooling down the model here would take the
// whole pool offline for correct image requests, so fail the attempt over
// without recording a per-model cooldown.
if IsGPTImageGenerationModel(requestedModel) {
return true
}
cooldown, reason = upstreamCodexPlanGatedModelCooldown, upstreamCodexPlanGatedModelReason
default:
return false
@@ -382,6 +382,49 @@ func TestRateLimitService_HandleUpstreamError_CodexPlanGatedModelIgnoresAPIKeyAc
require.Empty(t, repo.modelRateLimitCalls)
}
func TestRateLimitService_HandleUpstreamError_CodexPlanGatedImageModelSkipsCooldown(t *testing.T) {
for _, model := range []string{"gpt-image-1", "gpt-image-1.5", "gpt-image-2"} {
t.Run(model, func(t *testing.T) {
repo := &modelNotFoundAccountRepoStub{}
svc := &RateLimitService{accountRepo: repo}
account := openAICodexPlanGatedOAuthAccount()
handled := svc.HandleUpstreamError(
context.Background(),
account,
http.StatusBadRequest,
http.Header{},
[]byte(`{"detail":"The '`+model+`' model is not supported when using Codex with a ChatGPT account."}`),
model,
)
require.True(t, handled, "attempt should still fail over")
require.Empty(t, repo.modelRateLimitCalls,
"image models must not be cooled down: the account still serves them over /v1/images/*")
require.Zero(t, repo.tempCalls)
})
}
}
func TestRateLimitService_HandleUpstreamError_CodexPlanGatedTextModelStillCoolsDown(t *testing.T) {
repo := &modelNotFoundAccountRepoStub{}
svc := &RateLimitService{accountRepo: repo}
account := openAICodexPlanGatedOAuthAccount()
handled := svc.HandleUpstreamError(
context.Background(),
account,
http.StatusBadRequest,
http.Header{},
[]byte(`{"detail":"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account."}`),
"gpt-5.6-sol",
)
require.True(t, handled)
require.Len(t, repo.modelRateLimitCalls, 1, "non-image plan-gated models keep the existing cooldown")
require.Equal(t, upstreamCodexPlanGatedModelReason, repo.modelRateLimitCalls[0].reason)
}
func openAICodexPlanGatedOAuthAccount() *Account {
return &Account{
ID: 202,