diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 070eccaff5..e1462f5431 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -88,7 +88,7 @@ impl ModelsManager { { error!("failed to refresh available models: {err}"); } - let remote_models = self.remote_models(config).await; + let remote_models = self.get_remote_models(config).await; self.build_available_models(remote_models) } @@ -122,7 +122,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(config).await; + let remote_models = self.get_remote_models(config).await; if auth_mode == Some(AuthMode::ChatGPT) { let has_auto_balanced = self .build_available_models(remote_models) @@ -140,7 +140,7 @@ impl ModelsManager { /// Look up model metadata, applying remote overrides and config adjustments. pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo { let remote = self - .remote_models(config) + .get_remote_models(config) .await .into_iter() .find(|m| m.slug == model); @@ -255,8 +255,9 @@ 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); + let mut merged_presets = ModelPreset::merge(remote_presets, existing_presets); + let chatgpt_mode = self.auth_manager.get_auth_mode() == Some(AuthMode::ChatGPT); + merged_presets = ModelPreset::with_auth(merged_presets, chatgpt_mode); let has_default = merged_presets.iter().any(|preset| preset.is_default); if !has_default { @@ -273,40 +274,7 @@ impl ModelsManager { merged_presets } - 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| chatgpt_mode || model.supported_in_api) - .collect() - } - - fn merge_presets( - remote_presets: Vec, - existing_presets: Vec, - ) -> Vec { - if remote_presets.is_empty() { - return existing_presets; - } - - let remote_slugs: HashSet<&str> = remote_presets - .iter() - .map(|preset| preset.model.as_str()) - .collect(); - - let mut merged_presets = remote_presets.clone(); - for mut preset in existing_presets { - if remote_slugs.contains(preset.model.as_str()) { - continue; - } - preset.is_default = false; - merged_presets.push(preset); - } - - merged_presets - } - - async fn remote_models(&self, config: &Config) -> Vec { + async fn get_remote_models(&self, config: &Config) -> Vec { if config.features.enabled(Feature::RemoteModels) { self.remote_models.read().await.clone() } else { @@ -465,7 +433,7 @@ mod tests { .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); - let cached_remote = manager.remote_models(&config).await; + let cached_remote = manager.get_remote_models(&config).await; assert_eq!(cached_remote, remote_models); let available = manager @@ -528,7 +496,7 @@ mod tests { .await .expect("first refresh succeeds"); assert_eq!( - manager.remote_models(&config).await, + manager.get_remote_models(&config).await, remote_models, "remote cache should store fetched models" ); @@ -539,7 +507,7 @@ mod tests { .await .expect("cached refresh succeeds"); assert_eq!( - manager.remote_models(&config).await, + manager.get_remote_models(&config).await, remote_models, "cache path should not mutate stored models" ); @@ -607,7 +575,7 @@ mod tests { .await .expect("second refresh succeeds"); assert_eq!( - manager.remote_models(&config).await, + manager.get_remote_models(&config).await, updated_models, "stale cache should trigger refetch" ); diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 19ec1fcc36..b8ddf18a43 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -12,6 +12,7 @@ path = "src/lib.rs" workspace = true [dependencies] +codex-app-server-protocol = { workspace = true } codex-git = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-image = { workspace = true } diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index dbda4aebaf..896734df23 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::HashSet; use schemars::JsonSchema; use serde::Deserialize; @@ -243,6 +244,43 @@ impl From for ModelPreset { } } +impl ModelPreset { + /// Filter models based on authentication mode. + /// + /// In ChatGPT mode, all models are visible. Otherwise, only API-supported models are shown. + pub fn with_auth(models: Vec, chatgpt_mode: bool) -> Vec { + models + .into_iter() + .filter(|model| chatgpt_mode || model.supported_in_api) + .collect() + } + + /// Merge remote presets with existing presets, preferring remote when slugs match. + /// + /// Remote presets take precedence. Existing presets not in remote are appended with `is_default` set to false. + pub fn merge(remote_presets: Vec, existing_presets: Vec) -> Vec { + if remote_presets.is_empty() { + return existing_presets; + } + + let remote_slugs: HashSet<&str> = remote_presets + .iter() + .map(|preset| preset.model.as_str()) + .collect(); + + let mut merged_presets = remote_presets.clone(); + for mut preset in existing_presets { + if remote_slugs.contains(preset.model.as_str()) { + continue; + } + preset.is_default = false; + merged_presets.push(preset); + } + + merged_presets + } +} + fn reasoning_effort_mapping_from_presets( presets: &[ReasoningEffortPreset], ) -> Option> {