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 2d81f62684..eb6ea11f97 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 c58326c6f8..9339b7f69f 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 4023f76b51..dc60c5b770 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 cd99108190..f4cf5a9462 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 2bf04a2655..67653bd1dd 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 69176b0b26..7cbce735a9 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 3466678a6f..fd08098f74 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 4173747616..f38b6fd992 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 bd5c61ce0b..830ab0f783 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 c6b28fa71a..91a8477a92 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 b14267af49..2b50a755e5 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 94f2424db4..9dfc32fc73 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 172738ffd1..2bcd074189 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 9559397b7e..80e725ab8a 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 68c2bc0106..55855b6a0b 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 0200a945c5..8c131b16bc 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 b213e305e4..06dec12b47 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 068d0915b1..90b53216a6 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 b176af7205..fefec50ce7 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 3c633cbd88..3339e674be 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 fc1de7bc76..2ebfb605b9 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 dad10fa766..7584b232a8 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 9f3a4d83dd..0d0c70d116 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 040adc5a8f..053fb0ddba 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 4156082cbb..56803098d2 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 46a4129a45..d5b30bc042 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 8d14d4dda6..f551f159fe 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 00be80d4e0..c5545d59ee 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