From 7b359c9c8e455e5572c5a08c2488dfb5b028c329 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 4 Dec 2025 18:28:03 -0800 Subject: [PATCH] Call models endpoint in models manager (#7616) - Introduce `with_remote_overrides` and update `refresh_available_models` - Put `auth_manager` instead of `auth_mode` on `models_manager` - Remove `ShellType` and `ReasoningLevel` to use already existing structs --- .../codex-api/tests/models_integration.rs | 14 +- codex-rs/core/src/codex.rs | 21 +-- codex-rs/core/src/conversation_manager.rs | 2 +- .../core/src/openai_models/model_family.rs | 80 ++++++++- .../core/src/openai_models/models_manager.rs | 154 +++++++++++++++++- codex-rs/core/src/tools/spec.rs | 17 +- .../core/tests/chat_completions_payload.rs | 5 +- codex-rs/core/tests/chat_completions_sse.rs | 10 +- codex-rs/core/tests/responses_headers.rs | 14 +- codex-rs/core/tests/suite/client.rs | 8 +- codex-rs/protocol/src/openai_models.rs | 61 ++++--- codex-rs/tui/src/chatwidget/tests.rs | 4 +- codex-rs/tui/src/history_cell.rs | 27 ++- 13 files changed, 329 insertions(+), 88 deletions(-) diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 9994fe1d4..3b4077f53 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -5,11 +5,11 @@ use codex_api::provider::RetryConfig; use codex_api::provider::WireApi; use codex_client::ReqwestTransport; use codex_protocol::openai_models::ClientVersion; +use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelsResponse; -use codex_protocol::openai_models::ReasoningLevel; -use codex_protocol::openai_models::ShellType; +use codex_protocol::openai_models::ReasoningEffort; use http::HeaderMap; use http::Method; use wiremock::Mock; @@ -55,13 +55,13 @@ async fn models_client_hits_models_endpoint() { slug: "gpt-test".to_string(), display_name: "gpt-test".to_string(), description: Some("desc".to_string()), - default_reasoning_level: ReasoningLevel::Medium, + default_reasoning_level: ReasoningEffort::Medium, supported_reasoning_levels: vec![ - ReasoningLevel::Low, - ReasoningLevel::Medium, - ReasoningLevel::High, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, ], - shell_type: ShellType::ShellCommand, + shell_type: ConfigShellToolType::ShellCommand, visibility: ModelVisibility::List, minimal_client_version: ClientVersion(0, 1, 0), supported_in_api: true, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 436021a9d..bff038cd6 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2475,6 +2475,7 @@ pub(crate) use tests::make_session_and_context_with_rx; #[cfg(test)] mod tests { use super::*; + use crate::CodexAuth; use crate::config::ConfigOverrides; use crate::config::ConfigToml; use crate::exec::ExecToolCallOutput; @@ -2765,12 +2766,9 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); - let auth_manager = AuthManager::shared( - config.cwd.clone(), - false, - config.cli_auth_credentials_store_mode, - ); - let models_manager = Arc::new(ModelsManager::new(auth_manager.get_auth_mode())); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); let otel_event_manager = otel_event_manager(conversation_id, config.as_ref(), &models_manager); @@ -2801,7 +2799,7 @@ mod tests { rollout: Mutex::new(None), user_shell: default_user_shell(), show_raw_agent_reasoning: config.show_raw_agent_reasoning, - auth_manager: Arc::clone(&auth_manager), + auth_manager: auth_manager.clone(), otel_event_manager: otel_event_manager.clone(), models_manager: models_manager.clone(), tool_approvals: Mutex::new(ApprovalStore::default()), @@ -2847,12 +2845,9 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); - let auth_manager = AuthManager::shared( - config.cwd.clone(), - false, - config.cli_auth_credentials_store_mode, - ); - let models_manager = Arc::new(ModelsManager::new(auth_manager.get_auth_mode())); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); let otel_event_manager = otel_event_manager(conversation_id, config.as_ref(), &models_manager); diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index 22f73dfe1..e527507c1 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -47,7 +47,7 @@ impl ConversationManager { conversations: Arc::new(RwLock::new(HashMap::new())), auth_manager: auth_manager.clone(), session_source, - models_manager: Arc::new(ModelsManager::new(auth_manager.get_auth_mode())), + models_manager: Arc::new(ModelsManager::new(auth_manager)), } } diff --git a/codex-rs/core/src/openai_models/model_family.rs b/codex-rs/core/src/openai_models/model_family.rs index 9d89e1499..6ee18ad9e 100644 --- a/codex-rs/core/src/openai_models/model_family.rs +++ b/codex-rs/core/src/openai_models/model_family.rs @@ -1,11 +1,12 @@ use codex_protocol::config_types::Verbosity; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort; use crate::config::Config; use crate::config::types::ReasoningSummaryFormat; use crate::tools::handlers::apply_patch::ApplyPatchToolType; -use crate::tools::spec::ConfigShellToolType; use crate::truncate::TruncationPolicy; +use codex_protocol::openai_models::ConfigShellToolType; /// The `instructions` field in the payload sent to a model should always start /// with this content. @@ -83,6 +84,15 @@ impl ModelFamily { } self } + pub fn with_remote_overrides(mut self, remote_models: Vec) -> Self { + for model in remote_models { + if model.slug == self.slug { + self.default_reasoning_effort = Some(model.default_reasoning_level); + self.shell_type = model.shell_type; + } + } + self + } } macro_rules! model_family { @@ -275,3 +285,71 @@ fn derive_default_model_family(model: &str) -> ModelFamily { truncation_policy: TruncationPolicy::Bytes(10_000), } } + +#[cfg(test)] +mod tests { + use super::*; + use codex_protocol::openai_models::ClientVersion; + use codex_protocol::openai_models::ModelVisibility; + + fn remote(slug: &str, effort: ReasoningEffort, shell: ConfigShellToolType) -> ModelInfo { + ModelInfo { + slug: slug.to_string(), + display_name: slug.to_string(), + description: Some(format!("{slug} desc")), + default_reasoning_level: effort, + supported_reasoning_levels: vec![effort], + shell_type: shell, + visibility: ModelVisibility::List, + minimal_client_version: ClientVersion(0, 1, 0), + supported_in_api: true, + priority: 1, + } + } + + #[test] + fn remote_overrides_apply_when_slug_matches() { + let family = model_family!("gpt-4o-mini", "gpt-4o-mini"); + assert_ne!(family.default_reasoning_effort, Some(ReasoningEffort::High)); + + let updated = family.with_remote_overrides(vec![ + remote( + "gpt-4o-mini", + ReasoningEffort::High, + ConfigShellToolType::ShellCommand, + ), + remote( + "other-model", + ReasoningEffort::Low, + ConfigShellToolType::UnifiedExec, + ), + ]); + + assert_eq!( + updated.default_reasoning_effort, + Some(ReasoningEffort::High) + ); + assert_eq!(updated.shell_type, ConfigShellToolType::ShellCommand); + } + + #[test] + fn remote_overrides_skip_non_matching_models() { + let family = model_family!( + "codex-mini-latest", + "codex-mini-latest", + shell_type: ConfigShellToolType::Local + ); + + let updated = family.clone().with_remote_overrides(vec![remote( + "other", + ReasoningEffort::High, + ConfigShellToolType::ShellCommand, + )]); + + assert_eq!( + updated.default_reasoning_effort, + family.default_reasoning_effort + ); + assert_eq!(updated.shell_type, family.shell_type); + } +} diff --git a/codex-rs/core/src/openai_models/models_manager.rs b/codex-rs/core/src/openai_models/models_manager.rs index 23124f10d..fd0fea362 100644 --- a/codex-rs/core/src/openai_models/models_manager.rs +++ b/codex-rs/core/src/openai_models/models_manager.rs @@ -1,34 +1,172 @@ -use codex_app_server_protocol::AuthMode; +use codex_api::ModelsClient; +use codex_api::ReqwestTransport; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; +use http::HeaderMap; +use std::sync::Arc; use tokio::sync::RwLock; +use crate::api_bridge::auth_provider_from_auth; +use crate::api_bridge::map_api_error; +use crate::auth::AuthManager; use crate::config::Config; +use crate::default_client::build_reqwest_client; +use crate::error::Result as CoreResult; +use crate::model_provider_info::ModelProviderInfo; use crate::openai_models::model_family::ModelFamily; use crate::openai_models::model_family::find_family_for_model; use crate::openai_models::model_presets::builtin_model_presets; #[derive(Debug)] pub struct ModelsManager { + // todo(aibrahim) merge available_models and model family creation into one struct pub available_models: RwLock>, + pub remote_models: RwLock>, pub etag: String, - pub auth_mode: Option, + pub auth_manager: Arc, } impl ModelsManager { - pub fn new(auth_mode: Option) -> Self { + pub fn new(auth_manager: Arc) -> Self { Self { - available_models: RwLock::new(builtin_model_presets(auth_mode)), + available_models: RwLock::new(builtin_model_presets(auth_manager.get_auth_mode())), + remote_models: RwLock::new(Vec::new()), etag: String::new(), - auth_mode, + auth_manager, } } - pub async fn refresh_available_models(&self) { - let models = builtin_model_presets(self.auth_mode); - *self.available_models.write().await = models; + // do not use this function yet. It's work in progress. + pub async fn refresh_available_models( + &self, + provider: &ModelProviderInfo, + ) -> CoreResult> { + let auth = self.auth_manager.auth(); + let api_provider = provider.to_api_provider(auth.as_ref().map(|auth| auth.mode))?; + let api_auth = auth_provider_from_auth(auth.clone(), provider).await?; + let transport = ReqwestTransport::new(build_reqwest_client()); + let client = ModelsClient::new(transport, api_provider, api_auth); + + let response = client + .list_models(env!("CARGO_PKG_VERSION"), HeaderMap::new()) + .await + .map_err(map_api_error)?; + + let models = response.models; + *self.remote_models.write().await = models.clone(); + { + let mut available_models_guard = self.available_models.write().await; + *available_models_guard = self.build_available_models().await; + } + Ok(models) } pub fn construct_model_family(&self, model: &str, config: &Config) -> ModelFamily { find_family_for_model(model).with_config_overrides(config) } + + async fn build_available_models(&self) -> Vec { + let mut available_models = self.remote_models.read().await.clone(); + available_models.sort_by(|a, b| b.priority.cmp(&a.priority)); + let mut model_presets: Vec = + available_models.into_iter().map(Into::into).collect(); + if let Some(default) = model_presets.first_mut() { + default.is_default = true; + } + model_presets + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CodexAuth; + use crate::model_provider_info::WireApi; + use codex_protocol::openai_models::ModelsResponse; + use serde_json::json; + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + + fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo { + serde_json::from_value(json!({ + "slug": slug, + "display_name": display, + "description": format!("{display} desc"), + "default_reasoning_level": "medium", + "supported_reasoning_levels": ["low", "medium"], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": [0, 1, 0], + "supported_in_api": true, + "priority": priority + })) + .expect("valid model") + } + + fn provider_for(base_url: String) -> ModelProviderInfo { + ModelProviderInfo { + name: "mock".into(), + base_url: Some(base_url), + env_key: None, + env_key_instructions: None, + experimental_bearer_token: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: Some(0), + stream_max_retries: Some(0), + stream_idle_timeout_ms: Some(5_000), + requires_openai_auth: false, + } + } + + #[tokio::test] + async fn refresh_available_models_sorts_and_marks_default() { + let server = MockServer::start().await; + let remote_models = vec![ + remote_model("priority-low", "Low", 1), + remote_model("priority-high", "High", 10), + ]; + let response = ModelsResponse { + models: remote_models.clone(), + }; + Mock::given(method("GET")) + .and(path("/models")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "application/json") + .set_body_json(&response), + ) + .expect(1) + .mount(&server) + .await; + + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let manager = ModelsManager::new(auth_manager); + let provider = provider_for(server.uri()); + + let returned = manager + .refresh_available_models(&provider) + .await + .expect("refresh succeeds"); + + assert_eq!(returned, remote_models); + let cached_remote = manager.remote_models.read().await.clone(); + assert_eq!(cached_remote, remote_models); + + let available = manager.available_models.read().await.clone(); + assert_eq!(available.len(), 2); + assert_eq!(available[0].model, "priority-high"); + assert!( + available[0].is_default, + "highest priority should be default" + ); + assert_eq!(available[1].model, "priority-low"); + assert!(!available[1].is_default); + } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 2c3aa2d44..a36f54a6b 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -8,6 +8,7 @@ use crate::tools::handlers::apply_patch::ApplyPatchToolType; use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool; use crate::tools::handlers::apply_patch::create_apply_patch_json_tool; use crate::tools::registry::ToolRegistryBuilder; +use codex_protocol::openai_models::ConfigShellToolType; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -15,20 +16,6 @@ use serde_json::json; use std::collections::BTreeMap; use std::collections::HashMap; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ConfigShellToolType { - Default, - Local, - UnifiedExec, - /// Do not include a shell tool by default. Useful when using Codex - /// with tools provided exclusively provided by MCP servers. Often used - /// with `--config base_instructions=CUSTOM_INSTRUCTIONS` - /// to customize agent behavior. - Disabled, - /// Takes a command as a single string to be run in the user's default shell. - ShellCommand, -} - #[derive(Debug, Clone)] pub(crate) struct ToolsConfig { pub shell_type: ConfigShellToolType, @@ -58,7 +45,7 @@ impl ToolsConfig { } else if features.enabled(Feature::UnifiedExec) { ConfigShellToolType::UnifiedExec } else { - model_family.shell_type.clone() + model_family.shell_type }; let apply_patch_tool_type = match model_family.apply_patch_tool_type { diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index f24a9d642..db1407455 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use codex_app_server_protocol::AuthMode; +use codex_core::AuthManager; +use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::LocalShellAction; use codex_core::LocalShellExecAction; @@ -71,7 +73,8 @@ async fn run_request(input: Vec) -> Value { let config = Arc::new(config); let conversation_id = ConversationId::new(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index f50f3f2ca..0351263eb 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -1,9 +1,10 @@ use assert_matches::assert_matches; +use codex_core::AuthManager; use codex_core::openai_models::models_manager::ModelsManager; use std::sync::Arc; use tracing_test::traced_test; -use codex_app_server_protocol::AuthMode; +use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::ModelClient; use codex_core::ModelProviderInfo; @@ -71,8 +72,9 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let config = Arc::new(config); let conversation_id = ConversationId::new(); - let auth_mode = AuthMode::ApiKey; - let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let auth_mode = auth_manager.get_auth_mode(); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, @@ -80,7 +82,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(auth_mode), + auth_mode, false, "test".to_string(), ); diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 14264921f..02423f3df 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use codex_app_server_protocol::AuthMode; +use codex_core::AuthManager; +use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::ModelClient; use codex_core::ModelProviderInfo; @@ -63,7 +65,8 @@ async fn responses_stream_includes_subagent_header_on_review() { let conversation_id = ConversationId::new(); let auth_mode = AuthMode::ChatGPT; - let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, @@ -154,7 +157,8 @@ async fn responses_stream_includes_subagent_header_on_other() { let conversation_id = ConversationId::new(); let auth_mode = AuthMode::ChatGPT; - let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( @@ -246,8 +250,8 @@ async fn responses_respects_model_family_overrides_from_config() { let config = Arc::new(config); let conversation_id = ConversationId::new(); - let auth_mode = AuthMode::ChatGPT; - let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, @@ -255,7 +259,7 @@ async fn responses_respects_model_family_overrides_from_config() { model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(auth_mode), + auth_manager.get_auth_mode(), false, "test".to_string(), ); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 1170d13a9..2df1f6db1 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,4 +1,4 @@ -use codex_app_server_protocol::AuthMode; +use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::ConversationManager; @@ -1017,8 +1017,8 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let config = Arc::new(config); let conversation_id = ConversationId::new(); - let auth_mode = AuthMode::ChatGPT; - let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, @@ -1026,7 +1026,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(AuthMode::ChatGPT), + auth_manager.get_auth_mode(), false, "test".to_string(), ); diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 92dcf3e4c..02d50627c 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -86,28 +86,24 @@ pub enum ModelVisibility { None, } -/// Reasoning support level reported by the backend. -#[derive( - Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum ReasoningLevel { - None, - Minimal, - Low, - Medium, - High, - XHigh, -} - /// Shell execution capability for a model. #[derive( - Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display, + Debug, + Serialize, + Deserialize, + Clone, + Copy, + PartialEq, + Eq, + TS, + JsonSchema, + EnumIter, + Display, + Hash, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] -pub enum ShellType { +pub enum ConfigShellToolType { Default, Local, UnifiedExec, @@ -126,9 +122,9 @@ pub struct ModelInfo { pub display_name: String, #[serde(default)] pub description: Option, - pub default_reasoning_level: ReasoningLevel, - pub supported_reasoning_levels: Vec, - pub shell_type: ShellType, + pub default_reasoning_level: ReasoningEffort, + pub supported_reasoning_levels: Vec, + pub shell_type: ConfigShellToolType, #[serde(default = "default_visibility")] pub visibility: ModelVisibility, pub minimal_client_version: ClientVersion, @@ -147,3 +143,28 @@ pub struct ModelsResponse { fn default_visibility() -> ModelVisibility { ModelVisibility::None } + +// convert ModelInfo to ModelPreset +impl From for ModelPreset { + fn from(info: ModelInfo) -> Self { + ModelPreset { + id: info.slug.clone(), + model: info.slug, + display_name: info.display_name, + description: info.description.unwrap_or_default(), + default_reasoning_effort: info.default_reasoning_level, + supported_reasoning_efforts: info + .supported_reasoning_levels + .into_iter() + .map(|level| ReasoningEffortPreset { + effort: level, + // todo: add description for each reasoning effort + description: level.to_string(), + }) + .collect(), + is_default: false, // default is the highest priority available model + upgrade: None, // no upgrade available (todo: think about it) + show_in_picker: info.visibility == ModelVisibility::List, + } + } +} diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index ef1de1fde..eae5f5064 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -393,7 +393,7 @@ fn make_chatwidget_manual() -> ( active_cell: None, config: cfg.clone(), auth_manager: auth_manager.clone(), - models_manager: Arc::new(ModelsManager::new(auth_manager.get_auth_mode())), + models_manager: Arc::new(ModelsManager::new(auth_manager)), session_header: SessionHeader::new(cfg.model), initial_user_message: None, token_info: None, @@ -431,7 +431,7 @@ fn make_chatwidget_manual() -> ( fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); - chat.models_manager = Arc::new(ModelsManager::new(chat.auth_manager.get_auth_mode())); + chat.models_manager = Arc::new(ModelsManager::new(chat.auth_manager.clone())); } pub(crate) fn make_chatwidget_manual_with_sender() -> ( diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 1b8755efd..e70a31ffc 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1513,6 +1513,8 @@ mod tests { use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCall; use crate::exec_cell::ExecCell; + use codex_core::AuthManager; + use codex_core::CodexAuth; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; @@ -1520,7 +1522,6 @@ mod tests { use codex_core::config::types::McpServerTransportConfig; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::McpAuthStatus; - use codex_login::AuthMode; use codex_protocol::parse_command::ParsedCommand; use dirs::home_dir; use pretty_assertions::assert_eq; @@ -2325,7 +2326,9 @@ mod tests { #[test] fn reasoning_summary_block() { let config = test_config(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning**\n\nDetailed reasoning goes here.".to_string(), @@ -2342,7 +2345,9 @@ mod tests { #[test] fn reasoning_summary_block_returns_reasoning_cell_when_feature_disabled() { let config = test_config(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &model_family); @@ -2357,7 +2362,9 @@ mod tests { config.model = "gpt-3.5-turbo".to_string(); config.model_supports_reasoning_summaries = Some(true); config.model_reasoning_summary_format = Some(ReasoningSummaryFormat::Experimental); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); assert_eq!( @@ -2377,7 +2384,9 @@ mod tests { #[test] fn reasoning_summary_block_falls_back_when_header_is_missing() { let config = test_config(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning without closing".to_string(), @@ -2391,7 +2400,9 @@ mod tests { #[test] fn reasoning_summary_block_falls_back_when_summary_is_missing() { let config = test_config(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning without closing**".to_string(), @@ -2413,7 +2424,9 @@ mod tests { #[test] fn reasoning_summary_block_splits_header_and_summary_when_present() { let config = test_config(); - let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let models_manager = Arc::new(ModelsManager::new(auth_manager)); let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level plan**\n\nWe should fix the bug next.".to_string(),