diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 69b58446..229a1964 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -31,9 +31,9 @@ pool: durationMinutes: 30 description: Gateway upstream failures, including recovered upstream error wrappers, should cool down longer. - statusCode: 503 - keywords: [capacity, overloaded, temporarily unavailable, temporary, upstream, recovered upstream error] + keywords: [capacity, overloaded, temporarily unavailable, temporary, upstream, recovered upstream error, model_not_found, no available channel for model] durationMinutes: 30 - description: Service unavailable responses should cool down longer than one-off transient failures. + description: Service unavailable and upstream model-routing failures should cool down longer than one-off transient failures. - statusCode: 529 keywords: [capacity, overloaded, temporarily unavailable, temporary, recovered upstream error] durationMinutes: 30 diff --git a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts index fd0faaaa..34bbc691 100644 --- a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts +++ b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts @@ -50,12 +50,17 @@ if (parsed.pool?.defaultTempUnschedulable?.enabled === true) { assertCondition(gateway502Keywords.has("recovered upstream error"), "502 temporary-unschedulable rule must catch recovered upstream error wrappers", gateway502Rule); const accountState403Rule = rules.find((rule) => rule.statusCode === 403); const quota429Rule = rules.find((rule) => rule.statusCode === 429); + const serviceUnavailable503Rule = rules.find((rule) => rule.statusCode === 503); const accountState403Keywords = new Set((accountState403Rule?.keywords ?? []).map((keyword) => keyword.toLowerCase())); const quota429Keywords = new Set((quota429Rule?.keywords ?? []).map((keyword) => keyword.toLowerCase())); + const serviceUnavailable503Keywords = new Set((serviceUnavailable503Rule?.keywords ?? []).map((keyword) => keyword.toLowerCase())); for (const keyword of ["weekly limit", "less than 10% of your weekly limit left", "run /status for a breakdown"]) { assertCondition(accountState403Keywords.has(keyword), "403 temporary-unschedulable rule must catch Codex weekly-limit account-state prompts", { keyword, accountState403Rule }); assertCondition(quota429Keywords.has(keyword), "429 temporary-unschedulable rule must catch Codex weekly-limit quota prompts", { keyword, quota429Rule }); } + for (const keyword of ["model_not_found", "no available channel for model"]) { + assertCondition(serviceUnavailable503Keywords.has(keyword), "503 temporary-unschedulable rule must catch upstream model-routing failures", { keyword, serviceUnavailable503Rule }); + } } assertCondition(typeof parsed.localCodex?.responsesSmokeModel === "string" && parsed.localCodex.responsesSmokeModel.length > 0, "localCodex.responsesSmokeModel must be declared for Responses smoke validation", parsed.localCodex); @@ -69,6 +74,7 @@ console.log(JSON.stringify({ "temporary unschedulable rules are structurally valid when enabled", "generic recovered upstream error wrappers are caught by cooldown rules", "Codex weekly-limit prompts are caught by account-state and quota cooldown rules", + "upstream model-routing failures are caught by the 503 cooldown rule", "Responses smoke model is YAML-declared", ], })); diff --git a/scripts/platform-infra-sub2api-codex-temp-unsched-contract-test.ts b/scripts/platform-infra-sub2api-codex-temp-unsched-contract-test.ts index a937d755..a1e2da29 100644 --- a/scripts/platform-infra-sub2api-codex-temp-unsched-contract-test.ts +++ b/scripts/platform-infra-sub2api-codex-temp-unsched-contract-test.ts @@ -29,10 +29,14 @@ assertCondition(!("pool_mode" in credentials), "pool_mode must not be enabled be assertCondition(!("api_key" in credentials) && !("base_url" in credentials), "temporary-unschedulable rendering must not include secrets or endpoints", credentials); const accountState403Rule = rules.find((rule) => rule.error_code === 403); const quota429Rule = rules.find((rule) => rule.error_code === 429); +const serviceUnavailable503Rule = rules.find((rule) => rule.error_code === 503); for (const keyword of ["weekly limit", "less than 10% of your weekly limit left", "run /status for a breakdown"]) { assertCondition(accountState403Rule?.keywords?.includes(keyword), "403 rendered rule must preserve Codex weekly-limit account-state keyword", { keyword, accountState403Rule }); assertCondition(quota429Rule?.keywords?.includes(keyword), "429 rendered rule must preserve Codex weekly-limit quota keyword", { keyword, quota429Rule }); } +for (const keyword of ["model_not_found", "no available channel for model"]) { + assertCondition(serviceUnavailable503Rule?.keywords?.includes(keyword), "503 rendered rule must catch upstream model-routing failures", { keyword, serviceUnavailable503Rule }); +} const disabled = renderSub2ApiTempUnschedulableCredentials({ enabled: false, rules: policy.rules }) as { temp_unschedulable_enabled?: boolean; @@ -48,6 +52,7 @@ console.log(JSON.stringify({ "temporary unschedulable policy renders to Sub2API credential field names", "temporary unschedulable rendering follows the input policy without hard-coded policy gates", "Codex weekly-limit prompt keywords render into 403 and 429 cooldown rules", + "upstream model-routing failures render into the 503 cooldown rule", "disabled policies clear runtime rules", ], })); diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index 1f1d0a8a..1d91cb0e 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -684,9 +684,9 @@ export function defaultCodexTempUnschedulablePolicy(): CodexTempUnschedulablePol }, { statusCode: 503, - keywords: ["capacity", "overloaded", "temporarily unavailable", "temporary", "upstream", "recovered upstream error"], + keywords: ["capacity", "overloaded", "temporarily unavailable", "temporary", "upstream", "recovered upstream error", "model_not_found", "no available channel for model"], durationMinutes: 30, - description: "Service unavailable responses should cool down longer than one-off transient failures.", + description: "Service unavailable and upstream model-routing failures should cool down longer than one-off transient failures.", }, { statusCode: 529,