From 774bd9e432fa2e0f4e059e97648cf92216912e19 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 17 Dec 2025 16:12:35 -0800 Subject: [PATCH] feat: model picker (#8209) # External (non-OpenAI) Pull Request Requirements Before opening this Pull Request, please read the dedicated "Contributing" markdown file or your PR may be closed: https://github.com/openai/codex/blob/main/docs/contributing.md If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes. Include a link to a bug report or enhancement request. --- .../app-server/tests/common/models_cache.rs | 1 + .../app-server/tests/suite/v2/model_list.rs | 51 +++++++- .../core/src/openai_models/model_family.rs | 14 +++ .../core/src/openai_models/model_presets.rs | 73 ++++++++--- .../core/src/openai_models/models_manager.rs | 52 +++++--- codex-rs/core/tests/suite/client.rs | 4 +- .../core/tests/suite/compact_resume_fork.rs | 5 - codex-rs/core/tests/suite/list_models.rs | 113 ++++++++---------- codex-rs/core/tests/suite/remote_models.rs | 2 +- codex-rs/protocol/src/openai_models.rs | 3 + codex-rs/tui/src/chatwidget.rs | 4 +- codex-rs/tui/src/chatwidget/tests.rs | 3 +- codex-rs/tui2/src/chatwidget.rs | 4 +- codex-rs/tui2/src/chatwidget/tests.rs | 3 +- 14 files changed, 222 insertions(+), 110 deletions(-) diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index f1ff8c5a3..a65ea4b48 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -42,6 +42,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { } } +// todo(aibrahim): fix the priorities to be the opposite here. /// Write a models_cache.json file to the codex home directory. /// This prevents ModelsManager from making network requests to refresh models. /// The cache will be treated as fresh (within TTL) and used instead of fetching from the network. 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 a7f116b94..916282416 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -176,6 +176,33 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> { default_reasoning_effort: ReasoningEffort::Medium, is_default: false, }, + Model { + id: "caribou".to_string(), + model: "caribou".to_string(), + display_name: "caribou".to_string(), + description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(), + supported_reasoning_efforts: vec![ + ReasoningEffortOption { + reasoning_effort: ReasoningEffort::Low, + description: "Fast responses with lighter reasoning".to_string(), + }, + ReasoningEffortOption { + reasoning_effort: ReasoningEffort::Medium, + description: "Balances speed and reasoning depth for everyday tasks" + .to_string(), + }, + ReasoningEffortOption { + reasoning_effort: ReasoningEffort::High, + description: "Greater reasoning depth for complex problems".to_string(), + }, + ReasoningEffortOption { + reasoning_effort: ReasoningEffort::XHigh, + description: "Extra high reasoning depth for complex problems".to_string(), + }, + ], + default_reasoning_effort: ReasoningEffort::Medium, + is_default: false, + }, ]; assert_eq!(items, expected_models); @@ -299,7 +326,29 @@ async fn list_models_pagination_works() -> Result<()> { assert_eq!(fifth_items.len(), 1); assert_eq!(fifth_items[0].id, "gpt-5.1-codex-max"); - assert!(fifth_cursor.is_none()); + let sixth_cursor = fifth_cursor.ok_or_else(|| anyhow!("cursor for sixth page"))?; + + let sixth_request = mcp + .send_list_models_request(ModelListParams { + limit: Some(1), + cursor: Some(sixth_cursor.clone()), + }) + .await?; + + let sixth_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(sixth_request)), + ) + .await??; + + let ModelListResponse { + data: sixth_items, + next_cursor: sixth_cursor, + } = to_response::(sixth_response)?; + + assert_eq!(sixth_items.len(), 1); + assert_eq!(sixth_items[0].id, "caribou"); + assert!(sixth_cursor.is_none()); Ok(()) } diff --git a/codex-rs/core/src/openai_models/model_family.rs b/codex-rs/core/src/openai_models/model_family.rs index 77ec88b98..b107e3a06 100644 --- a/codex-rs/core/src/openai_models/model_family.rs +++ b/codex-rs/core/src/openai_models/model_family.rs @@ -294,6 +294,20 @@ pub(super) fn find_family_for_model(slug: &str) -> ModelFamily { ) // Production models. + } else if slug.starts_with("caribou") { + // Same as gpt-5.1-codex-max. + model_family!( + slug, slug, + supports_reasoning_summaries: true, + reasoning_summary_format: ReasoningSummaryFormat::Experimental, + base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: true, + support_verbosity: false, + truncation_policy: TruncationPolicy::Tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + ) } else if slug.starts_with("gpt-5.1-codex-max") { model_family!( slug, slug, diff --git a/codex-rs/core/src/openai_models/model_presets.rs b/codex-rs/core/src/openai_models/model_presets.rs index ef29cef8b..2dcd68e29 100644 --- a/codex-rs/core/src/openai_models/model_presets.rs +++ b/codex-rs/core/src/openai_models/model_presets.rs @@ -11,6 +11,35 @@ pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str = static PRESETS: Lazy> = Lazy::new(|| { vec![ + ModelPreset { + id: "caribou".to_string(), + model: "caribou".to_string(), + display_name: "caribou".to_string(), + description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(), + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Fast responses with lighter reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Balances speed and reasoning depth for everyday tasks".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Greater reasoning depth for complex problems".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::XHigh, + description: "Extra high reasoning depth for complex problems".to_string(), + }, + ], + is_default: true, + upgrade: None, + show_in_picker: true, + supported_in_api: false, + }, ModelPreset { id: "gpt-5.1-codex-max".to_string(), model: "gpt-5.1-codex-max".to_string(), @@ -35,9 +64,14 @@ static PRESETS: Lazy> = Lazy::new(|| { description: "Extra high reasoning depth for complex problems".to_string(), }, ], - is_default: true, - upgrade: None, + is_default: false, + upgrade: Some(ModelUpgrade { + id: "caribou".to_string(), + reasoning_effort_mapping: None, + migration_config_key: "caribou".to_string(), + }), show_in_picker: true, + supported_in_api: true, }, ModelPreset { id: "gpt-5.1-codex".to_string(), @@ -62,11 +96,12 @@ static PRESETS: Lazy> = Lazy::new(|| { ], is_default: false, upgrade: Some(ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), + id: "caribou".to_string(), reasoning_effort_mapping: None, - migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(), + migration_config_key: "caribou".to_string(), }), show_in_picker: true, + supported_in_api: true, }, ModelPreset { id: "gpt-5.1-codex-mini".to_string(), @@ -86,12 +121,9 @@ static PRESETS: Lazy> = Lazy::new(|| { }, ], is_default: false, - upgrade: Some(ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), - reasoning_effort_mapping: None, - migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(), - }), + upgrade: None, show_in_picker: true, + supported_in_api: true, }, ModelPreset { id: "gpt-5.2".to_string(), @@ -118,8 +150,13 @@ static PRESETS: Lazy> = Lazy::new(|| { }, ], is_default: false, - upgrade: None, + upgrade: Some(ModelUpgrade { + id: "caribou".to_string(), + reasoning_effort_mapping: None, + migration_config_key: "caribou".to_string(), + }), show_in_picker: true, + supported_in_api: true, }, ModelPreset { id: "gpt-5.1".to_string(), @@ -143,11 +180,12 @@ static PRESETS: Lazy> = Lazy::new(|| { ], is_default: false, upgrade: Some(ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), + id: "caribou".to_string(), reasoning_effort_mapping: None, - migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(), + migration_config_key: "caribou".to_string(), }), show_in_picker: true, + supported_in_api: true, }, // Deprecated models. ModelPreset { @@ -172,11 +210,12 @@ static PRESETS: Lazy> = Lazy::new(|| { ], is_default: false, upgrade: Some(ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), + id: "caribou".to_string(), reasoning_effort_mapping: None, - migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(), + migration_config_key: "caribou".to_string(), }), show_in_picker: false, + supported_in_api: true, }, ModelPreset { id: "gpt-5-codex-mini".to_string(), @@ -201,6 +240,7 @@ static PRESETS: Lazy> = Lazy::new(|| { migration_config_key: HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG.to_string(), }), show_in_picker: false, + supported_in_api: true, }, ModelPreset { id: "gpt-5".to_string(), @@ -228,11 +268,12 @@ static PRESETS: Lazy> = Lazy::new(|| { ], is_default: false, upgrade: Some(ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), + id: "caribou".to_string(), reasoning_effort_mapping: None, - migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(), + migration_config_key: "caribou".to_string(), }), show_in_picker: false, + supported_in_api: true, }, ] }); diff --git a/codex-rs/core/src/openai_models/models_manager.rs b/codex-rs/core/src/openai_models/models_manager.rs index cf7147154..ccf67b5e0 100644 --- a/codex-rs/core/src/openai_models/models_manager.rs +++ b/codex-rs/core/src/openai_models/models_manager.rs @@ -29,7 +29,8 @@ use crate::openai_models::model_presets::builtin_model_presets; const MODEL_CACHE_FILE: &str = "models_cache.json"; const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300); -const OPENAI_DEFAULT_MODEL: &str = "gpt-5.1-codex-max"; +const OPENAI_DEFAULT_API_MODEL: &str = "gpt-5.1-codex-max"; +const OPENAI_DEFAULT_CHATGPT_MODEL: &str = "caribou"; const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced"; /// Coordinates remote model discovery plus cached metadata on disk. @@ -110,12 +111,12 @@ impl ModelsManager { if let Err(err) = self.refresh_available_models(config).await { error!("failed to refresh available models: {err}"); } - let remote_models = self.remote_models.read().await.clone(); + let remote_models = self.remote_models(config).await; self.build_available_models(remote_models) } - pub fn try_list_models(&self) -> Result, TryLockError> { - let remote_models = self.remote_models.try_read()?.clone(); + pub fn try_list_models(&self, config: &Config) -> Result, TryLockError> { + let remote_models = self.try_get_remote_models(config)?; Ok(self.build_available_models(remote_models)) } @@ -126,7 +127,7 @@ impl ModelsManager { /// Look up the requested model family while applying remote metadata overrides. pub async fn construct_model_family(&self, model: &str, config: &Config) -> ModelFamily { Self::find_family_for_model(model) - .with_remote_overrides(self.remote_models.read().await.clone()) + .with_remote_overrides(self.remote_models(config).await) .with_config_overrides(config) } @@ -139,7 +140,7 @@ impl ModelsManager { } // if codex-auto-balanced exists & signed in with chatgpt mode, return it, otherwise return the default model let auth_mode = self.auth_manager.get_auth_mode(); - let remote_models = self.remote_models.read().await.clone(); + let remote_models = self.remote_models(config).await; if auth_mode == Some(AuthMode::ChatGPT) && self .build_available_models(remote_models) @@ -147,13 +148,15 @@ impl ModelsManager { .any(|m| m.model == CODEX_AUTO_BALANCED_MODEL) { return CODEX_AUTO_BALANCED_MODEL.to_string(); + } else if auth_mode == Some(AuthMode::ChatGPT) { + return OPENAI_DEFAULT_CHATGPT_MODEL.to_string(); } - OPENAI_DEFAULT_MODEL.to_string() + OPENAI_DEFAULT_API_MODEL.to_string() } #[cfg(any(test, feature = "test-support"))] pub fn get_model_offline(model: Option<&str>) -> String { - model.unwrap_or(OPENAI_DEFAULT_MODEL).to_string() + model.unwrap_or(OPENAI_DEFAULT_CHATGPT_MODEL).to_string() } #[cfg(any(test, feature = "test-support"))] @@ -217,7 +220,7 @@ impl ModelsManager { let remote_presets: Vec = remote_models.into_iter().map(Into::into).collect(); let existing_presets = self.local_models.clone(); let mut merged_presets = Self::merge_presets(remote_presets, existing_presets); - merged_presets = Self::filter_visible_models(merged_presets); + merged_presets = self.filter_visible_models(merged_presets); let has_default = merged_presets.iter().any(|preset| preset.is_default); if let Some(default) = merged_presets.first_mut() @@ -229,10 +232,11 @@ impl ModelsManager { merged_presets } - fn filter_visible_models(models: Vec) -> Vec { + fn filter_visible_models(&self, models: Vec) -> Vec { + let chatgpt_mode = self.auth_manager.get_auth_mode() == Some(AuthMode::ChatGPT); models .into_iter() - .filter(|model| model.show_in_picker) + .filter(|model| model.show_in_picker && (chatgpt_mode || model.supported_in_api)) .collect() } @@ -261,6 +265,22 @@ impl ModelsManager { merged_presets } + async fn remote_models(&self, config: &Config) -> Vec { + if config.features.enabled(Feature::RemoteModels) { + self.remote_models.read().await.clone() + } else { + Vec::new() + } + } + + fn try_get_remote_models(&self, config: &Config) -> Result, TryLockError> { + if config.features.enabled(Feature::RemoteModels) { + Ok(self.remote_models.try_read()?.clone()) + } else { + Ok(Vec::new()) + } + } + fn cache_path(&self) -> PathBuf { self.codex_home.join(MODEL_CACHE_FILE) } @@ -393,7 +413,7 @@ mod tests { .refresh_available_models(&config) .await .expect("refresh succeeds"); - let cached_remote = manager.remote_models.read().await.clone(); + let cached_remote = manager.remote_models(&config).await; assert_eq!(cached_remote, remote_models); let available = manager.list_models(&config).await; @@ -455,7 +475,7 @@ mod tests { .await .expect("first refresh succeeds"); assert_eq!( - *manager.remote_models.read().await, + manager.remote_models(&config).await, remote_models, "remote cache should store fetched models" ); @@ -466,7 +486,7 @@ mod tests { .await .expect("cached refresh succeeds"); assert_eq!( - *manager.remote_models.read().await, + manager.remote_models(&config).await, remote_models, "cache path should not mutate stored models" ); @@ -537,7 +557,7 @@ mod tests { .await .expect("second refresh succeeds"); assert_eq!( - *manager.remote_models.read().await, + manager.remote_models(&config).await, updated_models, "stale cache should trigger refetch" ); @@ -602,7 +622,7 @@ mod tests { .expect("second refresh succeeds"); let available = manager - .try_list_models() + .try_list_models(&config) .expect("models should be available"); assert!( available.iter().any(|preset| preset.model == "remote-new"), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5b4a9f757..42a06d3f3 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -762,7 +762,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_default_effort_in_request() -> anyhow::Result<()> { +async fn includes_no_effort_in_request() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = MockServer::start().await; @@ -791,7 +791,7 @@ async fn includes_default_effort_in_request() -> anyhow::Result<()> { .get("reasoning") .and_then(|t| t.get("effort")) .and_then(|v| v.as_str()), - Some("medium") + None ); Ok(()) diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index e627bd350..188e38da1 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -276,7 +276,6 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { - "effort": "medium", "summary": "auto" }, "store": false, @@ -346,7 +345,6 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { - "effort": "medium", "summary": "auto" }, "store": false, @@ -407,7 +405,6 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { - "effort": "medium", "summary": "auto" }, "store": false, @@ -488,7 +485,6 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { - "effort": "medium", "summary": "auto" }, "store": false, @@ -569,7 +565,6 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "tool_choice": "auto", "parallel_tool_calls": false, "reasoning": { - "effort": "medium", "summary": "auto" }, "store": false, diff --git a/codex-rs/core/tests/suite/list_models.rs b/codex-rs/core/tests/suite/list_models.rs index 041c844c2..720e68573 100644 --- a/codex-rs/core/tests/suite/list_models.rs +++ b/codex-rs/core/tests/suite/list_models.rs @@ -52,8 +52,11 @@ fn expected_models_for_api_key() -> Vec { } fn expected_models_for_chatgpt() -> Vec { + let mut gpt_5_1_codex_max = gpt_5_1_codex_max(); + gpt_5_1_codex_max.is_default = false; vec![ - gpt_5_1_codex_max(), + caribou(), + gpt_5_1_codex_max, gpt_5_1_codex(), gpt_5_1_codex_mini(), gpt_5_2(), @@ -61,6 +64,38 @@ fn expected_models_for_chatgpt() -> Vec { ] } +fn caribou() -> ModelPreset { + ModelPreset { + id: "caribou".to_string(), + model: "caribou".to_string(), + display_name: "caribou".to_string(), + description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(), + default_reasoning_effort: ReasoningEffort::Medium, + supported_reasoning_efforts: vec![ + effort( + ReasoningEffort::Low, + "Fast responses with lighter reasoning", + ), + effort( + ReasoningEffort::Medium, + "Balances speed and reasoning depth for everyday tasks", + ), + effort( + ReasoningEffort::High, + "Greater reasoning depth for complex problems", + ), + effort( + ReasoningEffort::XHigh, + "Extra high reasoning depth for complex problems", + ), + ], + is_default: true, + upgrade: None, + show_in_picker: true, + supported_in_api: false, + } +} + fn gpt_5_1_codex_max() -> ModelPreset { ModelPreset { id: "gpt-5.1-codex-max".to_string(), @@ -87,8 +122,9 @@ fn gpt_5_1_codex_max() -> ModelPreset { ), ], is_default: true, - upgrade: None, + upgrade: Some(caribou_upgrade()), show_in_picker: true, + supported_in_api: true, } } @@ -114,15 +150,9 @@ fn gpt_5_1_codex() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt_5_1_codex_max_upgrade( - "gpt-5.1-codex", - vec![ - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ], - )), + upgrade: Some(caribou_upgrade()), show_in_picker: true, + supported_in_api: true, } } @@ -144,11 +174,9 @@ fn gpt_5_1_codex_mini() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt_5_1_codex_max_upgrade( - "gpt-5.1-codex-mini", - vec![ReasoningEffort::Medium, ReasoningEffort::High], - )), + upgrade: None, show_in_picker: true, + supported_in_api: true, } } @@ -180,8 +208,9 @@ fn gpt_5_2() -> ModelPreset { ), ], is_default: false, - upgrade: None, + upgrade: Some(caribou_upgrade()), show_in_picker: true, + supported_in_api: true, } } @@ -207,59 +236,17 @@ fn gpt_5_1() -> ModelPreset { ), ], is_default: false, - upgrade: Some(gpt_5_1_codex_max_upgrade( - "gpt-5.1", - vec![ - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ], - )), + upgrade: Some(caribou_upgrade()), show_in_picker: true, + supported_in_api: true, } } -fn gpt_5_1_codex_max_upgrade( - migration_config_key: &str, - supported_efforts: Vec, -) -> codex_protocol::openai_models::ModelUpgrade { - use std::collections::HashMap; - - fn nearest_effort(effort: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort { - supported - .iter() - .min_by_key(|candidate| (effort_rank(effort) - effort_rank(**candidate)).abs()) - .copied() - .unwrap_or(ReasoningEffort::Low) - } - - fn effort_rank(effort: ReasoningEffort) -> i32 { - match effort { - ReasoningEffort::None => 0, - ReasoningEffort::Minimal => 1, - ReasoningEffort::Low => 2, - ReasoningEffort::Medium => 3, - ReasoningEffort::High => 4, - ReasoningEffort::XHigh => 5, - } - } - - let mut mapping = HashMap::new(); - for effort in [ - ReasoningEffort::None, - ReasoningEffort::Minimal, - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ReasoningEffort::XHigh, - ] { - mapping.insert(effort, nearest_effort(effort, &supported_efforts)); - } - +fn caribou_upgrade() -> codex_protocol::openai_models::ModelUpgrade { codex_protocol::openai_models::ModelUpgrade { - id: "gpt-5.1-codex-max".to_string(), - reasoning_effort_mapping: Some(mapping), - migration_config_key: migration_config_key.to_string(), + id: "caribou".to_string(), + reasoning_effort_mapping: None, + migration_config_key: "caribou".to_string(), } } diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 7179f78d0..fc54f900e 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -388,7 +388,7 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { ); let selected = manager.get_model(&None, &config).await; - assert_eq!(selected, "gpt-5.1-codex-max"); + assert_eq!(selected, "caribou"); let available = manager.list_models(&config).await; assert!( diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 5afaad15c..9b4f89134 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -75,6 +75,8 @@ pub struct ModelPreset { pub upgrade: Option, /// Whether this preset should appear in the picker UI. pub show_in_picker: bool, + /// whether this model is supported in the api + pub supported_in_api: bool, } /// Visibility of a model in the picker or APIs. @@ -216,6 +218,7 @@ impl From for ModelPreset { migration_config_key: info.slug.clone(), }), show_in_picker: info.visibility == ModelVisibility::List, + supported_in_api: info.supported_in_api, } } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a7bf4e3fa..79415cd52 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2191,7 +2191,7 @@ impl ChatWidget { } fn lower_cost_preset(&self) -> Option { - let models = self.models_manager.try_list_models().ok()?; + let models = self.models_manager.try_list_models(&self.config).ok()?; models .iter() .find(|preset| preset.model == NUDGE_MODEL_SLUG) @@ -2300,7 +2300,7 @@ impl ChatWidget { let current_model = self.model_family.get_model_slug().to_string(); let presets: Vec = // todo(aibrahim): make this async function - match self.models_manager.try_list_models() { + match self.models_manager.try_list_models(&self.config) { Ok(models) => models, Err(_) => { self.add_info_message( diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 07b2be040..3d055c9f7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -927,7 +927,7 @@ fn active_blob(chat: &ChatWidget) -> String { fn get_available_model(chat: &ChatWidget, model: &str) -> ModelPreset { let models = chat .models_manager - .try_list_models() + .try_list_models(&chat.config) .expect("models lock available"); models .iter() @@ -2014,6 +2014,7 @@ fn single_reasoning_option_skips_selection() { is_default: false, upgrade: None, show_in_picker: true, + supported_in_api: true, }; chat.open_reasoning_popup(preset); diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 71a3014d1..62d026d61 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -2100,7 +2100,7 @@ impl ChatWidget { } fn lower_cost_preset(&self) -> Option { - let models = self.models_manager.try_list_models().ok()?; + let models = self.models_manager.try_list_models(&self.config).ok()?; models .iter() .find(|preset| preset.model == NUDGE_MODEL_SLUG) @@ -2209,7 +2209,7 @@ impl ChatWidget { let current_model = self.model_family.get_model_slug().to_string(); let presets: Vec = // todo(aibrahim): make this async function - match self.models_manager.try_list_models() { + match self.models_manager.try_list_models(&self.config) { Ok(models) => models, Err(_) => { self.add_info_message( diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index 15517cea2..bff3ac5a0 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -925,7 +925,7 @@ fn active_blob(chat: &ChatWidget) -> String { fn get_available_model(chat: &ChatWidget, model: &str) -> ModelPreset { let models = chat .models_manager - .try_list_models() + .try_list_models(&chat.config) .expect("models lock available"); models .iter() @@ -1906,6 +1906,7 @@ fn single_reasoning_option_skips_selection() { is_default: false, upgrade: None, show_in_picker: true, + supported_in_api: true, }; chat.open_reasoning_popup(preset);