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.
This commit is contained in:
pash-openai
2026-04-07 17:55:40 -07:00
committed by GitHub
Unverified
parent 600c3e49e0
commit 80ebc80be5
29 changed files with 172 additions and 8 deletions
@@ -9308,6 +9308,13 @@
},
"Model": {
"properties": {
"additionalSpeedTiers": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"availabilityNux": {
"anyOf": [
{
@@ -6111,6 +6111,13 @@
},
"Model": {
"properties": {
"additionalSpeedTiers": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"availabilityNux": {
"anyOf": [
{
@@ -22,6 +22,13 @@
},
"Model": {
"properties": {
"additionalSpeedTiers": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"availabilityNux": {
"anyOf": [
{
@@ -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<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, 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<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, additionalSpeedTiers: Array<string>, isDefault: boolean, };
@@ -1791,6 +1791,8 @@ pub struct Model {
pub input_modalities: Vec<InputModality>,
#[serde(default)]
pub supports_personality: bool,
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
// Only one model should be marked as default.
pub is_default: bool,
}
+1 -1
View File
@@ -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 <feature_name> > 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.
+1
View File
@@ -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,
}
}
@@ -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,
@@ -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,
}
}
@@ -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,
@@ -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,
@@ -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,
+2
View File
@@ -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 {
@@ -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,
+1
View File
@@ -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,
@@ -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,
+1
View File
@@ -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,
+3
View File
@@ -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,
@@ -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(),
+16
View File
@@ -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<String>,
/// 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<String>,
pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
@@ -428,6 +434,7 @@ impl From<ModelInfo> 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<ModelInfo> 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());
}
}
+1
View File
@@ -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,
+5 -2
View File
@@ -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, "<VERSION>")
@@ -9064,7 +9067,7 @@ guardian_approval = true
.collect::<Vec<_>>()
.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 {
+1
View File
@@ -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,
+14 -2
View File
@@ -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<ServiceTier>,
) -> 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
+4
View File
@@ -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::*;
@@ -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<ModelPreset> = 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,
@@ -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,
@@ -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;