diff --git a/codex-rs/core/src/compact_model_fallback.rs b/codex-rs/core/src/compact_model_fallback.rs index 10d1ce743a..bda0792ee3 100644 --- a/codex-rs/core/src/compact_model_fallback.rs +++ b/codex-rs/core/src/compact_model_fallback.rs @@ -2,8 +2,17 @@ use codex_analytics::CompactionImplementation; use codex_analytics::CompactionReason; use codex_otel::SessionTelemetry; use codex_protocol::error::CodexErr; +use http::StatusCode; use tracing::warn; +pub(crate) fn should_retry_with_current_model(error: &CodexErr) -> bool { + match error { + CodexErr::InvalidRequest(_) => true, + CodexErr::UnexpectedStatus(response) => response.status == StatusCode::NOT_FOUND, + _ => false, + } +} + pub(crate) fn record_model_fallback( session_telemetry: &SessionTelemetry, previous_model: &str, diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 4b1db93555..7626632f46 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -8,6 +8,7 @@ use crate::compact::build_compaction_initial_context; use crate::compact::compaction_status_from_result; use crate::compact::insert_initial_context_before_last_real_user_or_summary; use crate::compact_model_fallback::record_model_fallback; +use crate::compact_model_fallback::should_retry_with_current_model; use crate::context::world_state::WorldState; use crate::context_manager::ContextManager; use crate::hook_runtime::PostCompactHookOutcome; @@ -218,7 +219,7 @@ async fn run_remote_compact_task_inner_impl( let Some(fallback_step_context) = fallback_step_context else { return Err(error); }; - if !matches!(&error, CodexErr::InvalidRequest(_)) { + if !should_retry_with_current_model(&error) { return Err(error); } let fallback_turn_context = &fallback_step_context.turn; diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 1571602cc1..f70bfadee0 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -9,6 +9,7 @@ use crate::compact::CompactionAnalyticsDetails; use crate::compact::InitialContextInjection; use crate::compact::compaction_status_from_result; use crate::compact_model_fallback::record_model_fallback; +use crate::compact_model_fallback::should_retry_with_current_model; use crate::compact_remote::process_compacted_history; use crate::compact_remote::should_keep_compacted_history_item; use crate::hook_runtime::PostCompactHookOutcome; @@ -230,7 +231,7 @@ async fn run_remote_compact_task_inner_impl( let Some(fallback_step_context) = fallback_step_context else { return Err(error); }; - if !matches!(&error, CodexErr::InvalidRequest(_)) { + if !should_retry_with_current_model(&error) { return Err(error); } let fallback_turn_context = &fallback_step_context.turn; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 93cabd3383..c433a3dd3c 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -289,6 +289,17 @@ fn invalid_request_response(message: impl Into) -> wiremock::ResponseTem })) } +fn model_not_found_response(model: &str) -> wiremock::ResponseTemplate { + wiremock::ResponseTemplate::new(/*status*/ 404).set_body_json(json!({ + "error": { + "message": format!("Model not found {model}"), + "type": "invalid_request_error", + "param": "model", + "code": null, + } + })) +} + fn write_global_file( home: &TempDir, filename: &str, @@ -2293,7 +2304,7 @@ async fn pre_sampling_compact_runs_when_comp_hash_changes() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_rename() { +async fn pre_sampling_compact_falls_back_when_previous_model_is_not_found() { skip_if_no_network!(); let server = MockServer::start().await; @@ -2320,9 +2331,7 @@ async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_renam ev_assistant_message("m1", "before switch"), ev_completed_with_tokens("r1", /*total_tokens*/ 100), ])), - invalid_request_response(format!( - "The '{retired_model}' model is not supported when using Codex with a ChatGPT account." - )), + model_not_found_response(retired_model), sse_response(sse(vec![ json!({ "type": "response.output_item.done", @@ -2384,7 +2393,9 @@ async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_renam }) .await; - let model_provider = openai_model_provider(&server); + let mut model_provider = openai_model_provider(&server); + model_provider.request_max_retries = Some(0); + model_provider.stream_max_retries = Some(0); let mut resumed_builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_model(retired_model)