404 "Model not found gpt-5.2" causes WebSocket fallback + reconnect loop in Codex CLI
What version of Codex CLI is running?
codex-cli 0.125.0
What subscription do you have?
ChatGPT Plus
Which model were you using?
gpt-5.2
What platform is your computer?
windows
What terminal emulator and version are you using (if applicable)?
VS Code terminal
What issue are you seeing?
Codex CLI fails with a 404 Not Found error for the gpt-5.2 model and enters a reconnect loop.
Observed behavior:
Falling back from WebSockets to HTTPS transport.
unexpected status 404 Not Found: Model not found gpt-5.2
Then:
Reconnecting... 2/5
Unexpected status 404 Not Found Model not found gpt-5.2
It appears the CLI either:
references an invalid/removed model alias, or
does not correctly resolve the configured model name.
The issue persists after fallback from WebSocket transport to HTTPS transport.
What steps can reproduce the bug?
What steps can reproduce the bug?
Launch Codex CLI
Configure/use model gpt-5.2
Start a normal coding session or request
CLI attempts WebSocket connection
Falls back to HTTPS
Receives:
404 Not Found: Model not found gpt-5.2
CLI enters reconnect loop
What is the expected behavior?
What is the expected behavior?
Expected behavior:
The configured model should resolve successfully, OR
CLI should return a clear validation error immediately if the model alias is unsupported.
Additionally:
reconnect retries should stop for permanent 404 model not found errors
error messaging should indicate supported/available model names
Additional information
Falling back from WebSockets to HTTPS transport.
unexpected status 404 Not Found: Model not found gpt-5.2
Reconnecting... 2/5
Unexpected status 404 Not Found Model not found gpt-5.2
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Hi — I'd like to take this. The fix looks like it belongs in the WebSocket → HTTPS fallback path: detect
404 Model not foundspecifically, bail out of the reconnect loop, and surface a clearer error pointing the user at their configured model name instead of treating it as a transient transport error.I'd aim for a minimal Rust change in the reconnect/error-classification code plus a unit test. Happy to defer if someone else is already on it or if you want a different shape — I'd prefer to confirm before opening a PR.
Thanks for jumping on this.
Your proposed approach makes sense to me especially treating
404 Model not foundas a non-retryable configuration/model-resolution error instead of a transient transport failure.The reconnect loop was the main confusing part from the user side, so surfacing the configured model name directly would definitely improve DX.
A minimal Rust-side fix + unit test sounds like the right scope to start with. Appreciate you taking a look.
I built this fix on a fork — branch
fix/unexpected-status-4xx-non-retryableat https://github.com/Jeff-Kazzee/codex/tree/fix/unexpected-status-4xx-non-retryable (commite14ff2cf). It's theis_retryable()change you'd expect:UnexpectedStatusretries only on 5xx; all unclassified 4xx (including 404 model-not-found) bail. 3 new tests incodex-rs/protocol/src/error_tests.rs,cargo test -p codex-protocol --libpasses 215/0.Opening a PR through the API gets rejected as a permissions error on this repo, so I can't push the diff up the normal way. Happy for anyone with merge rights to cherry-pick the commit if it's useful, or to send the patch through whatever channel works for you. Closing my claim here.
Built a fix on a fork — branch fix/client-no-retry-404-model-not-found at commit
5c832d1.Like @Jeff-Kazzee reported above,
gh pr create(and the rawPOST /repos/openai/codex/pullsAPI) get rejected with a permissions error, so I can't open a normal PR. Happy for anyone with merge rights to cherry-pick the commit if it's useful.Change summary:
codex-rs/protocol/src/error.rs:CodexErr::is_retryable()no longer treatsCodexErr::UnexpectedStatus(_)as unconditionally retryable. It delegates to a newUnexpectedResponseError::is_retryable()that classifies 5xx as retryable, retains 408 / 425 / 429 as retryable transient throttling, and classifies all other 4xx (including the 404 in this issue) as permanent.Model not found <name>, theDisplayimpl now surfaces a single message: "Model<name>not found. Check the configured model name in your Codex config." — instead of the rawunexpected status 404 Not Found: Model not found gpt-5.2repeated through the reconnect loop.codex-rs/protocol/src/error_tests.rscovering non-retryable 4xx, retryable transient 4xx (408/425/429), retryable 5xx, the model-not-found classification, and the friendly message contents.Test outcome:
cargo test -p codex-protocol-> 216 passed (5 new + 211 existing).cargo test -p codex-core --test all websocket_fallback-> 4 passed (existing fallback / retry suite still green; that suite exercises a transport-level path that does not produceUnexpectedStatus).cargo clippy -p codex-protocol --tests -- -D warnings-> clean.Before: WebSocket fallback warning + reconnect loop "Reconnecting... N/5" repeated until retry budget exhausted, with the raw
unexpected status 404for each attempt.After: single "Model
gpt-5.2not found. Check the configured model name in your Codex config." error, no retries, no reconnect loop.Diff: 144 insertions, 2 deletions across
codex-rs/protocol/src/error.rsandcodex-rs/protocol/src/error_tests.rs.Thanks @Ilya0527 — both points landed. Pushed a follow-up on the same fork branch fix/client-no-retry-404-model-not-found at commit
3294f8b.1. Envelope-based classification.
UnexpectedResponseError::is_retryable()now consultserror.typefrom the structured envelope first, falling back to HTTP-status classification only when the envelope is absent or carries an unrecognized type. Permanent types (invalid_request_error,authentication_error,permission_error,not_found_error,unprocessable_entity_error) short-circuit retries; transient types (rate_limit_error,server_error,api_error,overloaded_error) remain retryable. As you noted, that means a deterministicmodel_not_foundis treated the same whether it surfaced over WebSocket or HTTPS — the envelope tells us once, and we trust it.2. Pre-flight model validation.
session::spawn_internalnow comparesconfig.modelagainst the cached/modelscatalog before any transport is opened, returningCodexErr::InvalidRequestwith the same friendly "configured model" hint as the 404 path. Gated so it only fires when the user explicitly set a model AND the catalog is non-empty — offline sub-agents and failed catalog fetches defer to the transport layer rather than being blocked by a missing list. A typo likegpt-5.2never reaches a WebSocket handshake.3. Friendly-message robustness.
model_not_found_hintnow also triggers onerror.code == "model_not_found", not just the legacy "Model not found <name>" prose, so the message stays correct as the upstream envelope evolves.Also updated three
websocket_fallbackintegration tests that previously relied on wiremock's default 404 to exercise retry-then-fallback — they now mount a retryable 502, since 4xx is permanent under the new classification.Tests: 12 new (8 in
codex-protocol::error_tests, 4 incodex-core::session::tests).cargo test -p codex-protocol --lib→ 224 pass.cargo test -p codex-core --lib session::tests::validate_configured_model→ 4 pass.cargo test -p codex-core --test all websocket_fallback→ 4 pass.cargo clippy -p codex-protocol -p codex-core --tests -- -D warningsclean.Diff: 297 insertions, 7 deletions across
codex-rs/protocol/src/error.rs,codex-rs/protocol/src/error_tests.rs,codex-rs/core/src/session/mod.rs,codex-rs/core/src/session/tests.rs, andcodex-rs/core/tests/suite/websocket_fallback.rs. Falsifiable check still holds: with a valid model, neither code path fires and the transport flow is untouched.Pushed
9ebc5f0on the same branch covering all four points.(1) Coverage gap. Three changes:
ft:fine-tune IDs now bypass the catalog check entirely./v1/modelsdoes not enumerate fine-tunes for most callers, so checking against the catalog is a false negative.skip_model_checkconfig flag (boolean, defaultfalse). Whentrue, the gate is disabled for the session. Covers org-restricted snapshots and beta-access SKUs the endpoint never lists.skip_model_check, so anyone who hits a false positive has an immediate workaround.(2) Escape hatch. The
skip_model_checkflag above, set in~/.codex/config.toml:(3) Refresh-on-miss. Implemented. When the cached catalog reports a miss and the session's refresh strategy is not
Offline,spawn_internalforces oneOnlinefetch and re-validates before returningInvalidRequest. The happy path is untouched (single cached lookup); the refresh only fires when we are about to fail anyway, so it absorbs the "cache stale, new model dropped this morning" case at the cost of one extra request on the bad path.(4) Retry-After. Confirmed: not currently honored. The retry loop in
session/turn.rs:1117-1122derives the delay fromutil::backoff(retries), which is purely exponential plus jitter. The only delay-override path isCodexErr::Stream(_, requested_delay). So with the new envelope-based classification, arate_limit_errorgets the same backoff curve asserver_error, ignoring anyRetry-Afterheader. Worth fixing, but it lives inutil::backoffand the per-error retry path rather than the classification layer, so I'm leaving it as a follow-up rather than dragging it into this PR.Tests: 3 new in
session::testscovering the ft: bypass, theskip_model_check-mentioning rejection message, and the unchanged pass/reject paths.cargo test -p codex-core --lib session::tests::validate_configured_modelpasses 6/6. Clippy clean oncodex-coreandcodex-config.Falsifiable check still holds: a valid configured model goes straight through the gate to the existing transport flow.