Remove retired model entries while preserving migration prompts (#44250)

## Why

Saved model selections can outlive their bundled catalog entries and still need migration guidance.

## What changed

- Remove `gpt-5.2` and `gpt-5.4-mini` from the bundled model catalog.
- Preserve migration from saved `gpt-5.4-mini` selections to `gpt-5.6-luna` when the old model is absent, while respecting catalog-provided migration metadata.
- Update tests and snapshots to use current models or explicit test fixtures, and remove the obsolete prompt fixture.

## Testing

Extend migration tests to cover catalog metadata precedence, saved selections, replacement model and reasoning-effort persistence, acknowledged prompts, and missing or hidden migration targets.

GitOrigin-RevId: 82beea504cc4a15699677c28a61ec0939af3069c
This commit is contained in:
rhan-oai
2026-09-09 17:56:33 +00:00
committed by copyberry
parent 6eecd04fc1
commit eb7bd64ef9
58 changed files with 670 additions and 1192 deletions
+9 -9
View File
@@ -1449,17 +1449,17 @@ mod tests {
(ReasoningEffortConfig::Ultra, ReasoningEffortConfig::Medium),
] {
let mut app = make_test_app().await;
app.config.model = Some("gpt-5.4".to_string());
app.config.model = Some("gpt-5.5".to_string());
app.config.model_reasoning_effort = Some(configured_effort.clone());
app.chat_widget
.set_reasoning_effort(Some(configured_effort));
let default_effort =
app.on_apply_advanced_reasoning("gpt-5.4", ReasoningEffortConfig::Ultra);
app.on_apply_advanced_reasoning("gpt-5.5", ReasoningEffortConfig::Ultra);
let new_thread_config = app.fresh_session_config();
assert_eq!(default_effort, Some(expected_default_effort.clone()));
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
assert_eq!(
app.chat_widget.current_reasoning_effort(),
Some(ReasoningEffortConfig::Ultra)
@@ -1469,7 +1469,7 @@ mod tests {
new_thread_config.model.as_deref(),
new_thread_config.model_reasoning_effort,
),
(Some("gpt-5.4"), Some(expected_default_effort))
(Some("gpt-5.5"), Some(expected_default_effort))
);
}
}
@@ -1477,15 +1477,15 @@ mod tests {
#[tokio::test]
async fn conversation_reasoning_keeps_previous_default_for_ultra_only_model() {
let mut app = make_test_app().await;
app.config.model = Some("gpt-5.4".to_string());
app.config.model = Some("gpt-5.5".to_string());
app.config.model_reasoning_effort = Some(ReasoningEffortConfig::Low);
let mut preset = app
.model_catalog
.try_list_models()
.expect("model catalog is infallible")
.into_iter()
.find(|preset| preset.model == "gpt-5.4")
.expect("gpt-5.4 preset");
.find(|preset| preset.model == "gpt-5.5")
.expect("gpt-5.5 preset");
preset.model = "ultra-only".to_string();
preset.default_reasoning_effort = ReasoningEffortConfig::Ultra;
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
@@ -1509,7 +1509,7 @@ mod tests {
new_thread_config.model.as_deref(),
new_thread_config.model_reasoning_effort,
),
(Some("gpt-5.4"), Some(ReasoningEffortConfig::Low))
(Some("gpt-5.5"), Some(ReasoningEffortConfig::Low))
);
}
@@ -1526,7 +1526,7 @@ mod tests {
.handle_key_event(KeyEvent::from(KeyCode::BackTab));
let default_effort =
app.on_apply_advanced_reasoning("gpt-5.4", ReasoningEffortConfig::Ultra);
app.on_apply_advanced_reasoning("gpt-5.5", ReasoningEffortConfig::Ultra);
assert_eq!(default_effort, Some(ReasoningEffortConfig::Low));
assert_eq!(
+29 -8
View File
@@ -110,6 +110,31 @@ pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &
)));
}
pub(super) fn model_upgrade_for_migration(
model: &str,
available_models: &[ModelPreset],
) -> Option<ModelUpgrade> {
if let Some(preset) = available_models.iter().find(|preset| preset.model == model) {
return preset.upgrade.clone();
}
// Saved selections can outlive their catalog entries. Keep only their migration metadata.
let (target_model, current_name, target_name) = match model {
"gpt-5.4-mini" => ("gpt-5.6-luna", "GPT-5.4 Mini", "GPT-5.6 Luna"),
_ => return None,
};
Some(ModelUpgrade {
id: target_model.to_string(),
migration_config_key: model.to_string(),
model_link: None,
upgrade_copy: None,
migration_markdown: Some(format!(
"{current_name} is no longer available\n\nCodex now uses {target_name} in place of {current_name}. Switch to {target_name} to continue.\n"
)),
retirement_at: None,
})
}
pub(super) fn should_show_model_migration_prompt(
current_model: &str,
target_model: &str,
@@ -133,9 +158,8 @@ pub(super) fn should_show_model_migration_prompt(
return false;
}
if available_models
.iter()
.any(|preset| preset.model == current_model && preset.upgrade.is_some())
if model_upgrade_for_migration(current_model, available_models)
.is_some_and(|upgrade| upgrade.id == target_model)
{
return true;
}
@@ -274,10 +298,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
app_event_tx: &AppEventSender,
available_models: &[ModelPreset],
) -> std::io::Result<Option<AppExitInfo>> {
let upgrade = available_models
.iter()
.find(|preset| preset.model == model)
.and_then(|preset| preset.upgrade.as_ref());
let upgrade = model_upgrade_for_migration(model, available_models);
if let Some(ModelUpgrade {
id: target_model,
@@ -286,7 +307,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
upgrade_copy,
migration_markdown,
..
}) = upgrade
}) = upgrade.as_ref()
{
if migration_prompt_hidden(local_settings, migration_config_key.as_str()) {
return Ok(None);
+10 -10
View File
@@ -8796,8 +8796,8 @@ async fn selecting_cyber_model_defaults_active_thread_to_auto_review() {
.try_list_models()
.expect("model catalog")
.into_iter()
.find(|model| model.model == "gpt-5.4")
.expect("gpt-5.4 model");
.find(|model| model.model == "gpt-5.5")
.expect("gpt-5.5 model");
model.model_specialty = Some(MODEL_SPECIALTY_CYBER.to_string());
app.model_catalog = Arc::new(ModelCatalog::new(vec![model]));
@@ -8834,7 +8834,7 @@ async fn selecting_cyber_model_defaults_active_thread_to_auto_review() {
&mut tui,
&mut app_server,
AppEvent::ApplyAdvancedReasoning {
model: "gpt-5.4".to_string(),
model: "gpt-5.5".to_string(),
effort: ReasoningEffortConfig::High,
},
)
@@ -8845,7 +8845,7 @@ async fn selecting_cyber_model_defaults_active_thread_to_auto_review() {
app.handle_event(
&mut tui,
&mut app_server,
AppEvent::UpdateModel("gpt-5.4".to_string()),
AppEvent::UpdateModel("gpt-5.5".to_string()),
)
.await
.expect("model selection should succeed");
@@ -8993,8 +8993,8 @@ async fn selecting_cyber_model_falls_back_to_user_when_auto_review_is_unavailabl
.try_list_models()
.expect("model catalog")
.into_iter()
.find(|model| model.model == "gpt-5.4")
.expect("gpt-5.4 model");
.find(|model| model.model == "gpt-5.5")
.expect("gpt-5.5 model");
model.model_specialty = Some(MODEL_SPECIALTY_CYBER.to_string());
app.model_catalog = Arc::new(ModelCatalog::new(vec![model]));
let _ = app.config.features.disable(Feature::GuardianApproval);
@@ -9003,7 +9003,7 @@ async fn selecting_cyber_model_falls_back_to_user_when_auto_review_is_unavailabl
app.active_thread_id = Some(ThreadId::new());
let params = app
.active_thread_model_setting_update_params("gpt-5.4".to_string())
.active_thread_model_setting_update_params("gpt-5.5".to_string())
.expect("active thread should produce update params");
assert_eq!(
@@ -9048,8 +9048,8 @@ async fn selecting_cyber_model_respects_auto_review_requirements() {
.try_list_models()
.expect("model catalog")
.into_iter()
.find(|model| model.model == "gpt-5.4")
.expect("gpt-5.4 model");
.find(|model| model.model == "gpt-5.5")
.expect("gpt-5.5 model");
model.model_specialty = Some(MODEL_SPECIALTY_CYBER.to_string());
app.model_catalog = Arc::new(ModelCatalog::new(vec![model]));
@@ -9074,7 +9074,7 @@ async fn selecting_cyber_model_respects_auto_review_requirements() {
app.handle_event(
&mut tui,
&mut app_server,
AppEvent::UpdateModel("gpt-5.4".to_string()),
AppEvent::UpdateModel("gpt-5.5".to_string()),
)
.await
.expect("model selection should succeed");
@@ -1,6 +1,7 @@
use super::*;
use crate::chatwidget::UserMessage;
use crate::chatwidget::tests::helpers::normalize_snapshot_paths;
use crate::chatwidget::tests::helpers::set_fast_mode_test_catalog_for_models;
use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::ThreadSettingsUpdateParams;
use pretty_assertions::assert_eq;
@@ -10,9 +11,9 @@ pub(super) fn fallback_response() -> GetAccountRateLimitsResponse {
serde_json::from_value(json!({
"accountId": "workspace-a", "rateLimits": {},
"rateLimitUpsell": {
"banner_type": "model_recovery", "model_slug": "gpt-5.4",
"blocked_model_slug": "gpt-5.4",
"fallback_model_slugs": ["unavailable-model", "gpt-5.4", "gpt-5.2"],
"banner_type": "model_recovery", "model_slug": "gpt-5.5",
"blocked_model_slug": "gpt-5.5",
"fallback_model_slugs": ["unavailable-model", "gpt-5.5", "gpt-5.6-terra"],
"title": "Selected model usage exhausted",
"description": "Add credits to use this model again.",
"presentation": "dismissible",
@@ -24,9 +25,9 @@ pub(super) fn fallback_response() -> GetAccountRateLimitsResponse {
fn configure_fallback_model(app: &mut App) {
set_chatgpt_auth(&mut app.chat_widget);
set_fast_mode_test_catalog(&mut app.chat_widget);
set_fast_mode_test_catalog_for_models(&mut app.chat_widget, "gpt-5.5", "gpt-5.6-terra");
app.model_catalog = app.chat_widget.model_catalog();
app.chat_widget.set_model("gpt-5.4");
app.chat_widget.set_model("gpt-5.5");
}
pub(super) async fn start_fallback_thread(
@@ -65,7 +66,7 @@ async fn backend_banner_fallback_updates_task_settings_and_keeps_notice() -> Res
.set_service_tier(Some(ServiceTier::Fast.request_value().to_string()));
let mut expected_mode = app.chat_widget.effective_collaboration_mode();
assert_eq!(expected_mode.mode, mode_kind);
expected_mode.settings.model = "gpt-5.2".into();
expected_mode.settings.model = "gpt-5.6-terra".into();
expected_mode.settings.reasoning_effort = Some(ReasoningEffortConfig::Medium);
let default_model = app.config.model.clone();
let default_plan_effort = app.config.plan_mode_reasoning_effort.clone();
@@ -99,7 +100,7 @@ async fn backend_banner_fallback_updates_task_settings_and_keeps_notice() -> Res
serde_json::from_value::<ThreadSettingsUpdateParams>(sent[0].params.clone().unwrap())?,
ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
model: Some("gpt-5.2".into()),
model: Some("gpt-5.6-terra".into()),
effort: Some(ReasoningEffortConfig::Medium),
collaboration_mode: Some(expected_mode.clone()),
service_tier: Some(Some("default".into())),
@@ -146,7 +147,7 @@ async fn backend_banner_fallback_updates_task_settings_and_keeps_notice() -> Res
healthy.rate_limit_upsell = None;
app.chat_widget.update_backend_banner(&healthy);
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), "gpt-5.2");
assert_eq!(app.chat_widget.current_model(), "gpt-5.6-terra");
assert_eq!(requests.lock().unwrap().len(), 1);
assert!(
!std::iter::from_fn(|| events.try_recv().ok())
@@ -166,7 +167,7 @@ async fn backend_banner_fallback_updates_task_settings_and_keeps_notice() -> Res
app.chat_widget
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(next_user_turn_op(&mut ops), AppCommand::UserTurn { model, collaboration_mode: Some(mode), .. }
if model == "gpt-5.2" && mode == expected_mode);
if model == "gpt-5.6-terra" && mode == expected_mode);
server.shutdown().await?;
proxy.await??;
}
@@ -207,7 +208,7 @@ async fn backend_banner_fallback_uses_current_task_and_accepted_generation() ->
)
.await?;
if generation == read_generation {
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
assert!(requests.lock().unwrap().is_empty());
}
}
@@ -217,7 +218,7 @@ async fn backend_banner_fallback_uses_current_task_and_accepted_generation() ->
sent[0].params.as_ref().unwrap()["threadId"],
active_id.to_string()
);
assert_eq!(app.chat_widget.current_model(), "gpt-5.2");
assert_eq!(app.chat_widget.current_model(), "gpt-5.6-terra");
server.shutdown().await?;
proxy.await??;
Ok(())
@@ -231,13 +232,13 @@ async fn backend_banner_fallback_keeps_existing_recovery_without_candidate() ->
for (replacement, expect_legacy_banner) in [
(json!([]), true),
(json!(["unavailable-model"]), false),
(json!(["gpt-5.4"]), false),
(json!(["gpt-5.5"]), false),
] {
let mut response = fallback_response();
response.rate_limit_upsell.as_mut().unwrap()["fallback_model_slugs"] = replacement;
app.chat_widget.update_backend_banner(&response);
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 72);
assert_eq!(rendered.contains("View usage"), expect_legacy_banner);
if !expect_legacy_banner {
@@ -247,10 +248,10 @@ async fn backend_banner_fallback_keeps_existing_recovery_without_candidate() ->
);
}
}
app.chat_widget.set_model("gpt-5.2");
app.chat_widget.set_model("gpt-5.6-terra");
app.chat_widget.update_backend_banner(&fallback_response());
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), "gpt-5.2");
assert_eq!(app.chat_widget.current_model(), "gpt-5.6-terra");
assert!(
render_bottom_popup(&app.chat_widget, /*width*/ 72)
.contains("Selected model usage exhausted")
@@ -310,7 +311,7 @@ async fn backend_banner_fallback_handles_settings_failure_and_queued_manual_sele
} else {
if settings_requests == 1 {
// A manual selection arriving while the RPC is pending must win afterward.
manual_selection_tx.send(AppEvent::UpdateModel("gpt-5.4".into()));
manual_selection_tx.send(AppEvent::UpdateModel("gpt-5.5".into()));
}
json!({"id": request["id"], "result": {}})
}
@@ -369,7 +370,7 @@ async fn backend_banner_fallback_handles_settings_failure_and_queued_manual_sele
if use_reserve {
"gpt-reserve"
} else {
"gpt-5.2"
"gpt-5.6-terra"
}
);
let manual_selection = queued
@@ -380,7 +381,7 @@ async fn backend_banner_fallback_handles_settings_failure_and_queued_manual_sele
app.handle_event(&mut tui, &mut server, manual_selection)
.await?;
}
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 72).contains("View usage"));
if use_reserve && error_code.is_some() {
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 90);
@@ -447,7 +448,7 @@ async fn backend_banner_fallback_preserves_permissions_for_first_eligible_cyber_
let mut models = app.model_catalog.try_list_models()?;
let mut cyber_model = models
.iter()
.find(|model| model.model == "gpt-5.2")
.find(|model| model.model == "gpt-5.6-terra")
.unwrap()
.clone();
// Like the manual-selection tests, classify a fixture model through catalog metadata.
@@ -465,12 +466,16 @@ async fn backend_banner_fallback_preserves_permissions_for_first_eligible_cyber_
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
app.chat_widget
.handle_thread_session(app.primary_session_configured.clone().unwrap());
app.chat_widget.set_model("gpt-5.4");
app.chat_widget.set_model("gpt-5.5");
app.chat_widget
.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
let mut response = fallback_response();
response.rate_limit_upsell.as_mut().unwrap()["fallback_model_slugs"] =
json!(["unavailable-model", "gpt-5.4", cyber_model.model, "gpt-5.2"]);
response.rate_limit_upsell.as_mut().unwrap()["fallback_model_slugs"] = json!([
"unavailable-model",
"gpt-5.5",
cyber_model.model,
"gpt-5.6-terra"
]);
app.chat_widget.update_backend_banner(&response);
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), cyber_model.model);
@@ -1,13 +1,14 @@
use super::backend_banner_fallback_tests::fallback_response;
use super::*;
use crate::chatwidget::UserMessage;
use crate::chatwidget::tests::helpers::set_fast_mode_test_catalog_for_models;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn backend_banner_fallback_applies_before_initial_and_queued_startup_prompts() -> Result<()> {
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
let mut config = app.config.clone();
config.model = Some("gpt-5.4".into());
config.model = Some("gpt-5.5".into());
let mut tui = crate::tui::test_support::make_test_tui()?;
let init = app.chatwidget_init_for_forked_or_resumed_thread(
&mut tui,
@@ -16,10 +17,10 @@ async fn backend_banner_fallback_applies_before_initial_and_queued_startup_promp
);
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
set_chatgpt_auth(&mut app.chat_widget);
set_fast_mode_test_catalog(&mut app.chat_widget);
set_fast_mode_test_catalog_for_models(&mut app.chat_widget, "gpt-5.5", "gpt-5.6-terra");
app.model_catalog = app.chat_widget.model_catalog();
app.pending_startup_thread_start = true;
app.chat_widget.set_model("gpt-5.4");
app.chat_widget.set_model("gpt-5.5");
app.chat_widget
.set_queue_submissions_until_session_configured(/*queue*/ true);
app.chat_widget
@@ -68,7 +69,7 @@ async fn backend_banner_fallback_applies_before_initial_and_queued_startup_promp
assert_eq!(
submissions,
vec![(
"gpt-5.2".into(),
"gpt-5.6-terra".into(),
vec![UserInput::Text {
text: "initial prompt".into(),
text_elements: Vec::new(),
@@ -29,7 +29,7 @@ pub(super) fn configure_reserve_catalog(app: &mut App) {
let mut models = app.model_catalog.try_list_models().unwrap();
let mut reserve = models
.iter()
.find(|model| model.model == "gpt-5.4")
.find(|model| model.model == "gpt-5.5")
.unwrap()
.clone();
reserve.model = "gpt-reserve".into();
@@ -37,7 +37,7 @@ pub(super) fn configure_reserve_catalog(app: &mut App) {
models.push(reserve);
let original = models
.iter_mut()
.find(|model| model.model == "gpt-5.4")
.find(|model| model.model == "gpt-5.5")
.unwrap();
let mut high = original.supported_reasoning_efforts[0].clone();
high.effort = ReasoningEffortConfig::High;
@@ -54,7 +54,7 @@ pub(super) fn configure_reserve_catalog(app: &mut App) {
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
app.chat_widget
.handle_thread_session(app.primary_session_configured.clone().unwrap());
app.chat_widget.set_model("gpt-5.4");
app.chat_widget.set_model("gpt-5.5");
}
#[tokio::test]
@@ -87,7 +87,7 @@ async fn luna_reserve_recovery_survives_task_reconstruction() -> Result<()> {
recovered.rate_limit_upsell = None;
app.chat_widget.update_backend_banner(&recovered);
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
assert_eq!(
app.chat_widget.current_reasoning_effort(),
Some(ReasoningEffortConfig::High)
@@ -236,8 +236,8 @@ async fn luna_reserve_recovery_restores_task_and_pending_turn_after_fresh_backen
// A rounded percentage is not an authorization decision; backend allowed=true wins.
for (request_id, hard_stop_generation, expected) in [
(2, generation.wrapping_sub(1), "gpt-reserve"),
(3, generation, "gpt-5.4"),
(4, generation, "gpt-5.4"),
(3, generation, "gpt-5.5"),
(4, generation, "gpt-5.5"),
] {
app.handle_event(
&mut tui,
@@ -260,7 +260,7 @@ async fn luna_reserve_recovery_restores_task_and_pending_turn_after_fresh_backen
app.chat_widget
.apply_reserve_fallback_to_pending_turn(&mut pending);
assert_matches!(pending, AppCommand::UserTurn { model, effort, collaboration_mode, .. }
if model == "gpt-5.4" && effort == original_mode.reasoning_effort()
if model == "gpt-5.5" && effort == original_mode.reasoning_effort()
&& collaboration_mode == Some(original_mode.clone()));
let sent = requests.lock().unwrap().clone();
assert_eq!(sent.len(), 2);
@@ -270,7 +270,7 @@ async fn luna_reserve_recovery_restores_task_and_pending_turn_after_fresh_backen
restored,
ThreadSettingsUpdateParams {
thread_id: app.active_thread_id.unwrap().to_string(),
model: Some("gpt-5.4".into()),
model: Some("gpt-5.5".into()),
effort: Some(ReasoningEffortConfig::High),
collaboration_mode: Some(original_mode),
service_tier: None,
@@ -341,7 +341,7 @@ async fn luna_reserve_recovery_requires_permission_and_no_remaining_blocker() ->
app.chat_widget.clear_backend_banner();
app.chat_widget.update_backend_banner(&recovered);
app.apply_backend_banner_fallback(&mut server).await;
assert_eq!(app.chat_widget.current_model(), "gpt-5.4");
assert_eq!(app.chat_widget.current_model(), "gpt-5.5");
assert_eq!(requests.lock().unwrap().len(), 1);
server.shutdown().await?;
proxy.await??;
@@ -365,8 +365,8 @@ async fn luna_reserve_recovery_does_not_override_manual_choice_or_account_change
recovered.account_id = Some("workspace-b".into());
"gpt-reserve"
} else {
app.chat_widget.set_model("gpt-5.2");
"gpt-5.2"
app.chat_widget.set_model("gpt-5.6-terra");
"gpt-5.6-terra"
};
app.chat_widget.update_backend_banner(&recovered);
app.apply_backend_banner_fallback(&mut server).await;
+171 -116
View File
@@ -18,7 +18,7 @@ async fn model_picker_refresh_updates_app_catalog_from_app_server() -> Result<()
let fast = Some(ServiceTier::Fast.request_value());
let thread_id = ThreadId::new();
let mut session = test_thread_session(thread_id, test_path_buf("/tmp/project"));
session.model = "gpt-5.4".to_string();
session.model = "gpt-5.5".to_string();
app.primary_thread_id = Some(thread_id);
app.active_thread_id = Some(thread_id);
app.primary_session_configured = Some(session.clone());
@@ -72,7 +72,7 @@ async fn model_picker_refresh_updates_app_catalog_from_app_server() -> Result<()
};
assert!(!models.is_empty());
for model in models.iter_mut() {
model.is_default = model.model == "gpt-5.4";
model.is_default = model.model == "gpt-5.5";
model.default_service_tier = model
.is_default
.then(|| ServiceTier::Fast.request_value().to_string());
@@ -111,7 +111,7 @@ async fn model_picker_refresh_updates_app_catalog_from_app_server() -> Result<()
let mut config = app.config.clone();
config.service_tier = None;
config.features.enable(Feature::FastMode)?;
for (model, expected_tier) in [(None, fast), (Some("gpt-5.2"), Some("default"))] {
for (model, expected_tier) in [(None, fast), (Some("gpt-5.6-terra"), Some("default"))] {
config.model = model.map(str::to_string);
let started = app_server.start_thread(&config).await?;
assert_eq!(started.session.service_tier.as_deref(), expected_tier);
@@ -122,20 +122,23 @@ async fn model_picker_refresh_updates_app_catalog_from_app_server() -> Result<()
fn model_presets_with_test_upgrades() -> Vec<ModelPreset> {
let mut presets = all_model_presets();
for model in ["gpt-5.2", "gpt-5.4"] {
let preset = presets
.iter_mut()
.find(|preset| preset.model == model)
.unwrap_or_else(|| panic!("{model} preset present"));
preset.upgrade = Some(ModelUpgrade {
id: "gpt-5.5".to_string(),
migration_config_key: "hide_test_migration_prompt".to_string(),
model_link: None,
upgrade_copy: None,
migration_markdown: None,
retirement_at: None,
});
}
let mut preset = presets
.iter()
.find(|preset| preset.model == "gpt-5.5")
.expect("current model template")
.clone();
// Catalog-provided migration metadata is independent of the retired model's prompts.
preset.id = "gpt-5.2".to_string();
preset.model = "gpt-5.2".to_string();
preset.upgrade = Some(ModelUpgrade {
id: "gpt-5.5".to_string(),
migration_config_key: "hide_test_migration_prompt".to_string(),
model_link: None,
upgrade_copy: None,
migration_markdown: None,
retirement_at: None,
});
presets.push(preset);
presets
}
@@ -175,13 +178,39 @@ async fn model_migration_prompt_only_shows_for_deprecated_models() {
"gpt-5.2", "gpt-5.5", &seen, &presets
));
assert!(should_show_model_migration_prompt(
"gpt-5.4", "gpt-5.5", &seen, &presets
"gpt-5.4",
"gpt-5.6-terra",
&seen,
&presets
));
assert!(!should_show_model_migration_prompt(
"gpt-5.4", "gpt-5.4", &seen, &presets
));
}
#[test]
fn retired_model_migration_respects_catalog_metadata() {
let mut presets = model_presets_with_test_upgrades();
let current = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.2")
.expect("test preset");
current.id = "gpt-5.4-mini".to_string();
current.model = "gpt-5.4-mini".to_string();
let expected = current.upgrade.clone();
assert_eq!(
model_upgrade_for_migration("gpt-5.4-mini", &presets),
expected
);
presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4-mini")
.expect("catalog preset")
.upgrade = None;
assert_eq!(model_upgrade_for_migration("gpt-5.4-mini", &presets), None);
}
#[test]
fn select_model_availability_nux_picks_only_eligible_model() {
let mut presets = all_model_presets();
@@ -190,10 +219,10 @@ fn select_model_availability_nux_picks_only_eligible_model() {
});
let target = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.find(|preset| preset.model == "gpt-5.6-terra")
.expect("target preset present");
target.availability_nux = Some(ModelAvailabilityNux {
message: "gpt-5.4 is available".to_string(),
message: "gpt-5.6-terra is available".to_string(),
});
let selected = select_model_availability_nux(&presets, &model_availability_nux_config(&[]));
@@ -201,8 +230,8 @@ fn select_model_availability_nux_picks_only_eligible_model() {
assert_eq!(
selected,
Some(StartupTooltipOverride {
model_slug: "gpt-5.4".to_string(),
message: "gpt-5.4 is available".to_string(),
model_slug: "gpt-5.6-terra".to_string(),
message: "gpt-5.6-terra is available".to_string(),
})
);
}
@@ -215,22 +244,22 @@ fn select_model_availability_nux_does_not_fall_back_to_older_announcement() {
});
let gpt_5 = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.expect("gpt-5.4 preset present");
.find(|preset| preset.model == "gpt-5.6-terra")
.expect("gpt-5.6-terra preset present");
gpt_5.availability_nux = Some(ModelAvailabilityNux {
message: "gpt-5.4 is available".to_string(),
message: "gpt-5.6-terra is available".to_string(),
});
let gpt_5_2 = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4-mini")
.expect("gpt-5.4-mini preset present");
.find(|preset| preset.model == "gpt-5.5")
.expect("gpt-5.5 preset present");
gpt_5_2.availability_nux = Some(ModelAvailabilityNux {
message: "gpt-5.4-mini is available".to_string(),
message: "gpt-5.5 is available".to_string(),
});
let selected = select_model_availability_nux(
&presets,
&model_availability_nux_config(&[("gpt-5.4", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
&model_availability_nux_config(&[("gpt-5.6-terra", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
);
assert_eq!(selected, None);
@@ -244,15 +273,15 @@ fn select_model_availability_nux_uses_existing_model_order_as_priority() {
});
let first = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4-mini")
.expect("gpt-5.4-mini preset present");
.find(|preset| preset.model == "gpt-5.5")
.expect("gpt-5.5 preset present");
first.availability_nux = Some(ModelAvailabilityNux {
message: "first".to_string(),
});
let second = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.expect("gpt-5.4 preset present");
.find(|preset| preset.model == "gpt-5.6-terra")
.expect("gpt-5.6-terra preset present");
second.availability_nux = Some(ModelAvailabilityNux {
message: "second".to_string(),
});
@@ -262,7 +291,7 @@ fn select_model_availability_nux_uses_existing_model_order_as_priority() {
assert_eq!(
selected,
Some(StartupTooltipOverride {
model_slug: "gpt-5.4".to_string(),
model_slug: "gpt-5.6-terra".to_string(),
message: "second".to_string(),
})
);
@@ -276,15 +305,15 @@ fn select_model_availability_nux_returns_none_when_all_models_are_exhausted() {
});
let target = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.find(|preset| preset.model == "gpt-5.6-terra")
.expect("target preset present");
target.availability_nux = Some(ModelAvailabilityNux {
message: "gpt-5.4 is available".to_string(),
message: "gpt-5.6-terra is available".to_string(),
});
let selected = select_model_availability_nux(
&presets,
&model_availability_nux_config(&[("gpt-5.4", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
&model_availability_nux_config(&[("gpt-5.6-terra", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
);
assert_eq!(selected, None);
@@ -304,10 +333,10 @@ async fn prepare_startup_tooltip_override_persists_model_availability_nux_count(
});
let target = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.find(|preset| preset.model == "gpt-5.6-terra")
.expect("target preset present");
target.availability_nux = Some(ModelAvailabilityNux {
message: "gpt-5.4 is available".to_string(),
message: "gpt-5.6-terra is available".to_string(),
});
let mut local_settings = crate::local_settings::LocalSettings::from(&config);
@@ -318,10 +347,10 @@ async fn prepare_startup_tooltip_override_persists_model_availability_nux_count(
)
.await;
assert_eq!(tooltip.as_deref(), Some("gpt-5.4 is available"));
assert_eq!(tooltip.as_deref(), Some("gpt-5.6-terra is available"));
assert_eq!(
local_settings.tui.model_availability_nux.shown_count,
HashMap::from([("gpt-5.4".to_string(), 1)])
HashMap::from([("gpt-5.6-terra".to_string(), 1)])
);
let reloaded = ConfigBuilder::default()
@@ -331,125 +360,151 @@ async fn prepare_startup_tooltip_override_persists_model_availability_nux_count(
.expect("reloaded config");
assert_eq!(
reloaded.model_availability_nux.shown_count,
HashMap::from([("gpt-5.4".to_string(), 1)])
HashMap::from([("gpt-5.6-terra".to_string(), 1)])
);
}
#[tokio::test]
async fn accepted_model_migration_persists_target_default_reasoning_effort() {
let codex_home = tempdir().expect("temp codex home");
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("config");
config.model = Some("gpt-5.2".to_string());
config.model_reasoning_effort = Some(ReasoningEffortConfig::XHigh);
async fn accepted_model_migration_persists_target_default_reasoning_effort() -> Result<()> {
let presets = all_model_presets();
let mut migration_copies = Vec::new();
for (model, replacement) in [
("gpt-5.4", "gpt-5.6-terra"),
("gpt-5.4-mini", "gpt-5.6-luna"),
] {
let codex_home = tempdir()?;
std::fs::write(
codex_home.path().join("config.toml"),
format!("model = \"{model}\"\nmodel_reasoning_effort = \"xhigh\"\n"),
)?;
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
let current_model = config.model.clone().expect("saved model");
let upgrade = model_upgrade_for_migration(&current_model, &presets)
.expect("retired selection should still migrate");
assert_eq!(upgrade.id, replacement);
assert!(should_show_model_migration_prompt(
&current_model,
&upgrade.id,
&BTreeMap::new(),
&presets,
));
let target =
target_preset_for_upgrade(&presets, &upgrade.id).expect("available replacement");
let target_effort = target.default_reasoning_effort.clone();
let can_opt_out = presets.iter().any(|preset| preset.model == current_model);
let copy = migration_copy_for_models(
&current_model,
&upgrade.id,
upgrade.model_link,
upgrade.upgrade_copy,
upgrade.migration_markdown,
target.display_name.clone(),
Some(target.description.clone()),
can_opt_out,
);
migration_copies.push(model_migration_copy_to_plain_text(&copy));
let (tx_raw, mut rx) = unbounded_channel();
let app_event_tx = AppEventSender::new(tx_raw);
let (tx_raw, mut rx) = unbounded_channel();
let app_event_tx = AppEventSender::new(tx_raw);
apply_accepted_model_migration(
&mut config,
&app_event_tx,
current_model,
upgrade.id,
target_effort.clone(),
);
apply_accepted_model_migration(
&mut config,
&app_event_tx,
"gpt-5.2".to_string(),
"gpt-5.4".to_string(),
ReasoningEffortConfig::Medium,
);
assert_eq!(config.model.as_deref(), Some(replacement));
assert_eq!(config.model_reasoning_effort, Some(target_effort.clone()));
assert_eq!(config.model.as_deref(), Some("gpt-5.4"));
assert_eq!(
config.model_reasoning_effort,
Some(ReasoningEffortConfig::Medium)
);
let acknowledged = rx.try_recv().expect("acknowledged event");
assert_matches!(
acknowledged,
AppEvent::PersistModelMigrationPromptAcknowledged { from_model, to_model }
if from_model == model && to_model == replacement
);
let acknowledged = rx.try_recv().expect("acknowledged event");
assert_matches!(
acknowledged,
AppEvent::PersistModelMigrationPromptAcknowledged { from_model, to_model }
if from_model == "gpt-5.2" && to_model == "gpt-5.4"
);
let update_model = rx.try_recv().expect("update model event");
assert_matches!(
update_model,
AppEvent::UpdateModel(updated_model) if updated_model == replacement
);
let update_model = rx.try_recv().expect("update model event");
assert_matches!(
update_model,
AppEvent::UpdateModel(model) if model == "gpt-5.4"
);
let update_effort = rx.try_recv().expect("update effort event");
assert_matches!(
update_effort,
AppEvent::UpdateReasoningEffort(Some(effort)) if effort == target_effort
);
let update_effort = rx.try_recv().expect("update effort event");
assert_matches!(
update_effort,
AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::Medium))
);
let persist_selection = rx.try_recv().expect("persist model selection event");
assert_matches!(
persist_selection,
AppEvent::PersistModelSelection { model: selected_model, effort }
if selected_model == replacement && effort == Some(target_effort)
);
}
assert_snapshot!(migration_copies.join("\n"), @r"
GPT-5.4 is no longer available
let persist_selection = rx.try_recv().expect("persist model selection event");
assert_matches!(
persist_selection,
AppEvent::PersistModelSelection { model, effort }
if model == "gpt-5.4" && effort == Some(ReasoningEffortConfig::Medium)
);
Codex now uses GPT-5.6 Terra in place of GPT-5.4. Switch to GPT-5.6 Terra to continue.
GPT-5.4 Mini is no longer available
Codex now uses GPT-5.6 Luna in place of GPT-5.4 Mini. Switch to GPT-5.6 Luna to continue.
");
Ok(())
}
#[tokio::test]
async fn model_migration_prompt_respects_hide_flag_and_self_target() {
let presets = all_model_presets();
let mut seen = BTreeMap::new();
seen.insert("gpt-5.2".to_string(), "gpt-5.4".to_string());
seen.insert("gpt-5.4-mini".to_string(), "gpt-5.6-luna".to_string());
assert!(!should_show_model_migration_prompt(
"gpt-5.2",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.6-luna",
&seen,
&all_model_presets()
&presets
));
assert!(!should_show_model_migration_prompt(
"gpt-5.4",
"gpt-5.4",
"gpt-5.6-luna",
"gpt-5.6-luna",
&seen,
&all_model_presets()
&presets
));
}
#[tokio::test]
async fn model_migration_prompt_skips_when_target_missing_or_hidden() {
let mut available = all_model_presets();
let mut current = available
.iter()
.find(|preset| preset.model == "gpt-5.2")
.cloned()
.expect("preset present");
current.upgrade = Some(ModelUpgrade {
id: "missing-target".to_string(),
migration_config_key: HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG.to_string(),
model_link: None,
upgrade_copy: None,
migration_markdown: None,
retirement_at: None,
});
available.retain(|preset| preset.model != "gpt-5.2");
available.push(current.clone());
available.retain(|preset| preset.model != "gpt-5.6-luna");
assert!(!should_show_model_migration_prompt(
&current.model,
"missing-target",
"gpt-5.4-mini",
"gpt-5.6-luna",
&BTreeMap::new(),
&available,
));
assert!(target_preset_for_upgrade(&available, "missing-target").is_none());
assert!(target_preset_for_upgrade(&available, "gpt-5.6-luna").is_none());
let mut with_hidden_target = all_model_presets();
let target = with_hidden_target
.iter_mut()
.find(|preset| preset.model == "gpt-5.4")
.find(|preset| preset.model == "gpt-5.6-luna")
.expect("target preset present");
target.show_in_picker = false;
assert!(!should_show_model_migration_prompt(
"gpt-5.2",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.6-luna",
&BTreeMap::new(),
&with_hidden_target,
));
assert!(target_preset_for_upgrade(&with_hidden_target, "gpt-5.4").is_none());
assert!(target_preset_for_upgrade(&with_hidden_target, "gpt-5.6-luna").is_none());
}
#[tokio::test]
@@ -21,7 +21,7 @@ async fn model_default_saves_report_server_outcomes_and_target_server_profile()
loader_overrides.user_config_profile = Some("work".parse()?);
let overrides = if outcome == "overridden" {
vec![
("model".into(), toml::Value::String("gpt-5.2".into())),
("model".into(), toml::Value::String("gpt-5.6-terra".into())),
(
"model_reasoning_effort".into(),
toml::Value::String("low".into()),
@@ -69,7 +69,7 @@ async fn model_default_saves_report_server_outcomes_and_target_server_profile()
let mut tui = crate::tui::test_support::make_test_tui()?;
for event in [
AppEvent::PersistModelSelection {
model: "gpt-5.4".into(),
model: "gpt-5.5".into(),
effort: Some(ReasoningEffortConfig::High),
},
AppEvent::PersistPlanModeReasoningEffort(Some(ReasoningEffortConfig::High)),
@@ -77,7 +77,7 @@ async fn model_default_saves_report_server_outcomes_and_target_server_profile()
service_tier: Some(ServiceTier::Fast.request_value().into()),
},
AppEvent::ApplyAdvancedReasoning {
model: "gpt-5.4".into(),
model: "gpt-5.5".into(),
effort: ReasoningEffortConfig::Ultra,
},
] {
@@ -112,7 +112,7 @@ async fn model_default_saves_report_server_outcomes_and_target_server_profile()
assert_eq!(
toml::from_str::<toml::Value>(&persisted)?,
toml::Value::Table(toml::toml! {
model = "gpt-5.4"
model = "gpt-5.5"
model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "high"
service_tier = "fast"
+17 -2
View File
@@ -7,6 +7,7 @@ use codex_app_server_client::AppServerEvent;
use codex_app_server_protocol::ModelSafetyBufferingUpdatedNotification;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -25,8 +26,8 @@ use pretty_assertions::assert_eq;
use serde_json::Value;
use tokio::sync::oneshot;
const CURRENT_MODEL: &str = "gpt-5.2";
const FASTER_MODEL: &str = "gpt-5.4";
const CURRENT_MODEL: &str = "gpt-5.6-terra";
const FASTER_MODEL: &str = "gpt-5.6-luna";
const MODEL_PROVIDER_ID: &str = "safety-retry-test";
const PREVIOUS_PROMPT: &str = "Establish context";
const RETRY_PROMPT: &str = "Handle the safety-buffered request";
@@ -510,12 +511,25 @@ async fn run_safety_retry(
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
let codex_home = tempdir()?;
// Keep text-only retry fixtures independent of the optional Code Mode host.
let mut model_catalog = codex_models_manager::bundled_models_response()?;
for model in model_catalog
.models
.iter_mut()
.filter(|model| matches!(model.slug.as_str(), CURRENT_MODEL | FASTER_MODEL))
{
model.tool_mode = Some(ToolMode::Direct);
}
let model_catalog_path = codex_home.path().join("models.json");
std::fs::write(&model_catalog_path, serde_json::to_vec(&model_catalog)?)?;
let model_catalog_path = toml::Value::String(model_catalog_path.display().to_string());
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"
model = "{CURRENT_MODEL}"
model_provider = "{MODEL_PROVIDER_ID}"
model_catalog_json = {model_catalog_path}
[model_providers.{MODEL_PROVIDER_ID}]
name = "Safety retry test"
@@ -533,6 +547,7 @@ goals = true
app.config.codex_home = codex_home.path().to_path_buf().abs();
app.config.sqlite = codex_state::SqliteConfig::new_for_testing(codex_home.path().abs());
app.config.model = Some(CURRENT_MODEL.to_string());
app.config.model_catalog = Some(model_catalog);
app.config.model_provider_id = MODEL_PROVIDER_ID.to_string();
app.config.model_provider = ModelProviderInfo {
name: "Safety retry test".to_string(),
@@ -10,4 +10,4 @@ expression: normalize_snapshot_paths(rendered)
› Ask Codex to do anything
gpt-5.2 medium · /tmp/project
gpt-5.6-terra medium · /tmp/project
@@ -1,6 +1,5 @@
---
source: tui/src/app/tests/backend_banner_fallback_tests.rs
assertion_line: 124
expression: "notices[0]"
---
• Automatically switched to gpt-5.2 medium due to usage limits.
• Automatically switched to gpt-5.6-terra medium due to usage limits.
@@ -1,8 +1,7 @@
---
source: tui/src/app/tests/backend_banner_fallback_tests.rs
assertion_line: 234
expression: rendered
expression: normalize_snapshot_paths(rendered)
---
› Ask Codex to do anything
gpt-5.4 default · /tmp/project
gpt-5.5 default · /tmp/project
@@ -2,4 +2,4 @@
source: tui/src/app/tests/luna_reserve_recovery_tests.rs
expression: notice
---
• Automatically switched back to gpt-5.4 high because ordinary usage is available again.
• Automatically switched back to gpt-5.5 high because ordinary usage is available again.
@@ -1,11 +1,10 @@
---
source: tui/src/app/tests/model_defaults_tests.rs
assertion_line: 91
expression: messages
---
⚠ Saved default model and reasoning effort, but a higher-priority configuration
layer overrides the saved value.
• Model changed to gpt-5.4 high
• Model changed to gpt-5.5 high
⚠ Saved Plan mode reasoning effort, but a higher-priority configuration layer
overrides the saved value.
⚠ Saved default service tier, but a higher-priority configuration layer
@@ -13,4 +12,4 @@ expression: messages
• Service tier set to priority
⚠ Saved default model and reasoning effort, but a higher-priority configuration
layer overrides the saved value.
• Model changed to gpt-5.4 ultra for this conversation
• Model changed to gpt-5.5 ultra for this conversation
@@ -44,7 +44,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
let home = tempdir()?;
std::fs::write(
home.path().join("config.toml"),
"model = \"gpt-5.2\"\nmodel_reasoning_effort = \"low\"\nfeatures.fast_mode = true\n",
"model = \"gpt-5.5\"\nmodel_reasoning_effort = \"low\"\nfeatures.fast_mode = true\n",
)?;
let config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
@@ -76,7 +76,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
let client_preset = bootstrap
.available_models
.iter_mut()
.find(|preset| preset.model == "gpt-5.2")
.find(|preset| preset.model == "gpt-5.5")
.expect("client model in catalog");
client_preset.upgrade = Some(codex_protocol::openai_models::ModelUpgrade {
id: "server-model".into(),
@@ -88,7 +88,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
});
assert!(
crate::app::startup_prompts::should_show_model_migration_prompt(
"gpt-5.2",
"gpt-5.5",
"server-model",
&Default::default(),
&bootstrap.available_models,
+3 -3
View File
@@ -3466,7 +3466,7 @@ mod tests {
async fn persisted_resume_does_not_forward_implicit_service_tier() -> Result<()> {
let codex_home = tempfile::tempdir().expect("tempdir");
let mut config = build_config(&codex_home).await;
config.model = Some("gpt-5.4".to_string());
config.model = Some("gpt-5.5".to_string());
config.service_tier = None;
config
.features
@@ -3486,8 +3486,8 @@ mod tests {
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
let mut preset = crate::test_support::TEST_MODEL_PRESETS
.iter()
.find(|preset| preset.model == "gpt-5.4")
.expect("gpt-5.4 test preset")
.find(|preset| preset.model == "gpt-5.5")
.expect("gpt-5.5 test preset")
.clone();
preset.service_tiers = vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
@@ -2,7 +2,7 @@
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: popup
---
Select Reasoning Level for gpt-5.4
Select Reasoning Level for gpt-5.5
› 1. More reasoning… Ultra consumes usage limits faster
@@ -5,9 +5,8 @@ expression: "render_bottom_popup(&chat, 80)"
Select Model and Effort
Access legacy models by running codex -m <model_name> or in your config.toml
› 1. gpt-5.5 Frontier model for complex coding, research, and real-
› 1. gpt-5.6-terra Balanced agentic coding model for everyday work.
2. gpt-5.5 (current) Frontier model for complex coding, research, and real-
world work.
2. gpt-5.2 (current) Optimized for professional work and long-running
agents.
Press enter to confirm or esc to go back
@@ -7,6 +7,6 @@ expression: "render_bottom_popup(&chat, 80)"
1. codex-auto-test Auto model
› 2. All models (current) Choose a specific model and reasoning level
(current: gpt-5.2)
(current: gpt-5.5)
Press enter to confirm or esc to go back
@@ -2,7 +2,7 @@
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: popup
---
Select Reasoning Level for gpt-5.4
Select Reasoning Level for gpt-5.5
1. Low Fast responses with lighter reasoning
2. Medium (default) Balances speed and reasoning depth for everyday tasks
@@ -1,17 +0,0 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: popup
---
Select Reasoning Level for gpt-5.2
1. Low Balances speed with some reasoning; useful for
straightforward queries and short explanations
2. Medium (default) Provides a solid balance of reasoning depth and
latency for general-purpose tasks
3. High Maximizes reasoning depth for complex or ambiguous
problems
› 4. Extra high (current) Extra high reasoning for complex problems
⚠ Extra high reasoning effort can quickly consume
Plus plan rate limits.
Press enter to confirm or esc to go back
@@ -1,6 +1,6 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: popup
expression: "render_bottom_popup(&chat, 80)"
---
Select Model and Effort
Access legacy models by running codex -m <model_name> or in your config.toml
@@ -10,9 +10,7 @@ expression: popup
2. gpt-5.6-sol Latest frontier agentic coding model.
3. gpt-5.6-terra Balanced agentic coding model for everyday work.
4. gpt-5.6-luna Fast and affordable agentic coding model.
5. gpt-5.5 Frontier model for complex coding, research, and
› 5. gpt-5.5 (current) Frontier model for complex coding, research, and
real-world work.
› 6. gpt-5.2 (current) Optimized for professional work and long-running
agents.
Press enter to confirm or esc to go back
@@ -6,7 +6,7 @@ expression: "render_bottom_popup(&chat,\n100).replace(&directory,\n\"[[workspace
Copy to clipboard
› 1. Whole status >_ OpenAI Codex (vVERSION)
2. Model gpt-5.4
2. Model gpt-5.5
3. Directory [[workspace]]
4. Thread name Clipboard example
5. Session ID 00000000-0000-0000-0000-000000000123
@@ -7,7 +7,7 @@ expression: "whole_status.replace(&directory,\n\"[[workspace]]\").replace(crate:
Visit https://chatgpt.com/codex/settings/usage for up-to-date
information on rate limits and credits
Model: gpt-5.4 (reasoning medium, summaries auto)
Model: gpt-5.5 (reasoning medium, summaries auto)
Model provider: openai
Directory: [[workspace]]
Permissions: Read Only (Ask for approval)
@@ -187,7 +187,7 @@ fn open_safety_buffering_retry_confirmation(
#[tokio::test]
async fn safety_buffering_offers_one_retry_with_app_wording() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.model = "faster-model".to_string();
preset.display_name = "Faster Model".to_string();
chat.model_catalog = Arc::new(ModelCatalog::new(vec![preset]));
+12 -2
View File
@@ -298,13 +298,23 @@ fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> Model
}
pub(crate) fn set_fast_mode_test_catalog(chat: &mut ChatWidget) {
set_fast_mode_test_catalog_for_models(chat, "gpt-5.4", "gpt-5.2");
}
pub(crate) fn set_fast_mode_test_catalog_for_models(
chat: &mut ChatWidget,
fast_model: &str,
standard_model: &str,
) {
let models: Vec<ModelPreset> = ModelsResponse {
models: vec![
test_model_info(
"gpt-5.4", /*priority*/ 0, /*supports_fast_mode*/ true,
fast_model, /*priority*/ 0, /*supports_fast_mode*/ true,
),
test_model_info(
"gpt-5.2", /*priority*/ 1, /*supports_fast_mode*/ false,
standard_model,
/*priority*/ 1,
/*supports_fast_mode*/ false,
),
],
}
+45 -45
View File
@@ -259,7 +259,7 @@ async fn submit_user_message_with_mode_sets_coding_collaboration_mode() {
#[tokio::test]
async fn reasoning_selection_in_plan_mode_opens_scope_prompt_event() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -269,7 +269,7 @@ async fn reasoning_selection_in_plan_mode_opens_scope_prompt_event() {
set_chatgpt_auth(&mut chat);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -280,13 +280,13 @@ async fn reasoning_selection_in_plan_mode_opens_scope_prompt_event() {
AppEvent::OpenPlanReasoningScopePrompt {
model,
effort: Some(_)
} if model == "gpt-5.4"
} if model == "gpt-5.5"
);
}
#[tokio::test]
async fn reasoning_selection_in_plan_mode_without_effort_change_does_not_open_scope_prompt_event() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -297,7 +297,7 @@ async fn reasoning_selection_in_plan_mode_without_effort_change_does_not_open_sc
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -305,7 +305,7 @@ async fn reasoning_selection_in_plan_mode_without_effort_change_does_not_open_sc
assert!(
events.iter().any(|event| matches!(
event,
AppEvent::UpdateModel(model) if model == "gpt-5.4"
AppEvent::UpdateModel(model) if model == "gpt-5.5"
)),
"expected model update event; events: {events:?}"
);
@@ -320,7 +320,7 @@ async fn reasoning_selection_in_plan_mode_without_effort_change_does_not_open_sc
#[tokio::test]
async fn reasoning_selection_in_plan_mode_matching_plan_effort_but_different_global_opens_scope_prompt()
{
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -334,7 +334,7 @@ async fn reasoning_selection_in_plan_mode_matching_plan_effort_but_different_glo
// should open the scope prompt rather than silently rewriting the global default.
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -344,13 +344,13 @@ async fn reasoning_selection_in_plan_mode_matching_plan_effort_but_different_glo
AppEvent::OpenPlanReasoningScopePrompt {
model,
effort: Some(ReasoningEffortConfig::Medium)
} if model == "gpt-5.4"
} if model == "gpt-5.5"
);
}
#[tokio::test]
async fn reasoning_shortcut_in_plan_mode_updates_plan_override_without_prompt_or_persist() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -398,7 +398,7 @@ async fn reasoning_shortcut_in_plan_mode_updates_plan_override_without_prompt_or
#[tokio::test]
async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
for effort in [ReasoningEffortConfig::Ultra, ReasoningEffortConfig::Max] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -406,7 +406,7 @@ async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
chat.set_collaboration_mask(plan_mask);
let _ = drain_insert_history(&mut rx);
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
effort: effort.clone(),
description: "Advanced reasoning".to_string(),
@@ -421,7 +421,7 @@ async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
AppEvent::ApplyAdvancedReasoning {
model,
effort: ReasoningEffortConfig::Ultra,
} if model == "gpt-5.4"
} if model == "gpt-5.5"
)));
assert!(events.iter().all(|event| !matches!(
event,
@@ -435,7 +435,7 @@ async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
AppEvent::OpenPlanReasoningScopePrompt {
model,
effort: Some(ReasoningEffortConfig::Max),
} if model == "gpt-5.4"
} if model == "gpt-5.5"
)));
}
}
@@ -443,7 +443,7 @@ async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
#[tokio::test]
async fn plan_mode_reasoning_override_is_marked_current_in_reasoning_popup() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
set_chatgpt_auth(&mut chat);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
@@ -453,7 +453,7 @@ async fn plan_mode_reasoning_override_is_marked_current_in_reasoning_popup() {
.expect("expected plan collaboration mode");
chat.set_collaboration_mask(plan_mask);
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
let popup = render_bottom_popup(&chat, /*width*/ 100);
@@ -466,7 +466,7 @@ async fn plan_mode_reasoning_override_is_marked_current_in_reasoning_popup() {
#[tokio::test]
async fn reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_prompt_event() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
@@ -475,7 +475,7 @@ async fn reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_promp
let _ = drain_insert_history(&mut rx);
set_chatgpt_auth(&mut chat);
let preset = get_available_model(&chat, "gpt-5.2");
let preset = get_available_model(&chat, "gpt-5.6-terra");
chat.open_reasoning_popup(preset);
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -483,7 +483,7 @@ async fn reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_promp
assert!(
events.iter().any(|event| matches!(
event,
AppEvent::UpdateModel(model) if model == "gpt-5.2"
AppEvent::UpdateModel(model) if model == "gpt-5.6-terra"
)),
"expected model update event; events: {events:?}"
);
@@ -497,8 +497,8 @@ async fn reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_promp
#[tokio::test]
async fn plan_reasoning_scope_popup_all_modes_persists_global_and_plan_override() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.open_plan_reasoning_scope_prompt("gpt-5.4".to_string(), Some(ReasoningEffortConfig::High));
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.open_plan_reasoning_scope_prompt("gpt-5.5".to_string(), Some(ReasoningEffortConfig::High));
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -522,7 +522,7 @@ async fn plan_reasoning_scope_popup_all_modes_persists_global_and_plan_override(
events.iter().any(|event| matches!(
event,
AppEvent::PersistModelSelection { model, effort: Some(ReasoningEffortConfig::High) }
if model == "gpt-5.4"
if model == "gpt-5.5"
)),
"expected global model reasoning selection persistence; events: {events:?}"
);
@@ -548,7 +548,7 @@ fn plan_mode_prompt_notification_uses_dedicated_type_name() {
#[tokio::test]
async fn open_plan_implementation_prompt_sets_pending_notification() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.local_settings.tui.notification_settings.notifications =
Notifications::Custom(vec!["plan-mode-prompt".to_string()]);
@@ -562,11 +562,11 @@ async fn open_plan_implementation_prompt_sets_pending_notification() {
#[tokio::test]
async fn open_plan_reasoning_scope_prompt_sets_pending_notification() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.local_settings.tui.notification_settings.notifications =
Notifications::Custom(vec!["plan-mode-prompt".to_string()]);
chat.open_plan_reasoning_scope_prompt("gpt-5.4".to_string(), Some(ReasoningEffortConfig::High));
chat.open_plan_reasoning_scope_prompt("gpt-5.5".to_string(), Some(ReasoningEffortConfig::High));
assert_matches!(
chat.pending_notification,
@@ -576,7 +576,7 @@ async fn open_plan_reasoning_scope_prompt_sets_pending_notification() {
#[tokio::test]
async fn agent_turn_complete_does_not_override_pending_plan_mode_prompt_notification() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.open_plan_implementation_prompt();
chat.notify(Notification::AgentTurnComplete {
@@ -591,7 +591,7 @@ async fn agent_turn_complete_does_not_override_pending_plan_mode_prompt_notifica
#[tokio::test]
async fn request_user_input_notification_overrides_pending_agent_turn_complete_notification() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.notify(Notification::AgentTurnComplete {
response: "done".to_string(),
@@ -623,7 +623,7 @@ async fn request_user_input_notification_overrides_pending_agent_turn_complete_n
#[tokio::test]
async fn handle_request_user_input_sets_pending_notification() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.local_settings.tui.notification_settings.notifications =
Notifications::Custom(vec!["plan-mode-prompt".to_string()]);
@@ -654,10 +654,10 @@ async fn handle_request_user_input_sets_pending_notification() {
#[tokio::test]
async fn plan_reasoning_scope_popup_mentions_selected_reasoning() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::Low));
chat.open_plan_reasoning_scope_prompt(
"gpt-5.4".to_string(),
"gpt-5.5".to_string(),
Some(ReasoningEffortConfig::Medium),
);
@@ -671,9 +671,9 @@ async fn plan_reasoning_scope_popup_mentions_selected_reasoning() {
#[tokio::test]
async fn plan_reasoning_scope_popup_mentions_built_in_plan_default_when_no_override() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.open_plan_reasoning_scope_prompt(
"gpt-5.4".to_string(),
"gpt-5.5".to_string(),
Some(ReasoningEffortConfig::Medium),
);
@@ -683,8 +683,8 @@ async fn plan_reasoning_scope_popup_mentions_built_in_plan_default_when_no_overr
#[tokio::test]
async fn plan_reasoning_scope_popup_plan_only_does_not_update_all_modes_reasoning() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.open_plan_reasoning_scope_prompt("gpt-5.4".to_string(), Some(ReasoningEffortConfig::High));
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.open_plan_reasoning_scope_prompt("gpt-5.5".to_string(), Some(ReasoningEffortConfig::High));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -1365,7 +1365,7 @@ async fn mode_switch_surfaces_model_change_notification_when_effective_model_cha
let mut plan_mask =
collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
.expect("expected plan collaboration mode");
plan_mask.model = Some("gpt-5.4-mini".to_string());
plan_mask.model = Some("gpt-5.6-luna".to_string());
chat.set_collaboration_mask(plan_mask);
let plan_messages = drain_insert_history(&mut rx)
@@ -1374,7 +1374,7 @@ async fn mode_switch_surfaces_model_change_notification_when_effective_model_cha
.collect::<Vec<_>>()
.join("\n");
assert!(
plan_messages.contains("Model changed to gpt-5.4-mini medium for Plan mode."),
plan_messages.contains("Model changed to gpt-5.6-luna medium for Plan mode."),
"expected Plan-mode model switch notice, got: {plan_messages:?}"
);
@@ -1397,7 +1397,7 @@ async fn mode_switch_surfaces_model_change_notification_when_effective_model_cha
#[tokio::test]
async fn mode_switch_surfaces_reasoning_change_notification_when_model_stays_same() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.6-terra")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
@@ -1411,7 +1411,7 @@ async fn mode_switch_surfaces_reasoning_change_notification_when_model_stays_sam
.collect::<Vec<_>>()
.join("\n");
assert!(
plan_messages.contains("Model changed to gpt-5.2 medium for Plan mode."),
plan_messages.contains("Model changed to gpt-5.6-terra medium for Plan mode."),
"expected reasoning-change notice in Plan mode, got: {plan_messages:?}"
);
}
@@ -1657,21 +1657,21 @@ async fn make_startup_chat_with_cli_overrides(
#[tokio::test]
async fn set_model_updates_active_collaboration_mask() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.6-terra")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
.expect("expected plan collaboration mask");
chat.set_collaboration_mask(plan_mask);
chat.set_model("gpt-5.4-mini");
chat.set_model("gpt-5.6-luna");
assert_eq!(chat.current_model(), "gpt-5.4-mini");
assert_eq!(chat.current_model(), "gpt-5.6-luna");
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
}
#[tokio::test]
async fn set_reasoning_effort_updates_active_collaboration_mask() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.6-terra")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
.expect("expected plan collaboration mask");
@@ -1688,7 +1688,7 @@ async fn set_reasoning_effort_updates_active_collaboration_mask() {
#[tokio::test]
async fn set_reasoning_effort_does_not_override_active_plan_override() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.6-terra")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
chat.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::High));
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
@@ -1758,10 +1758,10 @@ async fn collab_mode_applies_default_preset() {
#[tokio::test]
async fn user_turn_includes_personality_from_config() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_feature_enabled(Feature::Personality, /*enabled*/ true);
chat.thread_id = Some(ThreadId::new());
chat.set_model("gpt-5.4");
chat.set_model("gpt-5.5");
chat.set_personality(Personality::Friendly);
chat.bottom_pane
@@ -3302,7 +3302,7 @@ async fn memories_reset_confirmation_sends_event_on_confirm() {
#[tokio::test]
async fn model_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.open_model_popup();
@@ -3317,7 +3317,7 @@ fn apply_model_list_response(chat: &mut ChatWidget, presets: Vec<ModelPreset>) {
#[tokio::test]
async fn model_picker_refresh_rejects_obsolete_and_unusable_replies() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let initial = chat.model_catalog.try_list_models().unwrap();
let mut refreshed = initial.clone();
@@ -3362,9 +3362,9 @@ async fn model_picker_refresh_rejects_obsolete_and_unusable_replies() {
#[tokio::test]
async fn model_picker_refreshes_startup_catalog() {
for (explicit_all_models, hidden_startup) in [(false, false), (true, false), (false, true)] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let mut startup = vec![get_available_model(&chat, "gpt-5.2")];
let mut startup = vec![get_available_model(&chat, "gpt-5.5")];
let mut refreshed = chat.model_catalog.try_list_models().unwrap();
let mut auto = startup[0].clone();
auto.model = "codex-auto-test".to_string();
@@ -3405,10 +3405,10 @@ async fn model_picker_refreshes_startup_catalog() {
#[tokio::test]
async fn model_picker_queued_all_models_uses_refreshed_catalog() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let refreshed = chat.model_catalog.try_list_models().unwrap();
let preset = get_available_model(&chat, "gpt-5.2");
let preset = get_available_model(&chat, "gpt-5.5");
let mut auto = preset.clone();
auto.model = "codex-auto-test".to_string();
auto.id = auto.model.clone();
@@ -3430,16 +3430,16 @@ async fn model_picker_queued_all_models_uses_refreshed_catalog() {
#[tokio::test]
async fn model_picker_refresh_preserves_highlight() {
for (remove_selected, reasoning_submenu, expected) in [
(false, false, "gpt-5.5"),
(true, false, "gpt-5.2"),
(false, true, "gpt-5.5"),
(true, true, "gpt-5.2"),
(false, false, "gpt-5.6-terra"),
(true, false, "gpt-5.5"),
(false, true, "gpt-5.6-terra"),
(true, true, "gpt-5.5"),
] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let mut presets = vec![
get_available_model(&chat, "gpt-5.2"),
get_available_model(&chat, "gpt-5.5"),
get_available_model(&chat, "gpt-5.6-terra"),
];
chat.model_catalog = Arc::new(ModelCatalog::new(presets.clone()));
chat.open_model_popup();
@@ -3506,9 +3506,9 @@ async fn model_picker_refresh_preserves_dismissal_and_reasoning_submenu() {
(false, false, true),
(false, true, true),
] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let preset = get_available_model(&chat, "gpt-5.2");
let preset = get_available_model(&chat, "gpt-5.5");
let refreshed = chat.model_catalog.try_list_models().unwrap();
chat.model_catalog = Arc::new(ModelCatalog::new(vec![preset.clone()]));
chat.open_model_popup();
@@ -3556,9 +3556,9 @@ async fn model_picker_refresh_dismisses_empty_choices() {
for (explicit_all_models, reasoning_submenu) in
[(false, false), (true, false), (false, true), (true, true)]
{
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let mut preset = get_available_model(&chat, "gpt-5.2");
let mut preset = get_available_model(&chat, "gpt-5.5");
chat.open_model_popup();
if explicit_all_models {
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
@@ -3592,7 +3592,7 @@ async fn model_picker_refresh_dismisses_empty_choices() {
#[tokio::test]
async fn personality_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.open_personality_popup();
@@ -3687,12 +3687,12 @@ async fn server_overloaded_error_does_not_switch_models() {
#[tokio::test]
async fn model_reasoning_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
set_chatgpt_auth(&mut chat);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.supported_reasoning_efforts.insert(
2,
ReasoningEffortPreset {
@@ -3720,10 +3720,10 @@ async fn model_reasoning_selection_popup_snapshot() {
#[tokio::test]
async fn model_advanced_reasoning_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Ultra));
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.supported_reasoning_efforts.extend([
ReasoningEffortPreset {
effort: ReasoningEffortConfig::Ultra,
@@ -3742,11 +3742,11 @@ async fn model_advanced_reasoning_selection_popup_snapshot() {
#[tokio::test]
async fn model_reasoning_selection_popup_applies_custom_effort() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
let custom_effort = ReasoningEffortConfig::Custom("future".to_string());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset
.supported_reasoning_efforts
.push(ReasoningEffortPreset {
@@ -3770,19 +3770,19 @@ async fn model_reasoning_selection_popup_applies_custom_effort() {
selected_effort_events,
vec![
(None, Some(custom_effort.clone())),
(Some("gpt-5.4".to_string()), Some(custom_effort)),
(Some("gpt-5.5".to_string()), Some(custom_effort)),
]
);
}
async fn select_ultra_with_multi_agent_thread_limit(max_threads: usize) -> (bool, Vec<String>) {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.config
.multi_agent_v2
.max_concurrent_threads_per_session = max_threads;
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.default_reasoning_effort = ReasoningEffortConfig::High;
preset.supported_reasoning_efforts = vec![
ReasoningEffortPreset {
@@ -3849,10 +3849,10 @@ async fn ultra_reasoning_selection_skips_warning_below_threshold() {
#[tokio::test]
async fn max_reasoning_selection_persists_model_selection() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
effort: ReasoningEffortConfig::Max,
description: "Maximum reasoning".to_string(),
@@ -3870,7 +3870,7 @@ async fn max_reasoning_selection_persists_model_selection() {
AppEvent::PersistModelSelection {
model,
effort: Some(ReasoningEffortConfig::Max),
} if model == "gpt-5.4"
} if model == "gpt-5.5"
)));
assert!(
events
@@ -3879,26 +3879,12 @@ async fn max_reasoning_selection_persists_model_selection() {
);
}
#[tokio::test]
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
set_chatgpt_auth(&mut chat);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
let preset = get_available_model(&chat, "gpt-5.2");
chat.open_reasoning_popup(preset);
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert_chatwidget_snapshot!("model_reasoning_selection_popup_extra_high_warning", popup);
}
async fn assert_reasoning_shortcuts_update_effort(
key_events: [KeyEvent; 2],
expected_effort: ReasoningEffortConfig,
) {
for key_event in key_events {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
@@ -3953,7 +3939,7 @@ async fn reasoning_down_shortcuts_lower_reasoning_effort() {
#[tokio::test]
async fn reasoning_shortcut_clears_armed_quit_shortcut() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
chat.arm_quit_shortcut(key_hint::ctrl(KeyCode::Char('c')));
@@ -3974,7 +3960,7 @@ async fn reasoning_shortcut_clears_armed_quit_shortcut() {
#[tokio::test]
async fn reasoning_shortcut_is_ignored_with_model_popup_open() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
chat.open_model_popup();
@@ -3999,12 +3985,12 @@ async fn reasoning_shortcut_is_ignored_with_model_popup_open() {
#[tokio::test]
async fn reasoning_up_shortcut_does_not_silently_enter_advanced_effort() {
for (model, model_path) in [
("gpt-5.4", "All models → gpt-5.4"),
("gpt-5.5", "All models → gpt-5.5"),
("codex-auto-test", "codex-auto-test"),
] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.id = model.to_string();
preset.model = model.to_string();
preset.display_name = model.to_string();
@@ -4051,9 +4037,9 @@ async fn reasoning_up_shortcut_does_not_silently_enter_advanced_effort() {
#[tokio::test]
async fn reasoning_down_shortcut_can_leave_advanced_effort() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.supported_reasoning_efforts.extend([
ReasoningEffortPreset {
effort: ReasoningEffortConfig::Ultra,
@@ -4088,11 +4074,11 @@ async fn reasoning_down_shortcut_can_leave_advanced_effort() {
#[tokio::test]
async fn reasoning_popup_shows_extra_high_with_space() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
set_chatgpt_auth(&mut chat);
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
let popup = render_bottom_popup(&chat, /*width*/ 120);
@@ -4158,7 +4144,7 @@ async fn single_reasoning_option_skips_selection() {
#[tokio::test]
async fn advanced_only_reasoning_option_requires_explicit_selection() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let mut preset = get_available_model(&chat, "gpt-5.4");
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.default_reasoning_effort = ReasoningEffortConfig::Ultra;
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
effort: ReasoningEffortConfig::Ultra,
@@ -4264,11 +4250,11 @@ async fn feedback_good_result_consent_popup_includes_connectivity_diagnostics_fi
#[tokio::test]
async fn reasoning_popup_escape_returns_to_model_popup() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.thread_id = Some(ThreadId::new());
chat.open_model_popup();
let preset = get_available_model(&chat, "gpt-5.4");
let preset = get_available_model(&chat, "gpt-5.5");
chat.open_reasoning_popup(preset);
let before_escape = render_bottom_popup(&chat, /*width*/ 80);
@@ -122,8 +122,8 @@ fn next_copy_selection(
#[tokio::test]
async fn service_tier_commands_lowercase_catalog_names() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let mut preset = get_available_model(&chat, "gpt-5.4");
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
let mut preset = get_available_model(&chat, "gpt-5.5");
let expected_description = preset
.service_tiers
.iter()
@@ -2068,7 +2068,7 @@ async fn slash_copy_picker_previews_whole_response_code_blocks_and_blockquotes()
#[tokio::test]
async fn slash_copy_picker_copies_status_fields_and_preserves_source_after_copying() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
let session_id = "00000000-0000-0000-0000-000000000123";
chat.thread_id = Some(ThreadId::from_string(session_id).expect("valid thread ID"));
chat.thread_name = Some("Clipboard example".to_string());
@@ -2098,7 +2098,7 @@ async fn slash_copy_picker_copies_status_fields_and_preserves_source_after_copyi
);
let expected = [
("Whole status", whole_status.as_str()),
("Model", "gpt-5.4"),
("Model", "gpt-5.5"),
("Directory", directory.as_str()),
("Thread name", "Clipboard example"),
("Session ID", session_id),
@@ -90,7 +90,7 @@ async fn status_command_renders_immediately_without_rate_limit_refresh() {
#[tokio::test]
async fn status_command_uses_catalog_default_reasoning_when_config_empty() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.config.model_reasoning_effort = None;
chat.dispatch_command(SlashCommand::Status);
@@ -102,7 +102,7 @@ async fn status_command_uses_catalog_default_reasoning_when_config_empty() {
other => panic!("expected status output, got {other:?}"),
};
assert!(
rendered.contains("gpt-5.4 (reasoning medium, summaries auto)"),
rendered.contains("gpt-5.5 (reasoning medium, summaries auto)"),
"expected /status to render the catalog default reasoning effort, got: {rendered}"
);
}