404 "Model not found gpt-5.2" causes WebSocket fallback + reconnect loop in Codex CLI

Open 💬 9 comments Opened May 12, 2026 by CodingHxx

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

View original on GitHub ↗

6 Comments

Jeff-Kazzee · 2 months ago

Hi — I'd like to take this. The fix looks like it belongs in the WebSocket → HTTPS fallback path: detect 404 Model not found specifically, 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.

CodingHxx · 2 months ago

Thanks for jumping on this.

Your proposed approach makes sense to me especially treating 404 Model not found as 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.

Jeff-Kazzee · 2 months ago

I built this fix on a fork — branch fix/unexpected-status-4xx-non-retryable at https://github.com/Jeff-Kazzee/codex/tree/fix/unexpected-status-4xx-non-retryable (commit e14ff2cf). It's the is_retryable() change you'd expect: UnexpectedStatus retries only on 5xx; all unclassified 4xx (including 404 model-not-found) bail. 3 new tests in codex-rs/protocol/src/error_tests.rs, cargo test -p codex-protocol --lib passes 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.

adityasingh2400 · 2 months ago

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 raw POST /repos/openai/codex/pulls API) 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 treats CodexErr::UnexpectedStatus(_) as unconditionally retryable. It delegates to a new UnexpectedResponseError::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.
  • Same file: when the body matches Model not found <name>, the Display impl now surfaces a single message: "Model <name> not found. Check the configured model name in your Codex config." — instead of the raw unexpected status 404 Not Found: Model not found gpt-5.2 repeated through the reconnect loop.
  • Adds 5 unit tests in codex-rs/protocol/src/error_tests.rs covering 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 produce UnexpectedStatus). 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 404 for each attempt.
After: single "Model gpt-5.2 not 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.rs and codex-rs/protocol/src/error_tests.rs.

adityasingh2400 · 2 months ago

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 consults error.type from 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 deterministic model_not_found is 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_internal now compares config.model against the cached /models catalog before any transport is opened, returning CodexErr::InvalidRequest with 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 like gpt-5.2 never reaches a WebSocket handshake.

3. Friendly-message robustness. model_not_found_hint now also triggers on error.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_fallback integration 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 in codex-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 warnings clean.

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, and codex-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.

adityasingh2400 · 2 months ago

Pushed 9ebc5f0 on the same branch covering all four points.

(1) Coverage gap. Three changes:

  • ft: fine-tune IDs now bypass the catalog check entirely. /v1/models does not enumerate fine-tunes for most callers, so checking against the catalog is a false negative.
  • New skip_model_check config flag (boolean, default false). When true, the gate is disabled for the session. Covers org-restricted snapshots and beta-access SKUs the endpoint never lists.
  • The rejection message now names skip_model_check, so anyone who hits a false positive has an immediate workaround.

(2) Escape hatch. The skip_model_check flag above, set in ~/.codex/config.toml:

skip_model_check = true

(3) Refresh-on-miss. Implemented. When the cached catalog reports a miss and the session's refresh strategy is not Offline, spawn_internal forces one Online fetch and re-validates before returning InvalidRequest. 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-1122 derives the delay from util::backoff(retries), which is purely exponential plus jitter. The only delay-override path is CodexErr::Stream(_, requested_delay). So with the new envelope-based classification, a rate_limit_error gets the same backoff curve as server_error, ignoring any Retry-After header. Worth fixing, but it lives in util::backoff and 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::tests covering the ft: bypass, the skip_model_check-mentioning rejection message, and the unchanged pass/reject paths. cargo test -p codex-core --lib session::tests::validate_configured_model passes 6/6. Clippy clean on codex-core and codex-config.

Falsifiable check still holds: a valid configured model goes straight through the gate to the existing transport flow.

Showing cached comments. Read the full discussion on GitHub ↗