From a73fa8a869b3de00cf64f3ab62e40505a5da0314 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 8 Nov 2026 17:07:12 +0000 Subject: [PATCH] Warn when a CLI model is absent from the ChatGPT model list (#60270) ## What changed Show a startup warning when the active model matches the CLI model override but is missing from the ChatGPT model list, for ChatGPT accounts using the OpenAI provider. Suggest checking the spelling or choosing another model with `/model`, while preserving the selected model and submitting queued startup input. Reuse the transcript-visible warning helper for this notice and existing usage warnings. ## Testing Extend the startup test to check the warning text, preserved model selection, and queued input submission. GitOrigin-RevId: bb1e8c415346bf271e25ef4e8571c13b4bc2c8c8 --- codex-rs/tui/src/app/session_lifecycle.rs | 15 ++++++++++++ codex-rs/tui/src/app/tests/startup.rs | 24 ++++++++++++++++--- codex-rs/tui/src/chatwidget/rate_limits.rs | 2 +- codex-rs/tui/src/history_cell/notices.rs | 2 +- .../tui/src/history_cell/warnings_tests.rs | 4 ++-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index c1d5539f2e..1ab678f622 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -16,6 +16,7 @@ use crate::app_server_session::source_agent_path; use crate::app_server_session::thread_blocks_direct_input; use crate::chatwidget::ThreadInputStateRestoreMode; use crate::streaming::commit_tick::CommitTickScope; +use codex_model_provider_info::OPENAI_PROVIDER_ID; use std::collections::HashSet; #[derive(Clone, Copy)] @@ -1108,6 +1109,20 @@ impl App { self.enqueue_primary_thread_session(started.session, started.turns) .await?; self.apply_backend_banner_fallback(app_server).await; + if let Some(model) = self.harness_overrides.model.as_deref() + && model == self.chat_widget.current_model() + && self.chat_widget.has_chatgpt_account() + && self.chat_widget.config_ref().model_provider_id == OPENAI_PROVIDER_ID + && !self + .model_catalog + .models + .iter() + .any(|preset| preset.model == model) + { + self.chat_widget.add_to_history(history_cell::new_visible_warning_event(format!( + "Your ChatGPT model list doesn't include `{model}`. Check the spelling or use /model to choose another model." + ))); + } if let Some(notice) = self.pending_server_version_notice.take() { self.chat_widget.add_server_version_warning(notice); } diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index 04d2c4ea26..7a91a3a2fb 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -1365,7 +1365,10 @@ async fn known_thread_started_preserves_session_without_reading_unmaterialized_r #[tokio::test] async fn startup_thread_started_submits_queued_startup_input() { - let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; + let (mut app, mut events, mut op_rx) = make_test_app_with_channels().await; + set_chatgpt_auth(&mut app.chat_widget); + let model = "unlisted-model"; + app.harness_overrides.model = Some(model.into()); app.pending_startup_thread_start = true; app.chat_widget .set_queue_submissions_until_session_configured(/*queue*/ true); @@ -1384,11 +1387,13 @@ async fn startup_thread_started_submits_queued_startup_input() { )) .await .expect("embedded app server"); - let thread_id = ThreadId::new(); + let mut session = test_thread_session(ThreadId::new(), test_path_buf("/tmp/project")); + session.model = model.into(); + session.model_provider_id = "openai".into(); app.handle_startup_thread_started( &mut app_server, Ok(AppServerStartedThread { - session: test_thread_session(thread_id, test_path_buf("/tmp/project")), + session, turns: Vec::new(), blocks_direct_input: false, task_tools_available: false, @@ -1397,6 +1402,19 @@ async fn startup_thread_started_submits_queued_startup_input() { .await .expect("startup thread should attach"); + let warnings: Vec<_> = std::iter::from_fn(|| events.try_recv().ok()) + .filter_map(|event| match event { + AppEvent::InsertHistoryCell(cell) if !cell.warning_entries().is_empty() => { + Some(lines_to_single_string(&cell.display_lines(/*width*/ 80))) + } + _ => None, + }) + .collect(); + insta::assert_snapshot!(warnings.join("\n"), @r" + ⚠ Your ChatGPT model list doesn't include `unlisted-model`. Check the spelling + or use /model to choose another model. + "); + assert_eq!(app.chat_widget.current_model(), model); match next_user_turn_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( items, diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index a7162b98b9..60c2470d1a 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -393,7 +393,7 @@ impl ChatWidget { if !warnings.is_empty() { for warning in warnings { - self.add_to_history(history_cell::new_usage_warning_event(warning)); + self.add_to_history(history_cell::new_visible_warning_event(warning)); } self.request_redraw(); } diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index 9f5e2137f0..7308338aa8 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -108,7 +108,7 @@ pub(crate) fn new_warning_event(message: String) -> WarningHistoryCell { } } -pub(crate) fn new_usage_warning_event(message: String) -> WarningHistoryCell { +pub(crate) fn new_visible_warning_event(message: String) -> WarningHistoryCell { WarningHistoryCell { visible_in_transcript: true, ..new_warning_event(message) diff --git a/codex-rs/tui/src/history_cell/warnings_tests.rs b/codex-rs/tui/src/history_cell/warnings_tests.rs index 42cbb002e9..020b3cf981 100644 --- a/codex-rs/tui/src/history_cell/warnings_tests.rs +++ b/codex-rs/tui/src/history_cell/warnings_tests.rs @@ -6,12 +6,12 @@ use pretty_assertions::assert_eq; #[test] fn usage_warnings_render_in_both_transcripts_including_composites() { let message = "Only 8% of your 5h limit remains. Run /status for details."; - let usage = new_usage_warning_event(message.into()); + let usage = new_visible_warning_event(message.into()); let diagnostic = new_warning_event(message.into()); let composite = CompositeHistoryCell::new(vec![ Box::new(PlainHistoryCell::new(vec!["Earlier output".into()])), Box::new(new_warning_event("Hidden diagnostic".into())), - Box::new(new_usage_warning_event(message.into())), + Box::new(new_visible_warning_event(message.into())), ]); assert_eq!(usage.warning_entries(), diagnostic.warning_entries()); for mode in [HistoryRenderMode::Rich, HistoryRenderMode::Raw] {