diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 2d8518507..a3c6df633 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -137,10 +137,8 @@ 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 - .get_remote_models(config) - .await - .into_iter() - .find(|m| m.slug == model); + .find_remote_model_by_longest_prefix(model, config) + .await; let model = if let Some(remote) = remote { remote } else { @@ -149,6 +147,28 @@ impl ModelsManager { model_info::with_config_overrides(model, config) } + async fn find_remote_model_by_longest_prefix( + &self, + model: &str, + config: &Config, + ) -> Option { + let mut best: Option = None; + for candidate in self.get_remote_models(config).await { + if !model.starts_with(&candidate.slug) { + continue; + } + let is_better_match = if let Some(current) = best.as_ref() { + candidate.slug.len() > current.slug.len() + } else { + true + }; + if is_better_match { + best = Some(candidate); + } + } + best + } + /// Refresh models if the provided ETag differs from the cached ETag. /// /// Uses `Online` strategy to fetch latest models when ETags differ. diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index ed46855f9..202ba412a 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -55,6 +55,69 @@ use wiremock::MockServer; const REMOTE_MODEL_SLUG: &str = "codex-test"; +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = MockServer::start().await; + let generic = test_remote_model_with_policy( + "gpt-5.3", + ModelVisibility::List, + 1_000, + TruncationPolicyConfig::bytes(10_000), + ); + let specific = test_remote_model_with_policy( + "gpt-5.3-codex", + ModelVisibility::List, + 1_000, + TruncationPolicyConfig::bytes(10_000), + ); + let specific = ModelInfo { + display_name: "GPT 5.3 Codex".to_string(), + base_instructions: "use specific prefix".to_string(), + ..specific + }; + let generic = ModelInfo { + display_name: "GPT 5.3".to_string(), + base_instructions: "use generic prefix".to_string(), + ..generic + }; + mount_models_once( + &server, + ModelsResponse { + models: vec![generic.clone(), specific.clone()], + }, + ) + .await; + + let codex_home = TempDir::new()?; + let mut config = load_default_config_for_test(&codex_home).await; + config.features.enable(Feature::RemoteModels); + + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + let manager = ModelsManager::with_provider( + codex_home.path().to_path_buf(), + codex_core::auth::AuthManager::from_auth_for_testing(auth), + provider, + ); + + manager + .list_models(&config, RefreshStrategy::OnlineIfUncached) + .await; + + let model_info = manager.get_model_info("gpt-5.3-codex-test", &config).await; + + assert_eq!(model_info.slug, specific.slug); + assert_eq!(model_info.base_instructions, specific.base_instructions); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { skip_if_no_network!(Ok(()));