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
This commit is contained in:
Eric Traut
2026-11-08 17:17:51 +00:00
committed by copyberry
parent 748ab55ca7
commit a73fa8a869
5 changed files with 40 additions and 7 deletions
+15
View File
@@ -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);
}
+21 -3
View File
@@ -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,
+1 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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)
@@ -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] {