From 80ebc80be5dbe61b300279c0123918275fc145a5 Mon Sep 17 00:00:00 2001 From: pash-openai Date: Tue, 7 Apr 2026 17:55:40 -0700 Subject: [PATCH] Use model metadata for Fast Mode status (#16949) Fast Mode status was still tied to one model name in the TUI and model-list plumbing. This changes the model metadata shape so a model can advertise additional speed tiers, carries that field through the app-server model list, and uses it to decide when to show Fast Mode status. For people using Codex, the behavior is intended to stay the same for existing models. Fast Mode still requires the existing signed-in / feature-gated path; the difference is that the UI can now recognize any model the model list marks as Fast-capable, instead of requiring a new client-side slug check. --- .../codex_app_server_protocol.schemas.json | 7 +++ .../codex_app_server_protocol.v2.schemas.json | 7 +++ .../schema/json/v2/ModelListResponse.json | 7 +++ .../schema/typescript/v2/Model.ts | 2 +- .../app-server-protocol/src/protocol/v2.rs | 2 + codex-rs/app-server/README.md | 2 +- codex-rs/app-server/src/models.rs | 1 + .../app-server/tests/common/models_cache.rs | 1 + .../app-server/tests/suite/v2/model_list.rs | 1 + .../codex-api/tests/models_integration.rs | 1 + codex-rs/core/tests/suite/model_switching.rs | 2 + codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + codex-rs/core/tests/suite/personality.rs | 2 + codex-rs/core/tests/suite/remote_models.rs | 3 + codex-rs/core/tests/suite/rmcp_client.rs | 1 + .../tests/suite/spawn_agent_description.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 1 + codex-rs/models-manager/models.json | 3 + codex-rs/models-manager/src/model_info.rs | 1 + codex-rs/protocol/src/openai_models.rs | 16 +++++ codex-rs/tools/src/agent_tool_tests.rs | 1 + codex-rs/tui/src/app.rs | 7 ++- codex-rs/tui/src/app_server_session.rs | 1 + codex-rs/tui/src/chatwidget.rs | 16 ++++- codex-rs/tui/src/chatwidget/tests.rs | 4 ++ codex-rs/tui/src/chatwidget/tests/helpers.rs | 63 +++++++++++++++++++ .../chatwidget/tests/popups_and_settings.rs | 2 + .../src/chatwidget/tests/status_and_layout.rs | 24 ++++++- ...ader_fast_status_fast_capable_models.snap} | 0 29 files changed, 172 insertions(+), 8 deletions(-) rename codex-rs/tui/src/snapshots/{codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap => codex_tui__app__tests__clear_ui_header_fast_status_fast_capable_models.snap} (100%) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 2d81f6268..eb6ea11f9 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9308,6 +9308,13 @@ }, "Model": { "properties": { + "additionalSpeedTiers": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "availabilityNux": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index c58326c6f..9339b7f69 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6111,6 +6111,13 @@ }, "Model": { "properties": { + "additionalSpeedTiers": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "availabilityNux": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json index 4023f76b5..dc60c5b77 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -22,6 +22,13 @@ }, "Model": { "properties": { + "additionalSpeedTiers": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "availabilityNux": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts index cd9910819..f4cf5a946 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts @@ -7,4 +7,4 @@ import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; import type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; import type { ReasoningEffortOption } from "./ReasoningEffortOption"; -export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; +export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, additionalSpeedTiers: Array, isDefault: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 2bf04a265..67653bd1d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1791,6 +1791,8 @@ pub struct Model { pub input_modalities: Vec, #[serde(default)] pub supports_personality: bool, + #[serde(default)] + pub additional_speed_tiers: Vec, // Only one model should be marked as default. pub is_default: bool, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 69176b0b2..7cbce735a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -172,7 +172,7 @@ Example with notification opt-out: - `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`. - `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`. - `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`. -- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. +- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `plugins`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 3466678a6..fd08098f7 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -42,6 +42,7 @@ fn model_from_preset(preset: ModelPreset) -> Model { default_reasoning_effort: preset.default_reasoning_effort, input_modalities: preset.input_modalities, supports_personality: preset.supports_personality, + additional_speed_tiers: preset.additional_speed_tiers, is_default: preset.is_default, } } diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 417374761..f38b6fd99 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -28,6 +28,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { }, supported_in_api: preset.supported_in_api, priority, + additional_speed_tiers: preset.additional_speed_tiers.clone(), upgrade: preset.upgrade.as_ref().map(Into::into), base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index bd5c61ce0..830ab0f78 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -50,6 +50,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model { // cache report `supports_personality = false`. // todo(sayan): fix, maybe make roundtrip use ModelInfo only supports_personality: false, + additional_speed_tiers: preset.additional_speed_tiers.clone(), is_default: preset.is_default, } } diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index c6b28fa71..91a8477a9 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -75,6 +75,7 @@ async fn models_client_hits_models_endpoint() { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b14267af4..2b50a755e 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -82,6 +82,7 @@ fn test_model_info( used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, @@ -898,6 +899,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 94f2424db..9dfc32fc7 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -335,6 +335,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { visibility: ModelVisibility::List, supported_in_api: true, priority, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 172738ffd..2bcd07418 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -644,6 +644,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages { @@ -760,6 +761,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages { diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 9559397b7..80e725ab8 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -297,6 +297,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, @@ -545,6 +546,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: remote_base.to_string(), model_messages: None, @@ -1027,6 +1029,7 @@ fn test_remote_model_with_policy( used_fallback_model_metadata: false, supports_search_tool: false, priority, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 68c2bc010..55855b6a0 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -405,6 +405,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 0200a945c..8c131b16b 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -67,6 +67,7 @@ fn test_model_info( used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index b213e305e..06dec12b4 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1349,6 +1349,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, + additional_speed_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/models-manager/models.json b/codex-rs/models-manager/models.json index 068d0915b..90b53216a 100644 --- a/codex-rs/models-manager/models.json +++ b/codex-rs/models-manager/models.json @@ -116,6 +116,9 @@ "visibility": "list", "minimal_client_version": "0.98.0", "supported_in_api": true, + "additional_speed_tiers": [ + "fast" + ], "availability_nux": null, "upgrade": null, "priority": 0, diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index b176af720..fefec50ce 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -69,6 +69,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo { visibility: ModelVisibility::None, supported_in_api: true, priority: 99, + additional_speed_tiers: Vec::new(), availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3c633cbd8..3339e674b 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -20,6 +20,7 @@ use crate::config_types::ReasoningSummary; use crate::config_types::Verbosity; const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}"; +pub const SPEED_TIER_FAST: &str = "fast"; /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning #[derive( @@ -132,6 +133,9 @@ pub struct ModelPreset { /// Whether this model supports personality-specific instructions. #[serde(default)] pub supports_personality: bool, + /// Additional speed tiers this model can run with beyond the standard path. + #[serde(default)] + pub additional_speed_tiers: Vec, /// Whether this is the default model for new users. pub is_default: bool, /// recommended upgrade model @@ -252,6 +256,8 @@ pub struct ModelInfo { pub visibility: ModelVisibility, pub supported_in_api: bool, pub priority: i32, + #[serde(default)] + pub additional_speed_tiers: Vec, pub availability_nux: Option, pub upgrade: Option, pub base_instructions: String, @@ -428,6 +434,7 @@ impl From for ModelPreset { .unwrap_or(ReasoningEffort::None), supported_reasoning_efforts: info.supported_reasoning_levels.clone(), supports_personality, + additional_speed_tiers: info.additional_speed_tiers, is_default: false, // default is the highest priority available model upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade { id: upgrade.model.clone(), @@ -449,6 +456,12 @@ impl From for ModelPreset { } impl ModelPreset { + pub fn supports_fast_mode(&self) -> bool { + self.additional_speed_tiers + .iter() + .any(|tier| tier == SPEED_TIER_FAST) + } + /// Filter models based on authentication mode. /// /// In ChatGPT mode, all models are visible. Otherwise, only API-supported models are shown. @@ -527,6 +540,7 @@ mod tests { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + additional_speed_tiers: Vec::new(), availability_nux: None, upgrade: None, base_instructions: "base".to_string(), @@ -772,6 +786,7 @@ mod tests { availability_nux: Some(ModelAvailabilityNux { message: "Try Spark.".to_string(), }), + additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()], ..test_model(/*spec*/ None) }); @@ -781,5 +796,6 @@ mod tests { message: "Try Spark.".to_string(), }) ); + assert!(preset.supports_fast_mode()); } } diff --git a/codex-rs/tools/src/agent_tool_tests.rs b/codex-rs/tools/src/agent_tool_tests.rs index fc1de7bc7..2ebfb605b 100644 --- a/codex-rs/tools/src/agent_tool_tests.rs +++ b/codex-rs/tools/src/agent_tool_tests.rs @@ -17,6 +17,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset { description: "Balanced".to_string(), }], supports_personality: false, + additional_speed_tiers: Vec::new(), is_default: false, upgrade: None, show_in_picker, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dad10fa76..7584b232a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -6220,6 +6220,7 @@ mod tests { use crate::chatwidget::create_initial_user_message; use crate::chatwidget::tests::make_chatwidget_manual_with_sender; use crate::chatwidget::tests::set_chatgpt_auth; + use crate::chatwidget::tests::set_fast_mode_test_catalog; use crate::file_search::FileSearchManager; use crate::history_cell::AgentMessageCell; use crate::history_cell::HistoryCell; @@ -9042,15 +9043,17 @@ guardian_approval = true target_os = "windows", ignore = "snapshot path rendering differs on Windows" )] - async fn clear_ui_header_shows_fast_status_only_for_gpt54() { + async fn clear_ui_header_shows_fast_status_for_fast_capable_models() { let mut app = make_test_app().await; app.config.cwd = PathBuf::from("/tmp/project").abs(); app.chat_widget.set_model("gpt-5.4"); + set_fast_mode_test_catalog(&mut app.chat_widget); app.chat_widget .set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); app.chat_widget .set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast)); set_chatgpt_auth(&mut app.chat_widget); + set_fast_mode_test_catalog(&mut app.chat_widget); let rendered = app .clear_ui_header_lines_with_version(/*width*/ 80, "") @@ -9064,7 +9067,7 @@ guardian_approval = true .collect::>() .join("\n"); - assert_snapshot!("clear_ui_header_fast_status_gpt54_only", rendered); + assert_snapshot!("clear_ui_header_fast_status_fast_capable_models", rendered); } async fn make_test_app() -> App { diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 9f3a4d83d..0d0c70d11 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -821,6 +821,7 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset { }) .collect(), supports_personality: model.supports_personality, + additional_speed_tiers: model.additional_speed_tiers, is_default: model.is_default, upgrade, show_in_picker: !model.hidden, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 040adc5a8..053fb0ddb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -387,7 +387,6 @@ use unicode_segmentation::UnicodeSegmentation; const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally"; const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls"; const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1"; -const FAST_STATUS_MODEL: &str = "gpt-5.4"; const DEFAULT_STATUS_LINE_ITEMS: [&str; 3] = ["model-with-reasoning", "context-remaining", "current-dir"]; // Track information about an in-flight exec command. @@ -9491,7 +9490,7 @@ impl ChatWidget { model: &str, service_tier: Option, ) -> bool { - model == FAST_STATUS_MODEL + self.model_supports_fast_mode(model) && matches!(service_tier, Some(ServiceTier::Fast)) && self.has_chatgpt_account } @@ -9608,6 +9607,19 @@ impl ChatWidget { .unwrap_or(false) } + fn model_supports_fast_mode(&self, model: &str) -> bool { + self.model_catalog + .try_list_models() + .ok() + .and_then(|models| { + models + .into_iter() + .find(|preset| preset.model == model) + .map(|preset| preset.supports_fast_mode()) + }) + .unwrap_or(false) + } + /// Return whether the effective model currently advertises image-input support. /// /// We intentionally default to `true` when model metadata cannot be read so transient catalog diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 4156082cb..56803098d 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -117,7 +117,9 @@ pub(super) use codex_protocol::models::FileSystemPermissions; pub(super) use codex_protocol::models::MessagePhase; pub(super) use codex_protocol::models::NetworkPermissions; pub(super) use codex_protocol::models::PermissionProfile; +pub(super) use codex_protocol::openai_models::ModelInfo; pub(super) use codex_protocol::openai_models::ModelPreset; +pub(super) use codex_protocol::openai_models::ModelsResponse; pub(super) use codex_protocol::openai_models::ReasoningEffortPreset; pub(super) use codex_protocol::openai_models::default_input_modalities; pub(super) use codex_protocol::parse_command::ParsedCommand; @@ -197,6 +199,7 @@ pub(super) use crossterm::event::KeyCode; pub(super) use crossterm::event::KeyEvent; pub(super) use crossterm::event::KeyModifiers; pub(super) use insta::assert_snapshot; +pub(super) use serde_json::json; #[cfg(target_os = "windows")] pub(super) use serial_test::serial; pub(super) use std::collections::BTreeMap; @@ -262,4 +265,5 @@ mod status_command_tests; pub(crate) use helpers::make_chatwidget_manual_with_sender; pub(crate) use helpers::set_chatgpt_auth; +pub(crate) use helpers::set_fast_mode_test_catalog; pub(super) use helpers::*; diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 46a4129a4..d5b30bc04 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -345,6 +345,69 @@ pub(crate) fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.model_catalog = test_model_catalog(&chat.config); } +fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> ModelInfo { + let additional_speed_tiers = if supports_fast_mode { + vec![codex_protocol::openai_models::SPEED_TIER_FAST] + } else { + Vec::new() + }; + serde_json::from_value(json!({ + "slug": slug, + "display_name": slug, + "description": format!("{slug} description"), + "default_reasoning_level": "medium", + "supported_reasoning_levels": [{"effort": "medium", "description": "medium"}], + "shell_type": "shell_command", + "visibility": "list", + "supported_in_api": true, + "priority": priority, + "additional_speed_tiers": additional_speed_tiers, + "availability_nux": null, + "upgrade": null, + "base_instructions": "base instructions", + "supports_reasoning_summaries": false, + "default_reasoning_summary": "none", + "support_verbosity": false, + "default_verbosity": null, + "apply_patch_tool_type": null, + "truncation_policy": {"mode": "bytes", "limit": 10_000}, + "supports_parallel_tool_calls": false, + "supports_image_detail_original": false, + "context_window": 272_000, + "experimental_supported_tools": [], + })) + .expect("valid model info") +} + +pub(crate) fn set_fast_mode_test_catalog(chat: &mut ChatWidget) { + let models: Vec = ModelsResponse { + models: vec![ + test_model_info( + "gpt-5.4", /*priority*/ 0, /*supports_fast_mode*/ true, + ), + test_model_info( + "gpt-5.3-codex", + /*priority*/ 1, + /*supports_fast_mode*/ false, + ), + ], + } + .models + .into_iter() + .map(Into::into) + .collect(); + + chat.model_catalog = Arc::new(ModelCatalog::new( + models, + CollaborationModesConfig { + default_mode_request_user_input: chat + .config + .features + .enabled(Feature::DefaultModeRequestUserInput), + }, + )); +} + pub(crate) async fn make_chatwidget_manual_with_sender() -> ( ChatWidget, AppEventSender, diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 8d14d4dda..f551f159f 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1592,6 +1592,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() { description: "medium".to_string(), }], supports_personality: false, + additional_speed_tiers: Vec::new(), is_default: false, upgrade: None, show_in_picker, @@ -1714,6 +1715,7 @@ async fn single_reasoning_option_skips_selection() { default_reasoning_effort: ReasoningEffortConfig::High, supported_reasoning_efforts: single_effort, supports_personality: false, + additional_speed_tiers: Vec::new(), is_default: false, upgrade: None, show_in_picker: true, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 00be80d4e..c5545d59e 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -574,20 +574,28 @@ async fn commentary_completion_restores_status_indicator_before_exec_begin() { #[tokio::test] async fn fast_status_indicator_requires_chatgpt_auth() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); chat.set_service_tier(Some(ServiceTier::Fast)); assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); set_chatgpt_auth(&mut chat); + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); assert!(chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); } #[tokio::test] -async fn fast_status_indicator_is_hidden_for_non_gpt54_model() { +async fn fast_status_indicator_is_hidden_for_models_without_fast_support() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; + set_fast_mode_test_catalog(&mut chat); + assert!(!get_available_model(&chat, "gpt-5.3-codex").supports_fast_mode()); chat.set_service_tier(Some(ServiceTier::Fast)); set_chatgpt_auth(&mut chat); + set_fast_mode_test_catalog(&mut chat); + assert!(!get_available_model(&chat, "gpt-5.3-codex").supports_fast_mode()); assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); } @@ -595,7 +603,11 @@ async fn fast_status_indicator_is_hidden_for_non_gpt54_model() { #[tokio::test] async fn fast_status_indicator_is_hidden_when_fast_mode_is_off() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); set_chatgpt_auth(&mut chat); + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); } @@ -946,8 +958,10 @@ async fn status_line_fast_mode_footer_snapshot() { } #[tokio::test] -async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() { +async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); chat.config.cwd = test_project_path().abs(); chat.config.tui_status_line = Some(vec![ "model-with-reasoning".to_string(), @@ -957,6 +971,8 @@ async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() { chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); chat.set_service_tier(Some(ServiceTier::Fast)); set_chatgpt_auth(&mut chat); + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); chat.refresh_status_line(); let test_cwd = test_path_display("/tmp/project"); @@ -1051,6 +1067,8 @@ async fn status_line_model_with_reasoning_fast_footer_snapshot() { use ratatui::backend::TestBackend; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); chat.show_welcome_banner = false; chat.config.cwd = test_project_path().abs(); chat.config.tui_status_line = Some(vec![ @@ -1061,6 +1079,8 @@ async fn status_line_model_with_reasoning_fast_footer_snapshot() { chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); chat.set_service_tier(Some(ServiceTier::Fast)); set_chatgpt_auth(&mut chat); + set_fast_mode_test_catalog(&mut chat); + assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode()); chat.refresh_status_line(); let width = 80; diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_fast_capable_models.snap similarity index 100% rename from codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap rename to codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_fast_capable_models.snap