From 382f047a1064742767f84e9c551ea3c6df59d6dd Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 8 Dec 2025 15:29:37 -0800 Subject: [PATCH] Remove legacy `ModelInfo` and merge it with `ModelFamily` (#7748) This is a step towards removing the need to know `model` when constructing config. We firstly don't need to know `model_info` and just respect if the user has already set it. Next step, we don't need to know `model` unless the user explicitly set it in `config.toml` --- codex-rs/core/src/client.rs | 13 +- codex-rs/core/src/codex.rs | 13 +- codex-rs/core/src/config/mod.rs | 35 ++-- codex-rs/core/src/lib.rs | 1 - codex-rs/core/src/openai_model_info.rs | 83 -------- .../core/src/openai_models/model_family.rs | 58 +++++- codex-rs/tui/src/chatwidget.rs | 3 +- codex-rs/tui/src/chatwidget/tests.rs | 183 +++++++++--------- codex-rs/tui/src/status/card.rs | 6 +- codex-rs/tui/src/status/tests.rs | 35 ++++ 10 files changed, 205 insertions(+), 225 deletions(-) delete mode 100644 codex-rs/core/src/openai_model_info.rs diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 4c3cf737b..d4a714cdd 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -48,7 +48,6 @@ use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; -use crate::openai_model_info::get_model_info; use crate::openai_models::model_family::ModelFamily; use crate::tools::spec::create_tools_json_for_chat_completions_api; use crate::tools::spec::create_tools_json_for_responses_api; @@ -95,19 +94,11 @@ impl ModelClient { pub fn get_model_context_window(&self) -> Option { let model_family = self.get_model_family(); let effective_context_window_percent = model_family.effective_context_window_percent; - self.config - .model_context_window - .or_else(|| get_model_info(&model_family).map(|info| info.context_window)) + model_family + .context_window .map(|w| w.saturating_mul(effective_context_window_percent) / 100) } - pub fn get_auto_compact_token_limit(&self) -> Option { - let model_family = self.get_model_family(); - self.config.model_auto_compact_token_limit.or_else(|| { - get_model_info(&model_family).and_then(|info| info.auto_compact_token_limit) - }) - } - pub fn config(&self) -> Arc { Arc::clone(&self.config) } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c33904e2f..74ef11c32 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -80,7 +80,6 @@ use crate::exec::StreamOutput; use crate::exec_policy::ExecPolicyUpdateError; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; -use crate::openai_model_info::get_model_info; use crate::project_doc::get_user_instructions; use crate::protocol::AgentMessageContentDeltaEvent; use crate::protocol::AgentReasoningSectionBreakEvent; @@ -415,15 +414,11 @@ impl Session { otel_event_manager: &OtelEventManager, provider: ModelProviderInfo, session_configuration: &SessionConfiguration, - mut per_turn_config: Config, + per_turn_config: Config, model_family: ModelFamily, conversation_id: ConversationId, sub_id: String, ) -> TurnContext { - if let Some(model_info) = get_model_info(&model_family) { - per_turn_config.model_context_window = Some(model_info.context_window); - } - let otel_event_manager = otel_event_manager.clone().with_model( session_configuration.model.as_str(), model_family.slug.as_str(), @@ -1955,9 +1950,6 @@ async fn spawn_review_thread( per_turn_config.model_reasoning_effort = Some(ReasoningEffortConfig::Low); per_turn_config.model_reasoning_summary = ReasoningSummaryConfig::Detailed; per_turn_config.features = review_features.clone(); - if let Some(model_info) = get_model_info(&model_family) { - per_turn_config.model_context_window = Some(model_info.context_window); - } let otel_event_manager = parent_turn_context .client @@ -2097,7 +2089,8 @@ pub(crate) async fn run_task( } = turn_output; let limit = turn_context .client - .get_auto_compact_token_limit() + .get_model_family() + .auto_compact_token_limit() .unwrap_or(i64::MAX); let total_usage_tokens = sess.get_total_token_usage().await; let token_limit_reached = total_usage_tokens >= limit; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index a1cc46cf2..df7637a30 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -26,8 +26,6 @@ use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID; use crate::model_provider_info::built_in_model_providers; -use crate::openai_model_info::get_model_info; -use crate::openai_models::model_family::find_family_for_model; use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; @@ -1106,23 +1104,12 @@ impl Config { let forced_login_method = cfg.forced_login_method; + // todo(aibrahim): make model optional let model = model .or(config_profile.model) .or(cfg.model) .unwrap_or_else(default_model); - let model_family = find_family_for_model(&model); - - let openai_model_info = get_model_info(&model_family); - let model_context_window = cfg - .model_context_window - .or_else(|| openai_model_info.as_ref().map(|info| info.context_window)); - let model_auto_compact_token_limit = cfg.model_auto_compact_token_limit.or_else(|| { - openai_model_info - .as_ref() - .and_then(|info| info.auto_compact_token_limit) - }); - let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { let trimmed = value.trim(); if trimmed.is_empty() { @@ -1168,8 +1155,8 @@ impl Config { let config = Self { model, review_model, - model_context_window, - model_auto_compact_token_limit, + model_context_window: cfg.model_context_window, + model_auto_compact_token_limit: cfg.model_auto_compact_token_limit, model_provider_id, model_provider, cwd: resolved_cwd, @@ -2950,8 +2937,8 @@ model_verbosity = "high" Config { model: "o3".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_context_window: Some(200_000), - model_auto_compact_token_limit: Some(180_000), + model_context_window: None, + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::Never, @@ -3025,8 +3012,8 @@ model_verbosity = "high" let expected_gpt3_profile_config = Config { model: "gpt-3.5-turbo".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_context_window: Some(16_385), - model_auto_compact_token_limit: Some(14_746), + model_context_window: None, + model_auto_compact_token_limit: None, model_provider_id: "openai-chat-completions".to_string(), model_provider: fixture.openai_chat_completions_provider.clone(), approval_policy: AskForApproval::UnlessTrusted, @@ -3115,8 +3102,8 @@ model_verbosity = "high" let expected_zdr_profile_config = Config { model: "o3".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_context_window: Some(200_000), - model_auto_compact_token_limit: Some(180_000), + model_context_window: None, + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, @@ -3191,8 +3178,8 @@ model_verbosity = "high" let expected_gpt5_profile_config = Config { model: "gpt-5.1".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_context_window: Some(272_000), - model_auto_compact_token_limit: Some(244_800), + model_context_window: None, + model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 721c6bb43..59dac84d2 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -67,7 +67,6 @@ pub use conversation_manager::NewConversation; pub use auth::AuthManager; pub use auth::CodexAuth; pub mod default_client; -mod openai_model_info; pub mod project_doc; mod rollout; pub(crate) mod safety; diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs deleted file mode 100644 index 4ee7d7187..000000000 --- a/codex-rs/core/src/openai_model_info.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::openai_models::model_family::ModelFamily; - -// Shared constants for commonly used window/token sizes. -pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000; - -/// Metadata about a model, particularly OpenAI models. -/// We may want to consider including details like the pricing for -/// input tokens, output tokens, etc., though users will need to be able to -/// override this in config.toml, as this information can get out of date. -/// Though this would help present more accurate pricing information in the UI. -#[derive(Debug)] -pub(crate) struct ModelInfo { - /// Size of the context window in tokens. This is the maximum size of the input context. - pub(crate) context_window: i64, - - /// Token threshold where we should automatically compact conversation history. This considers - /// input tokens + output tokens of this turn. - pub(crate) auto_compact_token_limit: Option, -} - -impl ModelInfo { - const fn new(context_window: i64) -> Self { - Self { - context_window, - auto_compact_token_limit: Some(Self::default_auto_compact_limit(context_window)), - } - } - - const fn default_auto_compact_limit(context_window: i64) -> i64 { - (context_window * 9) / 10 - } -} - -pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { - let slug = model_family.slug.as_str(); - match slug { - // OSS models have a 128k shared token pool. - // Arbitrarily splitting it: 3/4 input context, 1/4 output. - // https://openai.com/index/gpt-oss-model-card/ - "gpt-oss-20b" => Some(ModelInfo::new(96_000)), - "gpt-oss-120b" => Some(ModelInfo::new(96_000)), - // https://platform.openai.com/docs/models/o3 - "o3" => Some(ModelInfo::new(200_000)), - - // https://platform.openai.com/docs/models/o4-mini - "o4-mini" => Some(ModelInfo::new(200_000)), - - // https://platform.openai.com/docs/models/codex-mini-latest - "codex-mini-latest" => Some(ModelInfo::new(200_000)), - - // As of Jun 25, 2025, gpt-4.1 defaults to gpt-4.1-2025-04-14. - // https://platform.openai.com/docs/models/gpt-4.1 - "gpt-4.1" | "gpt-4.1-2025-04-14" => Some(ModelInfo::new(1_047_576)), - - // As of Jun 25, 2025, gpt-4o defaults to gpt-4o-2024-08-06. - // https://platform.openai.com/docs/models/gpt-4o - "gpt-4o" | "gpt-4o-2024-08-06" => Some(ModelInfo::new(128_000)), - - // https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-05-13 - "gpt-4o-2024-05-13" => Some(ModelInfo::new(128_000)), - - // https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-11-20 - "gpt-4o-2024-11-20" => Some(ModelInfo::new(128_000)), - - // https://platform.openai.com/docs/models/gpt-3.5-turbo - "gpt-3.5-turbo" => Some(ModelInfo::new(16_385)), - - _ if slug.starts_with("gpt-5-codex") - || slug.starts_with("gpt-5.1-codex") - || slug.starts_with("gpt-5.1-codex-max") => - { - Some(ModelInfo::new(CONTEXT_WINDOW_272K)) - } - - _ if slug.starts_with("gpt-5") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)), - - _ if slug.starts_with("codex-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)), - - _ if slug.starts_with("exp-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)), - - _ => None, - } -} diff --git a/codex-rs/core/src/openai_models/model_family.rs b/codex-rs/core/src/openai_models/model_family.rs index 507e1a48d..6665165ee 100644 --- a/codex-rs/core/src/openai_models/model_family.rs +++ b/codex-rs/core/src/openai_models/model_family.rs @@ -15,6 +15,7 @@ const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md"); const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md"); const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md"); +pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000; /// A model family is a group of models that share certain characteristics. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -23,14 +24,20 @@ pub struct ModelFamily { /// "gpt-4.1-2025-04-14". pub slug: String, - /// The model family name, e.g. "gpt-4.1". Note this should able to be used - /// with [`crate::openai_model_info::get_model_info`]. + /// The model family name, e.g. "gpt-4.1". This string is used when deriving + /// default metadata for the family, such as context windows. pub family: String, /// True if the model needs additional instructions on how to use the /// "virtual" `apply_patch` CLI. pub needs_special_apply_patch_instructions: bool, + /// Maximum supported context window, if known. + pub context_window: Option, + + /// Token threshold for automatic compaction if config does not override it. + auto_compact_token_limit: Option, + // Whether the `reasoning` field can be set when making a request to this // model family. Note it has `effort` and `summary` subfields (though // `summary` is optional). @@ -82,6 +89,12 @@ impl ModelFamily { if let Some(reasoning_summary_format) = config.model_reasoning_summary_format.as_ref() { self.reasoning_summary_format = reasoning_summary_format.clone(); } + if let Some(context_window) = config.model_context_window { + self.context_window = Some(context_window); + } + if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit { + self.auto_compact_token_limit = Some(auto_compact_token_limit); + } self } pub fn with_remote_overrides(mut self, remote_models: Vec) -> Self { @@ -93,6 +106,15 @@ impl ModelFamily { } self } + + pub fn auto_compact_token_limit(&self) -> Option { + self.auto_compact_token_limit + .or(self.context_window.map(Self::default_auto_compact_limit)) + } + + const fn default_auto_compact_limit(context_window: i64) -> i64 { + (context_window * 9) / 10 + } } macro_rules! model_family { @@ -105,6 +127,8 @@ macro_rules! model_family { slug: $slug.to_string(), family: $family.to_string(), needs_special_apply_patch_instructions: false, + context_window: Some(CONTEXT_WINDOW_272K), + auto_compact_token_limit: None, supports_reasoning_summaries: false, reasoning_summary_format: ReasoningSummaryFormat::None, supports_parallel_tool_calls: false, @@ -136,12 +160,14 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { slug, "o3", supports_reasoning_summaries: true, needs_special_apply_patch_instructions: true, + context_window: Some(200_000), ) } else if slug.starts_with("o4-mini") { model_family!( slug, "o4-mini", supports_reasoning_summaries: true, needs_special_apply_patch_instructions: true, + context_window: Some(200_000), ) } else if slug.starts_with("codex-mini-latest") { model_family!( @@ -149,18 +175,32 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { supports_reasoning_summaries: true, needs_special_apply_patch_instructions: true, shell_type: ConfigShellToolType::Local, + context_window: Some(200_000), ) } else if slug.starts_with("gpt-4.1") { model_family!( slug, "gpt-4.1", needs_special_apply_patch_instructions: true, + context_window: Some(1_047_576), ) } else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") { - model_family!(slug, "gpt-oss", apply_patch_tool_type: Some(ApplyPatchToolType::Function)) + model_family!( + slug, "gpt-oss", + apply_patch_tool_type: Some(ApplyPatchToolType::Function), + context_window: Some(96_000), + ) } else if slug.starts_with("gpt-4o") { - model_family!(slug, "gpt-4o", needs_special_apply_patch_instructions: true) + model_family!( + slug, "gpt-4o", + needs_special_apply_patch_instructions: true, + context_window: Some(128_000), + ) } else if slug.starts_with("gpt-3.5") { - model_family!(slug, "gpt-3.5", needs_special_apply_patch_instructions: true) + model_family!( + slug, "gpt-3.5", + needs_special_apply_patch_instructions: true, + context_window: Some(16_385), + ) } else if slug.starts_with("test-gpt-5") { model_family!( slug, slug, @@ -196,6 +236,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { supports_parallel_tool_calls: true, support_verbosity: true, truncation_policy: TruncationPolicy::Tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), ) } else if slug.starts_with("exp-") { model_family!( @@ -209,6 +250,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { truncation_policy: TruncationPolicy::Bytes(10_000), shell_type: ConfigShellToolType::UnifiedExec, supports_parallel_tool_calls: true, + context_window: Some(CONTEXT_WINDOW_272K), ) // Production models. @@ -223,6 +265,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { supports_parallel_tool_calls: true, support_verbosity: false, truncation_policy: TruncationPolicy::Tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), ) } else if slug.starts_with("gpt-5-codex") || slug.starts_with("gpt-5.1-codex") @@ -238,6 +281,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { supports_parallel_tool_calls: true, support_verbosity: false, truncation_policy: TruncationPolicy::Tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), ) } else if slug.starts_with("gpt-5.1") { model_family!( @@ -251,6 +295,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { truncation_policy: TruncationPolicy::Bytes(10_000), shell_type: ConfigShellToolType::ShellCommand, supports_parallel_tool_calls: true, + context_window: Some(CONTEXT_WINDOW_272K), ) } else if slug.starts_with("gpt-5") { model_family!( @@ -260,6 +305,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily { shell_type: ConfigShellToolType::Default, support_verbosity: true, truncation_policy: TruncationPolicy::Bytes(10_000), + context_window: Some(CONTEXT_WINDOW_272K), ) } else { derive_default_model_family(slug) @@ -271,6 +317,8 @@ fn derive_default_model_family(model: &str) -> ModelFamily { slug: model.to_string(), family: model.to_string(), needs_special_apply_patch_instructions: false, + context_window: None, + auto_compact_token_limit: None, supports_reasoning_summaries: false, reasoning_summary_format: ReasoningSummaryFormat::None, supports_parallel_tool_calls: false, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1302b2343..a0b42ddbe 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -550,7 +550,7 @@ impl ChatWidget { fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option { info.model_context_window - .or(self.config.model_context_window) + .or(self.model_family.context_window) .map(|window| { info.last_token_usage .percent_of_context_window_remaining(window) @@ -2024,6 +2024,7 @@ impl ChatWidget { self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), + &self.model_family, total_usage, context_usage, &self.conversation_id, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 126c91f9d..0135abff7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -98,7 +98,7 @@ fn snapshot(percent: f64) -> RateLimitSnapshot { #[test] fn resumed_initial_messages_render_history() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None); let conversation_id = ConversationId::new(); let rollout_file = NamedTempFile::new().unwrap(); @@ -154,7 +154,7 @@ fn resumed_initial_messages_render_history() { /// Entering review mode uses the hint provided by the review request. #[test] fn entered_review_mode_uses_request_hint() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "review-start".into(), @@ -175,7 +175,7 @@ fn entered_review_mode_uses_request_hint() { /// Entering review mode renders the current changes banner when requested. #[test] fn entered_review_mode_defaults_to_current_changes_banner() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "review-start".into(), @@ -195,7 +195,7 @@ fn entered_review_mode_defaults_to_current_changes_banner() { /// the closing banner while clearing review mode state. #[test] fn exited_review_mode_emits_results_and_finishes() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None); let review = ReviewOutputEvent { findings: vec![ReviewFinding { @@ -229,7 +229,7 @@ fn exited_review_mode_emits_results_and_finishes() { /// Exiting review restores the pre-review context window indicator. #[test] fn review_restores_context_window_indicator() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None); let context_window = 13_000; let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline. @@ -278,7 +278,7 @@ fn review_restores_context_window_indicator() { /// Receiving a TokenCount event without usage clears the context indicator. #[test] fn token_count_none_resets_context_indicator() { - let (mut chat, _rx, _ops) = make_chatwidget_manual(); + let (mut chat, _rx, _ops) = make_chatwidget_manual(None); let context_window = 13_000; let pre_compact_tokens = 12_700; @@ -304,7 +304,7 @@ fn token_count_none_resets_context_indicator() { #[test] fn context_indicator_shows_used_tokens_when_window_unknown() { - let (mut chat, _rx, _ops) = make_chatwidget_manual(); + let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("unknown-model")); chat.config.model_context_window = None; let auto_compact_limit = 200_000; @@ -371,7 +371,9 @@ async fn helpers_are_available_and_do_not_panic() { } // --- Helpers for tests that need direct construction and event draining --- -fn make_chatwidget_manual() -> ( +fn make_chatwidget_manual( + model_override: Option<&str>, +) -> ( ChatWidget, tokio::sync::mpsc::UnboundedReceiver, tokio::sync::mpsc::UnboundedReceiver, @@ -379,7 +381,10 @@ fn make_chatwidget_manual() -> ( let (tx_raw, rx) = unbounded_channel::(); let app_event_tx = AppEventSender::new(tx_raw); let (op_tx, op_rx) = unbounded_channel::(); - let cfg = test_config(); + let mut cfg = test_config(); + if let Some(model) = model_override { + cfg.model = model.to_string(); + } let bottom = BottomPane::new(BottomPaneParams { app_event_tx: app_event_tx.clone(), frame_requester: FrameRequester::test_dummy(), @@ -447,7 +452,7 @@ pub(crate) fn make_chatwidget_manual_with_sender() -> ( tokio::sync::mpsc::UnboundedReceiver, tokio::sync::mpsc::UnboundedReceiver, ) { - let (widget, rx, op_rx) = make_chatwidget_manual(); + let (widget, rx, op_rx) = make_chatwidget_manual(None); let app_event_tx = widget.app_event_tx.clone(); (widget, app_event_tx, rx, op_rx) } @@ -543,7 +548,7 @@ fn test_rate_limit_warnings_monthly() { #[test] fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.on_rate_limit_snapshot(Some(RateLimitSnapshot { primary: None, @@ -592,7 +597,7 @@ fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() { #[test] fn rate_limit_snapshot_updates_and_retains_plan_type() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.on_rate_limit_snapshot(Some(RateLimitSnapshot { primary: Some(RateLimitWindow { @@ -645,7 +650,7 @@ fn rate_limit_snapshot_updates_and_retains_plan_type() { #[test] fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { - let (mut chat, _, _) = make_chatwidget_manual(); + let (mut chat, _, _) = make_chatwidget_manual(None); chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); chat.config.model = NUDGE_MODEL_SLUG.to_string(); @@ -661,7 +666,7 @@ fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { #[test] fn rate_limit_switch_prompt_shows_once_per_session() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let (mut chat, _, _) = make_chatwidget_manual(); + let (mut chat, _, _) = make_chatwidget_manual(None); chat.config.model = "gpt-5".to_string(); chat.auth_manager = AuthManager::from_auth_for_testing(auth); @@ -686,7 +691,7 @@ fn rate_limit_switch_prompt_shows_once_per_session() { #[test] fn rate_limit_switch_prompt_respects_hidden_notice() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let (mut chat, _, _) = make_chatwidget_manual(); + let (mut chat, _, _) = make_chatwidget_manual(None); chat.config.model = "gpt-5".to_string(); chat.auth_manager = AuthManager::from_auth_for_testing(auth); chat.config.notices.hide_rate_limit_model_nudge = Some(true); @@ -702,7 +707,7 @@ fn rate_limit_switch_prompt_respects_hidden_notice() { #[test] fn rate_limit_switch_prompt_defers_until_task_complete() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let (mut chat, _, _) = make_chatwidget_manual(); + let (mut chat, _, _) = make_chatwidget_manual(None); chat.config.model = "gpt-5".to_string(); chat.auth_manager = AuthManager::from_auth_for_testing(auth); @@ -723,7 +728,7 @@ fn rate_limit_switch_prompt_defers_until_task_complete() { #[test] fn rate_limit_switch_prompt_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); chat.config.model = "gpt-5".to_string(); @@ -739,7 +744,7 @@ fn rate_limit_switch_prompt_popup_snapshot() { #[test] fn exec_approval_emits_proposed_command_and_decision_history() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Trigger an exec approval request with a short, single-line command let ev = ExecApprovalRequestEvent { @@ -784,7 +789,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() { #[test] fn exec_approval_decision_truncates_multiline_and_long_commands() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Multiline command: modal should show full command, history records decision only let ev_multi = ExecApprovalRequestEvent { @@ -969,7 +974,7 @@ fn get_available_model(chat: &ChatWidget, model: &str) -> ModelPreset { #[test] fn empty_enter_during_task_does_not_queue() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Simulate running task so submissions would normally be queued. chat.bottom_pane.set_task_running(true); @@ -983,7 +988,7 @@ fn empty_enter_during_task_does_not_queue() { #[test] fn alt_up_edits_most_recent_queued_message() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Simulate a running task so messages would normally be queued. chat.bottom_pane.set_task_running(true); @@ -1016,7 +1021,7 @@ fn alt_up_edits_most_recent_queued_message() { /// is queued repeatedly. #[test] fn enqueueing_history_prompt_multiple_times_is_stable() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Submit an initial prompt to seed history. chat.bottom_pane.set_composer_text("repeat me".to_string()); @@ -1042,7 +1047,7 @@ fn enqueueing_history_prompt_multiple_times_is_stable() { #[test] fn streaming_final_answer_keeps_task_running_state() { - let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None); chat.on_task_started(); chat.on_agent_message_delta("Final answer line\n".to_string()); @@ -1072,7 +1077,7 @@ fn streaming_final_answer_keeps_task_running_state() { #[test] fn ctrl_c_shutdown_ignores_caps_lock() { - let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None); chat.handle_key_event(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::CONTROL)); @@ -1084,7 +1089,7 @@ fn ctrl_c_shutdown_ignores_caps_lock() { #[test] fn ctrl_c_cleared_prompt_is_recoverable_via_history() { - let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None); chat.bottom_pane.insert_str("draft message "); chat.bottom_pane @@ -1118,7 +1123,7 @@ fn ctrl_c_cleared_prompt_is_recoverable_via_history() { #[test] fn exec_history_cell_shows_working_then_completed() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Begin command let begin = begin_exec(&mut chat, "call-1", "echo done"); @@ -1148,7 +1153,7 @@ fn exec_history_cell_shows_working_then_completed() { #[test] fn exec_history_cell_shows_working_then_failed() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Begin command let begin = begin_exec(&mut chat, "call-2", "false"); @@ -1172,7 +1177,7 @@ fn exec_history_cell_shows_working_then_failed() { #[test] fn exec_history_shows_unified_exec_startup_commands() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let begin = begin_exec_with_source( &mut chat, @@ -1200,7 +1205,7 @@ fn exec_history_shows_unified_exec_startup_commands() { /// OpenReviewCustomPrompt to the app event channel. #[test] fn review_popup_custom_prompt_action_sends_event() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Open the preset selection popup chat.open_review_popup(); @@ -1225,7 +1230,7 @@ fn review_popup_custom_prompt_action_sends_event() { #[test] fn slash_init_skips_when_project_doc_exists() { - let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None); let tempdir = tempdir().unwrap(); let existing_path = tempdir.path().join(DEFAULT_PROJECT_DOC_FILENAME); std::fs::write(&existing_path, "existing instructions").unwrap(); @@ -1257,7 +1262,7 @@ fn slash_init_skips_when_project_doc_exists() { #[test] fn slash_quit_requests_exit() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.dispatch_command(SlashCommand::Quit); @@ -1266,7 +1271,7 @@ fn slash_quit_requests_exit() { #[test] fn slash_exit_requests_exit() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.dispatch_command(SlashCommand::Exit); @@ -1275,7 +1280,7 @@ fn slash_exit_requests_exit() { #[test] fn slash_resume_opens_picker() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.dispatch_command(SlashCommand::Resume); @@ -1284,7 +1289,7 @@ fn slash_resume_opens_picker() { #[test] fn slash_undo_sends_op() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.dispatch_command(SlashCommand::Undo); @@ -1296,7 +1301,7 @@ fn slash_undo_sends_op() { #[test] fn slash_rollout_displays_current_path() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let rollout_path = PathBuf::from("/tmp/codex-test-rollout.jsonl"); chat.current_rollout_path = Some(rollout_path.clone()); @@ -1313,7 +1318,7 @@ fn slash_rollout_displays_current_path() { #[test] fn slash_rollout_handles_missing_path() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.dispatch_command(SlashCommand::Rollout); @@ -1332,7 +1337,7 @@ fn slash_rollout_handles_missing_path() { #[test] fn undo_success_events_render_info_messages() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "turn-1".to_string(), @@ -1369,7 +1374,7 @@ fn undo_success_events_render_info_messages() { #[test] fn undo_failure_events_render_error_message() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "turn-2".to_string(), @@ -1404,7 +1409,7 @@ fn undo_failure_events_render_error_message() { #[test] fn undo_started_hides_interrupt_hint() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "turn-hint".to_string(), @@ -1424,7 +1429,7 @@ fn undo_started_hides_interrupt_hint() { /// The commit picker shows only commit subjects (no timestamps). #[test] fn review_commit_picker_shows_subjects_without_timestamps() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Open the Review presets parent popup. chat.open_review_popup(); @@ -1486,7 +1491,7 @@ fn review_commit_picker_shows_subjects_without_timestamps() { /// and uses the same text for the user-facing hint. #[test] fn custom_prompt_submit_sends_review_op() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.show_review_custom_prompt(); // Paste prompt text via ChatWidget handler, then submit @@ -1514,7 +1519,7 @@ fn custom_prompt_submit_sends_review_op() { /// Hitting Enter on an empty custom prompt view does not submit. #[test] fn custom_prompt_enter_empty_does_not_send() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.show_review_custom_prompt(); // Enter without any text @@ -1526,7 +1531,7 @@ fn custom_prompt_enter_empty_does_not_send() { #[test] fn view_image_tool_call_adds_history_cell() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let image_path = chat.config.cwd.join("example.png"); chat.handle_codex_event(Event { @@ -1547,7 +1552,7 @@ fn view_image_tool_call_adds_history_cell() { // marker (replacing the spinner) and flushes it into history. #[test] fn interrupt_exec_marks_failed_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Begin a long-running command so we have an active exec cell with a spinner. begin_exec(&mut chat, "call-int", "sleep 1"); @@ -1576,7 +1581,7 @@ fn interrupt_exec_marks_failed_snapshot() { // suggesting the user to tell the model what to do differently and to use /feedback. #[test] fn interrupted_turn_error_message_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Simulate an in-progress task so the widget is in a running state. chat.handle_codex_event(Event { @@ -1607,7 +1612,7 @@ fn interrupted_turn_error_message_snapshot() { /// parent popup, pressing Esc again dismisses all panels (back to normal mode). #[test] fn review_custom_prompt_escape_navigates_back_then_dismisses() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Open the Review presets parent popup. chat.open_review_popup(); @@ -1642,7 +1647,7 @@ fn review_custom_prompt_escape_navigates_back_then_dismisses() { /// parent popup, pressing Esc again dismisses all panels (back to normal mode). #[tokio::test] async fn review_branch_picker_escape_navigates_back_then_dismisses() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Open the Review presets parent popup. chat.open_review_popup(); @@ -1729,7 +1734,7 @@ fn render_bottom_popup(chat: &ChatWidget, width: u16) -> String { #[test] fn model_selection_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.config.model = "gpt-5-codex".to_string(); chat.open_model_popup(); @@ -1740,7 +1745,7 @@ fn model_selection_popup_snapshot() { #[test] fn approvals_selection_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.config.notices.hide_full_access_warning = None; chat.open_approvals_popup(); @@ -1779,7 +1784,7 @@ fn preset_matching_ignores_extra_writable_roots() { #[test] fn full_access_confirmation_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); let preset = builtin_approval_presets() .into_iter() @@ -1794,7 +1799,7 @@ fn full_access_confirmation_popup_snapshot() { #[cfg(target_os = "windows")] #[test] fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); let preset = builtin_approval_presets() .into_iter() @@ -1812,7 +1817,7 @@ fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { #[cfg(target_os = "windows")] #[test] fn startup_prompts_for_windows_sandbox_when_agent_requested() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); set_windows_sandbox_enabled(false); chat.config.forced_auto_mode_downgraded_on_windows = true; @@ -1834,7 +1839,7 @@ fn startup_prompts_for_windows_sandbox_when_agent_requested() { #[test] fn model_reasoning_selection_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); set_chatgpt_auth(&mut chat); chat.config.model = "gpt-5.1-codex-max".to_string(); @@ -1849,7 +1854,7 @@ fn model_reasoning_selection_popup_snapshot() { #[test] fn model_reasoning_selection_popup_extra_high_warning_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); set_chatgpt_auth(&mut chat); chat.config.model = "gpt-5.1-codex-max".to_string(); @@ -1864,7 +1869,7 @@ fn model_reasoning_selection_popup_extra_high_warning_snapshot() { #[test] fn reasoning_popup_shows_extra_high_with_space() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); set_chatgpt_auth(&mut chat); chat.config.model = "gpt-5.1-codex-max".to_string(); @@ -1885,7 +1890,7 @@ fn reasoning_popup_shows_extra_high_with_space() { #[test] fn single_reasoning_option_skips_selection() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let single_effort = vec![ReasoningEffortPreset { effort: ReasoningEffortConfig::High, @@ -1925,7 +1930,7 @@ fn single_reasoning_option_skips_selection() { #[test] fn feedback_selection_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Open the feedback category selection popup via slash command. chat.dispatch_command(SlashCommand::Feedback); @@ -1936,7 +1941,7 @@ fn feedback_selection_popup_snapshot() { #[test] fn feedback_upload_consent_popup_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Open the consent popup directly for a chosen category. chat.open_feedback_consent(crate::app_event::FeedbackCategory::Bug); @@ -1947,7 +1952,7 @@ fn feedback_upload_consent_popup_snapshot() { #[test] fn reasoning_popup_escape_returns_to_model_popup() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.config.model = "gpt-5.1".to_string(); chat.open_model_popup(); @@ -1967,7 +1972,7 @@ fn reasoning_popup_escape_returns_to_model_popup() { #[test] fn exec_history_extends_previous_when_consecutive() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // 1) Start "ls -la" (List) let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la"); @@ -1998,7 +2003,7 @@ fn exec_history_extends_previous_when_consecutive() { #[test] fn user_shell_command_renders_output_not_exploring() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let begin_ls = begin_exec_with_source( &mut chat, @@ -2021,7 +2026,7 @@ fn user_shell_command_renders_output_not_exploring() { #[test] fn disabled_slash_command_while_task_running_snapshot() { // Build a chat widget and simulate an active task - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.bottom_pane.set_task_running(true); // Dispatch a command that is unavailable while a task runs (e.g., /model) @@ -2045,7 +2050,7 @@ fn disabled_slash_command_while_task_running_snapshot() { #[test] fn approval_modal_exec_snapshot() { // Build a chat widget with manual channels to avoid spawning the agent. - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Ensure policy allows surfacing approvals explicitly (not strictly required for direct event). chat.config.approval_policy = AskForApproval::OnRequest; // Inject an exec approval request to display the approval modal. @@ -2100,7 +2105,7 @@ fn approval_modal_exec_snapshot() { // Ensures spacing looks correct when no reason text is provided. #[test] fn approval_modal_exec_without_reason_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.config.approval_policy = AskForApproval::OnRequest; let ev = ExecApprovalRequestEvent { @@ -2139,7 +2144,7 @@ fn approval_modal_exec_without_reason_snapshot() { // Snapshot test: patch approval modal #[test] fn approval_modal_patch_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.config.approval_policy = AskForApproval::OnRequest; // Build a small changeset and a reason/grant_root to exercise the prompt text. @@ -2178,7 +2183,7 @@ fn approval_modal_patch_snapshot() { #[test] fn interrupt_restores_queued_messages_into_composer() { - let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None); // Simulate a running task to enable queuing of user inputs. chat.bottom_pane.set_task_running(true); @@ -2217,7 +2222,7 @@ fn interrupt_restores_queued_messages_into_composer() { #[test] fn interrupt_prepends_queued_messages_before_existing_composer_text() { - let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None); chat.bottom_pane.set_task_running(true); chat.bottom_pane @@ -2255,7 +2260,7 @@ fn interrupt_prepends_queued_messages_before_existing_composer_text() { fn ui_snapshots_small_heights_idle() { use ratatui::Terminal; use ratatui::backend::TestBackend; - let (chat, _rx, _op_rx) = make_chatwidget_manual(); + let (chat, _rx, _op_rx) = make_chatwidget_manual(None); for h in [1u16, 2, 3] { let name = format!("chat_small_idle_h{h}"); let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal"); @@ -2272,7 +2277,7 @@ fn ui_snapshots_small_heights_idle() { fn ui_snapshots_small_heights_task_running() { use ratatui::Terminal; use ratatui::backend::TestBackend; - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Activate status line chat.handle_codex_event(Event { id: "task-1".into(), @@ -2303,7 +2308,7 @@ fn ui_snapshots_small_heights_task_running() { fn status_widget_and_approval_modal_snapshot() { use codex_core::protocol::ExecApprovalRequestEvent; - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Begin a running task so the status indicator would be active. chat.handle_codex_event(Event { id: "task-1".into(), @@ -2356,7 +2361,7 @@ fn status_widget_and_approval_modal_snapshot() { // Ensures the VT100 rendering of the status indicator is stable when active. #[test] fn status_widget_active_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Activate the status indicator by simulating a task start. chat.handle_codex_event(Event { id: "task-1".into(), @@ -2383,7 +2388,7 @@ fn status_widget_active_snapshot() { #[test] fn background_event_updates_status_header() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "bg-1".into(), @@ -2399,7 +2404,7 @@ fn background_event_updates_status_header() { #[test] fn apply_patch_events_emit_history_cells() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // 1) Approval request -> proposed patch summary cell let mut changes = HashMap::new(); @@ -2497,7 +2502,7 @@ fn apply_patch_events_emit_history_cells() { #[test] fn apply_patch_manual_approval_adjusts_header() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let mut proposed_changes = HashMap::new(); proposed_changes.insert( @@ -2546,7 +2551,7 @@ fn apply_patch_manual_approval_adjusts_header() { #[test] fn apply_patch_manual_flow_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let mut proposed_changes = HashMap::new(); proposed_changes.insert( @@ -2599,7 +2604,7 @@ fn apply_patch_manual_flow_snapshot() { #[test] fn apply_patch_approval_sends_op_with_submission_id() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Simulate receiving an approval request with a distinct submission id and call id let mut changes = HashMap::new(); changes.insert( @@ -2638,7 +2643,7 @@ fn apply_patch_approval_sends_op_with_submission_id() { #[test] fn apply_patch_full_flow_integration_like() { - let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None); // 1) Backend requests approval let mut changes = HashMap::new(); @@ -2716,7 +2721,7 @@ fn apply_patch_full_flow_integration_like() { #[test] fn apply_patch_untrusted_shows_approval_modal() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); // Ensure approval policy is untrusted (OnRequest) chat.config.approval_policy = AskForApproval::OnRequest; @@ -2761,7 +2766,7 @@ fn apply_patch_untrusted_shows_approval_modal() { #[test] fn apply_patch_request_shows_diff_summary() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Ensure we are in OnRequest so an approval is surfaced chat.config.approval_policy = AskForApproval::OnRequest; @@ -2827,7 +2832,7 @@ fn apply_patch_request_shows_diff_summary() { #[test] fn plan_update_renders_history_cell() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let update = UpdatePlanArgs { explanation: Some("Adapting plan".to_string()), plan: vec![ @@ -2863,7 +2868,7 @@ fn plan_update_renders_history_cell() { #[test] fn stream_error_updates_status_indicator() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.bottom_pane.set_task_running(true); let msg = "Reconnecting... 2/5"; chat.handle_codex_event(Event { @@ -2888,7 +2893,7 @@ fn stream_error_updates_status_indicator() { #[test] fn warning_event_adds_warning_history_cell() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "sub-1".into(), msg: EventMsg::Warning(WarningEvent { @@ -2907,7 +2912,7 @@ fn warning_event_adds_warning_history_cell() { #[test] fn stream_recovery_restores_previous_status_header() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "task".into(), msg: EventMsg::TaskStarted(TaskStartedEvent { @@ -2940,7 +2945,7 @@ fn stream_recovery_restores_previous_status_header() { #[test] fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Begin turn chat.handle_codex_event(Event { @@ -2994,7 +2999,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { #[test] fn final_reasoning_then_message_without_deltas_are_rendered() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // No deltas; only final reasoning followed by final message. chat.handle_codex_event(Event { @@ -3021,7 +3026,7 @@ fn final_reasoning_then_message_without_deltas_are_rendered() { #[test] fn deltas_then_same_final_message_are_rendered_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Stream some reasoning deltas first. chat.handle_codex_event(Event { @@ -3085,7 +3090,7 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { // then the exec block, another blank line, the status line, a blank line, and the composer. #[test] fn chatwidget_exec_and_status_layout_vt100_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), msg: EventMsg::AgentMessage(AgentMessageEvent { message: "I’m going to search the repo for where “Change Approved” is rendered to update that view.".into() }), @@ -3177,7 +3182,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { // E2E vt100 snapshot for complex markdown with indented and nested fenced code blocks #[test] fn chatwidget_markdown_code_blocks_vt100_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); // Simulate a final agent message via streaming deltas instead of a single message @@ -3268,7 +3273,7 @@ printf 'fenced within fenced\n' #[test] fn chatwidget_tall() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), msg: EventMsg::TaskStarted(TaskStartedEvent { diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 797eded5f..7049d13ff 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -7,6 +7,7 @@ use chrono::DateTime; use chrono::Local; use codex_common::create_config_summary_entries; use codex_core::config::Config; +use codex_core::openai_models::model_family::ModelFamily; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TokenUsage; use codex_protocol::ConversationId; @@ -70,6 +71,7 @@ struct StatusHistoryCell { pub(crate) fn new_status_output( config: &Config, auth_manager: &AuthManager, + model_family: &ModelFamily, total_usage: &TokenUsage, context_usage: Option<&TokenUsage>, session_id: &Option, @@ -81,6 +83,7 @@ pub(crate) fn new_status_output( let card = StatusHistoryCell::new( config, auth_manager, + model_family, total_usage, context_usage, session_id, @@ -97,6 +100,7 @@ impl StatusHistoryCell { fn new( config: &Config, auth_manager: &AuthManager, + model_family: &ModelFamily, total_usage: &TokenUsage, context_usage: Option<&TokenUsage>, session_id: &Option, @@ -119,7 +123,7 @@ impl StatusHistoryCell { let agents_summary = compose_agents_summary(config); let account = compose_account_display(auth_manager, plan_type); let session_id = session_id.as_ref().map(std::string::ToString::to_string); - let context_window = config.model_context_window.and_then(|window| { + let context_window = model_family.context_window.and_then(|window| { context_usage.map(|usage| StatusContextWindowData { percent_remaining: usage.percent_of_context_window_remaining(window), tokens_in_context: usage.tokens_in_context_window(), diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 35989883f..1b16453c4 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -8,6 +8,8 @@ use codex_core::AuthManager; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; +use codex_core::openai_models::model_family::ModelFamily; +use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::RateLimitSnapshot; use codex_core::protocol::RateLimitWindow; @@ -37,6 +39,10 @@ fn test_auth_manager(config: &Config) -> AuthManager { ) } +fn test_model_family(config: &Config) -> ModelFamily { + ModelsManager::construct_model_family_offline(config.model.as_str(), config) +} + fn render_lines(lines: &[Line<'static>]) -> Vec { lines .iter() @@ -124,9 +130,12 @@ fn status_snapshot_includes_reasoning_details() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); + let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -177,9 +186,11 @@ fn status_snapshot_includes_monthly_limit() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -218,9 +229,11 @@ fn status_snapshot_shows_unlimited_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -258,9 +271,11 @@ fn status_snapshot_shows_positive_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -298,9 +313,11 @@ fn status_snapshot_hides_zero_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -336,9 +353,11 @@ fn status_snapshot_hides_when_has_no_credits_flag() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -374,9 +393,11 @@ fn status_card_token_usage_excludes_cached_tokens() { .single() .expect("timestamp"); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -427,9 +448,11 @@ fn status_snapshot_truncates_in_narrow_terminal() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -469,9 +492,11 @@ fn status_snapshot_shows_missing_limits_message() { .single() .expect("timestamp"); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -529,9 +554,11 @@ fn status_snapshot_includes_credits_and_limits() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -577,9 +604,11 @@ fn status_snapshot_shows_empty_limits_message() { .expect("timestamp"); let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -634,9 +663,11 @@ fn status_snapshot_shows_stale_limits_message() { let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let now = captured_at + ChronoDuration::minutes(20); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -695,9 +726,11 @@ fn status_snapshot_cached_limits_hide_credits_without_flag() { let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let now = captured_at + ChronoDuration::minutes(20); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &usage, Some(&usage), &None, @@ -742,9 +775,11 @@ fn status_context_window_uses_last_usage() { .single() .expect("timestamp"); + let model_family = test_model_family(&config); let composite = new_status_output( &config, &auth_manager, + &model_family, &total_usage, Some(&last_usage), &None,