Model switch is blocked when previous-model compaction hits HTTP 429

Open 💬 1 comment Opened Jul 11, 2026 by Wibias

What version of the Codex App are you using (From “About Codex” dialog)?

26.707.31428

What subscription do you have?

Pro

What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What issue are you seeing?

Switching an existing Codex thread from a rate-limited model to another available model fails when the thread requires compaction before the next turn can run.

In my case, the thread previously used:

anthropic/claude-fable-5

That model's usage quota was exhausted, so I switched the thread to:

gpt-5.6-sol

Codex confirmed the model change:

Modellwechsel von Benutzerdefiniert zu GPT-5.6 Sol.

The session log also shows that the new model was applied:

thread_settings_applied:
  model: gpt-5.6-sol
  model_provider_id: openai

However, when I sent the next message, Codex failed before the newly selected model handled it:

Error running remote compact task: exceeded retry limit, last status: 429 Too Many Requests

The compact request was still sent to the previous model:

{
  "model": "claude-fable-5",
  "provider": "anthropic",
  "requestedModel": "anthropic/claude-fable-5",
  "status": 429,
  "errorCode": "rate_limit_exceeded"
}

The upstream provider returned:

This request would exceed your account's rate limit. Please try again later.

It appears that Codex attempts to compact the existing history using the previous model before continuing with the newly selected model.

Codex already has a fallback that retries compaction with the current model when the previous model returns CodexErr::InvalidRequest. However, when an HTTP 429 retry budget is exhausted, the failure is surfaced as CodexErr::RetryLimit, so the current-model fallback is not used.

As a result, the previous provider remains a hard dependency even after the user explicitly switches to another available model. The thread cannot continue until the previous provider's quota resets.

What steps can reproduce the bug?

  1. Create or open a Codex thread using model A, for example:

``text
anthropic/claude-fable-5
``

  1. Continue using the thread until its history is large enough that switching to a model with a smaller context window requires compaction.
  1. Exhaust model A's account quota, or otherwise make its provider consistently return HTTP 429.
  1. Switch the existing thread to an available model B from another provider, for example:

``text
gpt-5.6-sol
``

  1. Confirm that Codex reports the model switch and applies model B to the thread settings.
  1. Send a new message in the same thread.
  1. Codex starts a remote compaction task using model A instead of model B.
  1. The previous provider returns HTTP 429 until the retry limit is exceeded.
  1. The entire turn fails before model B receives the new user message.

Observed error:

Error running remote compact task: exceeded retry limit, last status: 429 Too Many Requests

Retrying the message produces the same result because Codex continues trying to compact with the unavailable previous model.

What is the expected behavior?

When Codex switches an existing thread to a new model and compaction with the previous model fails because that model is unavailable due to rate or quota limits, Codex should retry compaction using the currently selected model.

Expected flow:

Attempt compaction with previous model
    ↓
Previous model returns HTTP 429 and retries are exhausted
    ↓
Retry compaction with the currently selected model
    ↓
Continue the turn using the currently selected model

This should use the same fallback mechanism that already exists for CodexErr::InvalidRequest.

The fallback should only apply when:

  • Codex is compacting during a model transition.
  • A current-model fallback_step_context exists.
  • The previous model failed because it is unavailable, rate-limited, quota-limited, retired, or unsupported.

Normal requests that receive HTTP 429 should not silently switch models.

If compaction with the current model also fails, Codex should return that failure normally.

Additional information

The model switch itself appears to be applied correctly. The session records gpt-5.6-sol as the active thread model and uses its context-window configuration. However, the subsequent remote compaction request is still sent using the previous model, anthropic/claude-fable-5.

The failing request is:

Model: claude-fable-5
Provider: anthropic
Requested model: anthropic/claude-fable-5
Status: 429
Error: exceeded retry limit, last status: 429 Too Many Requests

The upstream response was:

{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "This request would exceed your account's rate limit. Please try again later."
  }
}

The likely source of the issue is the fallback condition in both remote-compaction implementations:

codex-rs/core/src/compact_remote.rs
codex-rs/core/src/compact_remote_v2.rs

The current logic appears to retry with the selected model only for CodexErr::InvalidRequest:

if !matches!(&error, CodexErr::InvalidRequest(_)) {
    return Err(error);
}

The reproduced failure is instead surfaced as:

exceeded retry limit, last status: 429 Too Many Requests

That appears to correspond to:

CodexErr::RetryLimit(RetryLimitReachedError {
    status: StatusCode::TOO_MANY_REQUESTS,
    ...
})

A possible narrowly scoped fix would be to allow fallback when the previous-model compact request exhausts its retries with HTTP 429:

fn should_fallback_to_current_model(error: &CodexErr) -> bool {
    matches!(error, CodexErr::InvalidRequest(_))
        || matches!(
            error,
            CodexErr::RetryLimit(inner)
                if inner.status == StatusCode::TOO_MANY_REQUESTS
        )
}

Both remote-compaction paths could then replace:

if !matches!(&error, CodexErr::InvalidRequest(_)) {
    return Err(error);
}

with:

if !should_fallback_to_current_model(&error) {
    return Err(error);
}

Because this fallback would still only run when a current-model fallback_step_context exists, ordinary requests receiving HTTP 429 would not silently switch models.

Suggested regression coverage:

InvalidRequest                  -> fallback
RetryLimit with HTTP 429        -> fallback
RetryLimit with HTTP 503        -> no fallback
No fallback_step_context        -> preserve original error
Current-model fallback fails    -> return fallback error normally

Additional information

The model switch itself appears to be applied correctly. The session records gpt-5.6-sol as the active thread model and uses its context-window configuration. However, the subsequent remote compaction request is still sent using the previous model, anthropic/claude-fable-5.

The failing request is:

Model: claude-fable-5
Provider: anthropic
Requested model: anthropic/claude-fable-5
Status: 429
Error: exceeded retry limit, last status: 429 Too Many Requests

The upstream response was:

{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "This request would exceed your account's rate limit. Please try again later."
  }
}

This suggests that Codex intentionally attempts compaction with the previous model during a model transition. The existing fallback to the newly selected model appears to be limited to CodexErr::InvalidRequest.

In this case, the exhausted HTTP 429 retries are surfaced as CodexErr::RetryLimit, so the fallback is skipped and the whole turn fails before gpt-5.6-sol receives the user message.

The relevant condition appears to exist in both:

codex-rs/core/src/compact_remote.rs
codex-rs/core/src/compact_remote_v2.rs

Current behavior:

if !matches!(&error, CodexErr::InvalidRequest(_)) {
    return Err(error);
}

A possible narrowly scoped fix would be to also allow fallback when the previous-model compact request exhausts its retries with HTTP 429:

fn should_fallback_to_current_model(error: &CodexErr) -> bool {
    matches!(error, CodexErr::InvalidRequest(_))
        || matches!(
            error,
            CodexErr::RetryLimit(inner)
                if inner.status == StatusCode::TOO_MANY_REQUESTS
        )
}

This fallback would still only run when a current-model fallback_step_context exists, so ordinary model requests receiving HTTP 429 would not silently switch models.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗