diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 8794f54af..35424d822 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1048,6 +1048,7 @@ dependencies = [ "codex-app-server-protocol", "codex-core", "codex-protocol", + "once_cell", "serde", "toml", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 12b0fe9cd..c50c69aa3 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -122,8 +122,8 @@ eventsource-stream = "0.2.3" futures = { version = "0.3", default-features = false } http = "1.3.1" icu_decimal = "2.1" -icu_provider = { version = "2.1", features = ["sync"] } icu_locale_core = "2.1" +icu_provider = { version = "2.1", features = ["sync"] } ignore = "0.4.23" image = { version = "^0.25.8", default-features = false } indexmap = "2.12.0" @@ -140,6 +140,7 @@ mime_guess = "2.0.5" multimap = "0.10.0" notify = "8.2.0" nucleo-matcher = "0.3.1" +once_cell = "1" openssl-sys = "*" opentelemetry = "0.30.0" opentelemetry-appender-tracing = "0.30.0" @@ -278,8 +279,8 @@ opt-level = 0 [patch.crates-io] # Uncomment to debug local changes. # ratatui = { path = "../../ratatui" } -ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" } crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" } +ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" } # Uncomment to debug local changes. # rmcp = { path = "../../rust-sdk/crates/rmcp" } 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 c807a6591..8b17185f4 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -93,10 +93,6 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> { display_name: "gpt-5.1".to_string(), description: "Broad world knowledge with strong general reasoning.".to_string(), supported_reasoning_efforts: vec![ - ReasoningEffortOption { - reasoning_effort: ReasoningEffort::Minimal, - description: "Fastest responses with little reasoning".to_string(), - }, ReasoningEffortOption { reasoning_effort: ReasoningEffort::Low, description: "Balances speed with some reasoning; useful for straightforward \ diff --git a/codex-rs/common/Cargo.toml b/codex-rs/common/Cargo.toml index d8f30cc09..285d56b99 100644 --- a/codex-rs/common/Cargo.toml +++ b/codex-rs/common/Cargo.toml @@ -8,9 +8,10 @@ workspace = true [dependencies] clap = { workspace = true, features = ["derive", "wrap_help"], optional = true } +codex-app-server-protocol = { workspace = true } codex-core = { workspace = true } codex-protocol = { workspace = true } -codex-app-server-protocol = { workspace = true } +once_cell = { workspace = true } serde = { workspace = true, optional = true } toml = { workspace = true, optional = true } diff --git a/codex-rs/common/src/model_presets.rs b/codex-rs/common/src/model_presets.rs index b642486f1..9921f969a 100644 --- a/codex-rs/common/src/model_presets.rs +++ b/codex-rs/common/src/model_presets.rs @@ -1,5 +1,8 @@ +use std::collections::HashMap; + use codex_app_server_protocol::AuthMode; use codex_core::protocol_config_types::ReasoningEffort; +use once_cell::sync::Lazy; /// A reasoning effort option that can be surfaced for a model. #[derive(Debug, Clone, Copy)] @@ -10,8 +13,14 @@ pub struct ReasoningEffortPreset { pub description: &'static str, } +#[derive(Debug, Clone)] +pub struct ModelUpgrade { + pub id: &'static str, + pub reasoning_effort_mapping: Option>, +} + /// Metadata describing a Codex-supported model. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct ModelPreset { /// Stable identifier for the preset. pub id: &'static str, @@ -28,162 +37,172 @@ pub struct ModelPreset { /// Whether this is the default model for new users. pub is_default: bool, /// recommended upgrade model - pub recommended_upgrade_model: Option<&'static str>, + pub upgrade: Option, } -const PRESETS: &[ModelPreset] = &[ - ModelPreset { - id: "gpt-5.1-codex", - model: "gpt-5.1-codex", - display_name: "gpt-5.1-codex", - description: "Optimized for codex.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Fastest responses with limited reasoning", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Dynamically adjusts reasoning based on the task", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: true, - recommended_upgrade_model: None, - }, - ModelPreset { - id: "gpt-5.1-codex-mini", - model: "gpt-5.1-codex-mini", - display_name: "gpt-5.1-codex-mini", - description: "Optimized for codex. Cheaper, faster, but less capable.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Dynamically adjusts reasoning based on the task", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: false, - recommended_upgrade_model: None, - }, - ModelPreset { - id: "gpt-5.1", - model: "gpt-5.1", - display_name: "gpt-5.1", - description: "Broad world knowledge with strong general reasoning.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Minimal, - description: "Fastest responses with little reasoning", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Balances speed with some reasoning; useful for straightforward queries and short explanations", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: false, - recommended_upgrade_model: None, - }, - // Deprecated models. - ModelPreset { - id: "gpt-5-codex", - model: "gpt-5-codex", - display_name: "gpt-5-codex", - description: "Optimized for codex.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Fastest responses with limited reasoning", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Dynamically adjusts reasoning based on the task", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: false, - recommended_upgrade_model: Some("gpt-5.1-codex"), - }, - ModelPreset { - id: "gpt-5-codex-mini", - model: "gpt-5-codex-mini", - display_name: "gpt-5-codex-mini", - description: "Optimized for codex. Cheaper, faster, but less capable.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Dynamically adjusts reasoning based on the task", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: false, - recommended_upgrade_model: Some("gpt-5.1-codex-mini"), - }, - ModelPreset { - id: "gpt-5", - model: "gpt-5", - display_name: "gpt-5", - description: "Broad world knowledge with strong general reasoning.", - default_reasoning_effort: ReasoningEffort::Medium, - supported_reasoning_efforts: &[ - ReasoningEffortPreset { - effort: ReasoningEffort::Minimal, - description: "Fastest responses with little reasoning", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Balances speed with some reasoning; useful for straightforward queries and short explanations", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks", - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems", - }, - ], - is_default: false, - recommended_upgrade_model: Some("gpt-5.1"), - }, -]; +static PRESETS: Lazy> = Lazy::new(|| { + vec![ + ModelPreset { + id: "gpt-5.1-codex", + model: "gpt-5.1-codex", + display_name: "gpt-5.1-codex", + description: "Optimized for codex.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Fastest responses with limited reasoning", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Dynamically adjusts reasoning based on the task", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: true, + upgrade: None, + }, + ModelPreset { + id: "gpt-5.1-codex-mini", + model: "gpt-5.1-codex-mini", + display_name: "gpt-5.1-codex-mini", + description: "Optimized for codex. Cheaper, faster, but less capable.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Dynamically adjusts reasoning based on the task", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: false, + upgrade: None, + }, + ModelPreset { + id: "gpt-5.1", + model: "gpt-5.1", + display_name: "gpt-5.1", + description: "Broad world knowledge with strong general reasoning.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Balances speed with some reasoning; useful for straightforward queries and short explanations", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: false, + upgrade: None, + }, + // Deprecated models. + ModelPreset { + id: "gpt-5-codex", + model: "gpt-5-codex", + display_name: "gpt-5-codex", + description: "Optimized for codex.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Fastest responses with limited reasoning", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Dynamically adjusts reasoning based on the task", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: false, + upgrade: Some(ModelUpgrade { + id: "gpt-5.1-codex", + reasoning_effort_mapping: None, + }), + }, + ModelPreset { + id: "gpt-5-codex-mini", + model: "gpt-5-codex-mini", + display_name: "gpt-5-codex-mini", + description: "Optimized for codex. Cheaper, faster, but less capable.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Dynamically adjusts reasoning based on the task", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: false, + upgrade: Some(ModelUpgrade { + id: "gpt-5.1-codex-mini", + reasoning_effort_mapping: None, + }), + }, + ModelPreset { + id: "gpt-5", + model: "gpt-5", + display_name: "gpt-5", + description: "Broad world knowledge with strong general reasoning.", + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: &[ + ReasoningEffortPreset { + effort: ReasoningEffort::Minimal, + description: "Fastest responses with little reasoning", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Balances speed with some reasoning; useful for straightforward queries and short explanations", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks", + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems", + }, + ], + is_default: false, + upgrade: Some(ModelUpgrade { + id: "gpt-5.1", + reasoning_effort_mapping: Some(HashMap::from([( + ReasoningEffort::Minimal, + ReasoningEffort::Low, + )])), + }), + }, + ] +}); pub fn builtin_model_presets(_auth_mode: Option) -> Vec { // leave auth mode for later use PRESETS .iter() - .filter(|preset| preset.recommended_upgrade_model.is_none()) - .copied() + .filter(|preset| preset.upgrade.is_none()) + .cloned() .collect() } -pub fn all_model_presets() -> &'static [ModelPreset] { - PRESETS +pub fn all_model_presets() -> &'static Vec { + &PRESETS } #[cfg(test)] diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 8f950314d..a947e7d13 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -19,10 +19,12 @@ use ts_rs::TS; JsonSchema, TS, EnumIter, + Hash, )] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum ReasoningEffort { + None, Minimal, Low, #[default] diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 42967a7c5..817445c59 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -17,6 +17,7 @@ use crate::tui; use crate::tui::TuiEvent; use crate::update_action::UpdateAction; use codex_ansi_escape::ansi_escape_line; +use codex_common::model_presets::ModelUpgrade; use codex_common::model_presets::all_model_presets; use codex_core::AuthManager; use codex_core::ConversationManager; @@ -64,7 +65,7 @@ fn should_show_model_migration_prompt( all_model_presets() .iter() - .filter(|preset| preset.recommended_upgrade_model.is_some()) + .filter(|preset| preset.upgrade.is_some()) .any(|preset| preset.model == current_model) } @@ -73,38 +74,59 @@ async fn handle_model_migration_prompt_if_needed( config: &mut Config, app_event_tx: &AppEventSender, ) -> Option { - let target_model = all_model_presets() + let upgrade = all_model_presets() .iter() .find(|preset| preset.model == config.model) - .and_then(|preset| preset.recommended_upgrade_model) - .unwrap_or(&config.model) - .to_string(); - let hide_prompt_flag = config.notices.hide_gpt5_1_migration_prompt; - if !should_show_model_migration_prompt(&config.model, &target_model, hide_prompt_flag) { - return None; - } + .and_then(|preset| preset.upgrade.as_ref()); - match run_model_migration_prompt(tui).await { - ModelMigrationOutcome::Accepted => { - app_event_tx.send(AppEvent::PersistModelMigrationPromptAcknowledged { - migration_config: "hide_gpt5_1_migration_prompt".to_string(), - }); - config.model = target_model.clone(); - if let Some(family) = find_family_for_model(&target_model) { - config.model_family = family; - } - app_event_tx.send(AppEvent::UpdateModel(target_model.clone())); - app_event_tx.send(AppEvent::PersistModelSelection { - model: target_model, - effort: config.model_reasoning_effort, - }); + if let Some(ModelUpgrade { + id: target_model, + reasoning_effort_mapping, + }) = upgrade + { + let target_model = target_model.to_string(); + let hide_prompt_flag = config.notices.hide_gpt5_1_migration_prompt; + if !should_show_model_migration_prompt(&config.model, &target_model, hide_prompt_flag) { + return None; } - ModelMigrationOutcome::Exit => { - return Some(AppExitInfo { - token_usage: TokenUsage::default(), - conversation_id: None, - update_action: None, - }); + + match run_model_migration_prompt(tui).await { + ModelMigrationOutcome::Accepted => { + app_event_tx.send(AppEvent::PersistModelMigrationPromptAcknowledged { + migration_config: "hide_gpt5_1_migration_prompt".to_string(), + }); + config.model = target_model.to_string(); + if let Some(family) = find_family_for_model(&target_model) { + config.model_family = family; + } + + let mapped_effort = if let Some(reasoning_effort_mapping) = reasoning_effort_mapping + && let Some(reasoning_effort) = config.model_reasoning_effort + { + reasoning_effort_mapping + .get(&reasoning_effort) + .cloned() + .or(config.model_reasoning_effort) + } else { + config.model_reasoning_effort + }; + + config.model_reasoning_effort = mapped_effort; + + app_event_tx.send(AppEvent::UpdateModel(target_model.clone())); + app_event_tx.send(AppEvent::UpdateReasoningEffort(mapped_effort)); + app_event_tx.send(AppEvent::PersistModelSelection { + model: target_model.clone(), + effort: mapped_effort, + }); + } + ModelMigrationOutcome::Exit => { + return Some(AppExitInfo { + token_usage: TokenUsage::default(), + conversation_id: None, + update_action: None, + }); + } } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index ac0995caa..4b51a7bdb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1836,11 +1836,12 @@ impl ChatWidget { Some(preset.description.to_string()) }; let is_current = preset.model == current_model; - let preset_for_action = preset; - let single_supported_effort = preset_for_action.supported_reasoning_efforts.len() == 1; + let single_supported_effort = preset.supported_reasoning_efforts.len() == 1; + let preset_for_action = preset.clone(); let actions: Vec = vec![Box::new(move |tx| { + let preset_for_event = preset_for_action.clone(); tx.send(AppEvent::OpenReasoningPopup { - model: preset_for_action, + model: preset_for_event, }); })]; items.push(SelectionItem { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 7c6f93699..217e0fb30 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1533,7 +1533,7 @@ fn single_reasoning_option_skips_selection() { default_reasoning_effort: ReasoningEffortConfig::High, supported_reasoning_efforts: &SINGLE_EFFORT, is_default: false, - recommended_upgrade_model: None, + upgrade: None, }; chat.open_reasoning_popup(preset); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index a9abd42f6..bb451f5a5 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -712,6 +712,7 @@ impl SessionHeaderHistoryCell { ReasoningEffortConfig::Low => "low", ReasoningEffortConfig::Medium => "medium", ReasoningEffortConfig::High => "high", + ReasoningEffortConfig::None => "none", }) } } diff --git a/codex-rs/utils/git/Cargo.toml b/codex-rs/utils/git/Cargo.toml index 072587bdc..5fed0aecf 100644 --- a/codex-rs/utils/git/Cargo.toml +++ b/codex-rs/utils/git/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "codex-git" -version.workspace = true edition.workspace = true +name = "codex-git" readme = "README.md" +version.workspace = true [lints] workspace = true [dependencies] -once_cell = "1" +once_cell = { workspace = true } regex = "1" schemars = { workspace = true } serde = { workspace = true, features = ["derive"] }