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