From 8e987a4702eb3ffbd1ade1574f64dcc8ebcd9e30 Mon Sep 17 00:00:00 2001 From: AGSaturn <206668465+AGSaturn@users.noreply.github.com> Date: Wed, 6 May 2026 17:52:10 +0800 Subject: [PATCH] fix: preserve requested model ID casing in registry resolution (#733) Previously, ModelRegistry::resolve() lowercased the requested model name before looking it up in the alias map, and always returned the registry's canonical (lowercase) model ID. This broke third-party API providers that enforce case-sensitive model name matching. Now when the resolved model ID differs from the requested name only in case (eq_ignore_ascii_case), the requested casing is preserved. Closes #729 Co-authored-by: Claude Opus 4.7 --- crates/agent/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 3d2644d1..bea66181 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -227,7 +227,7 @@ impl ModelRegistry { { return ModelResolution { requested: Some(name.to_string()), - resolved: model, + resolved: preserve_requested_model_id_case(model, name), used_fallback: false, fallback_chain, }; @@ -235,7 +235,7 @@ impl ModelRegistry { if let Some(idx) = self.alias_map.get(&normalize(name)) { return ModelResolution { requested: Some(name.to_string()), - resolved: self.models[*idx].clone(), + resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name), used_fallback: false, fallback_chain, }; @@ -283,6 +283,14 @@ fn model_matches(model: &ModelInfo, requested: &str) -> bool { .any(|alias| normalize(alias) == requested) } +fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo { + let requested = requested.trim(); + if model.id.eq_ignore_ascii_case(requested) { + model.id = requested.to_string(); + } + model +} + #[cfg(test)] mod tests { use super::*; @@ -406,4 +414,40 @@ mod tests { assert_eq!(resolved.resolved.provider, ProviderKind::Vllm); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash"); } + + #[test] + fn preserves_requested_model_casing_for_third_party_providers() { + let registry = ModelRegistry::default(); + let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None); + + assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); + assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro"); + } + + #[test] + fn preserves_requested_model_casing_with_provider_hint() { + let registry = ModelRegistry::default(); + let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek)); + + assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); + assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro"); + } + + #[test] + fn preserves_requested_model_casing_without_surrounding_whitespace() { + let registry = ModelRegistry::default(); + let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None); + + assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); + assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro"); + } + + #[test] + fn alias_match_does_not_override_requested_casing() { + let registry = ModelRegistry::default(); + let resolved = registry.resolve(Some("deepseek-reasoner"), None); + + assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); + assert_eq!(resolved.resolved.id, "deepseek-v4-flash"); + } }