Files
sub2api/backend/internal/service/model_not_found_error.go
T
Bestony 5aeb03018c fix(scheduler): cool down Codex plan-gated models per account
OpenAI OAuth (ChatGPT) accounts deterministically reject plan-gated
models with 400 "The 'X' model is not supported when using Codex with
a ChatGPT account". Account selection had no capability filtering for
this, so the scheduler kept picking the same account for the same model
forever; every attempt burned an upstream call and surfaced to clients
as a retryable 502, sustaining client retry storms.

Treat this 400 like upstream model-not-found: mark the (account, model)
pair via SetModelRateLimit (30min cooldown) so
IsSchedulableForModelWithContext skips the account for that model
during selection, and return true so the in-flight request fails over
to another account through the existing UpstreamFailoverError path.
2026-07-13 16:29:18 +08:00

69 lines
2.3 KiB
Go

package service
import (
"net/http"
"strings"
)
var upstreamModelNotFoundKeywords = []string{"model not found", "unknown model", "not found"}
func isUpstreamModelNotFoundError(statusCode int, body []byte) bool {
if statusCode != http.StatusNotFound {
return false
}
normalized := normalizeModelNotFoundBody(body)
if normalized == "" || !strings.Contains(normalized, "model") {
return false
}
return containsModelNotFoundKeyword(normalized)
}
func isModelNotFoundError(statusCode int, body []byte) bool {
return isUpstreamModelNotFoundError(statusCode, body) || statusCode == http.StatusNotFound
}
// openAICodexPlanGatedModelPhrase matches the deterministic Codex 400 returned
// when a ChatGPT OAuth account's plan cannot serve the requested model, e.g.
// {"detail":"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account."}
// The phrase is compared against the normalized body (lowercased, "_"/"-"
// folded to spaces), so it also matches the same message embedded in
// error.message-style payloads.
const openAICodexPlanGatedModelPhrase = "model is not supported when using codex"
// isOpenAICodexPlanGatedModelError reports whether the upstream response is the
// deterministic Codex rejection of a plan-gated model on a ChatGPT account.
// Unlike transient failures, retrying the same account cannot succeed until the
// account's plan changes, so callers should treat it like model-not-found and
// cool the (account, model) pair down instead of re-selecting the account.
func isOpenAICodexPlanGatedModelError(statusCode int, body []byte) bool {
if statusCode != http.StatusBadRequest {
return false
}
normalized := normalizeModelNotFoundBody(body)
if normalized == "" {
return false
}
return strings.Contains(normalized, openAICodexPlanGatedModelPhrase)
}
func containsModelNotFoundKeyword(normalizedBody string) bool {
if normalizedBody == "" {
return false
}
for _, keyword := range upstreamModelNotFoundKeywords {
if strings.Contains(normalizedBody, keyword) {
return true
}
}
return false
}
func normalizeModelNotFoundBody(body []byte) string {
if len(body) == 0 {
return ""
}
normalized := strings.ToLower(string(body))
normalized = strings.NewReplacer("_", " ", "-", " ", "\n", " ", "\r", " ", "\t", " ").Replace(normalized)
return strings.Join(strings.Fields(normalized), " ")
}