Broaden compaction fallback to the current model (#46324)

## Why

After a model switch, compaction with the previous model could fail after exhausting stream retries without falling back to the selected model.

## What changed

Allow compaction to fall back to the current model for all errors except `TurnAborted`, `Interrupted`, and `SessionBudgetExceeded`.

## Testing

Add a regression test that exhausts the previous model's compaction stream retries, then verifies that fallback compaction and turn sampling use the selected model.

GitOrigin-RevId: 9c9b7ccbb206d19f1ae750a32636fc9acaacfb2b
This commit is contained in:
Ahmed Ibrahim
2026-09-17 23:45:01 +00:00
committed by copyberry
parent a1efb59c4a
commit 5492c2b06e
2 changed files with 104 additions and 9 deletions

View File

@@ -5,17 +5,13 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
use tracing::warn;
/// Retries failures that may be model-specific and succeed with a different model.
/// Returns whether a failed compaction attempt should use the current model.
pub(crate) fn should_retry_with_current_model(error: &CodexErr) -> bool {
matches!(
!matches!(
error.details(),
CodexErrorDetails::InvalidRequest(_)
| CodexErrorDetails::UnexpectedStatus(_)
| CodexErrorDetails::ContextWindowExceeded
| CodexErrorDetails::UsageLimitReached(_)
| CodexErrorDetails::ServerOverloaded
| CodexErrorDetails::InternalServerError
| CodexErrorDetails::RetryLimit(_)
CodexErrorDetails::TurnAborted
| CodexErrorDetails::Interrupted
| CodexErrorDetails::SessionBudgetExceeded
)
}

View File

@@ -2965,6 +2965,105 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_sampling_compact_falls_back_after_previous_model_stream_retries_are_exhausted() {
skip_if_no_network!();
let server = MockServer::start().await;
let previous_model = "gpt-5.4";
let selected_model = "gpt-5.2";
let _models_mock = mount_models_once(
&server,
ModelsResponse {
models: vec![
model_info_with_context_window(previous_model, /*context_window*/ 273_000),
model_info_with_context_window(selected_model, /*context_window*/ 125_000),
],
},
)
.await;
let compaction_failure = sse_failed(
"compact-failure",
"server_error",
"previous-model compaction failed",
);
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", "before switch"),
ev_completed_with_tokens("r1", /*total_tokens*/ 120_000),
]),
compaction_failure.clone(),
compaction_failure.clone(),
compaction_failure,
remote_v2_compaction_response(),
sse(vec![
ev_assistant_message("m3", "after switch"),
ev_completed_with_tokens("r3", /*total_tokens*/ 100),
]),
],
)
.await;
let mut model_provider = openai_model_provider(&server);
model_provider.stream_max_retries = Some(2);
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model(previous_model)
.with_config(move |config| {
config.model_provider = model_provider;
set_test_compact_prompt(config);
});
let test = builder.build(&server).await.expect("build test codex");
test.codex
.start_or_steer_turn(disabled_permission_user_turn(
"before switch",
test.cwd.path().to_path_buf(),
previous_model.to_string(),
))
.await
.expect("submit first user turn");
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
test.codex
.start_or_steer_turn(disabled_permission_user_turn(
"after switch",
test.cwd.path().to_path_buf(),
selected_model.to_string(),
))
.await
.expect("submit selected-model turn");
assert_compaction_uses_turn_lifecycle_id(&test.codex).await;
let actual_models = request_log
.requests()
.iter()
.map(|request| {
request.body_json()["model"]
.as_str()
.expect("request model")
.to_string()
})
.collect::<Vec<_>>();
assert_eq!(
actual_models,
vec![
previous_model, // Initial turn.
previous_model, // Compaction attempt.
previous_model, // First retry.
previous_model, // Second retry.
selected_model, // Fallback compaction.
selected_model, // Turn sampling.
]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_sampling_compact_keeps_unknown_previous_model_for_api_key_auth_and_custom_provider() {
skip_if_no_network!();