diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index 024ab8ca..09fe09c2 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -16,9 +16,7 @@ use crate::types::{ ToolChoice, ToolDefinition, ToolResultContentBlock, Usage, }; -use super::{ - preflight_message_request, resolve_model_alias, strip_provider_prefix, Provider, ProviderFuture, -}; +use super::{preflight_message_request, resolve_model_alias, Provider, ProviderFuture}; pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1"; pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1"; @@ -219,13 +217,12 @@ impl OpenAiCompatClient { ) -> Result { let original_model = request.model.clone(); let canonical = resolve_model_alias(&request.model); - let downstream_model = strip_provider_prefix(&canonical); let mut request = MessageRequest { stream: false, ..request.clone() }; - request.model = downstream_model; + request.model = canonical; preflight_message_request(&request)?; let response = self.send_with_retry(&request).await?; @@ -278,10 +275,9 @@ impl OpenAiCompatClient { ) -> Result { let original_model = request.model.clone(); let canonical = resolve_model_alias(&request.model); - let downstream_model = strip_provider_prefix(&canonical); let mut streaming_request = request.clone().with_streaming(); - streaming_request.model = downstream_model; + streaming_request.model = canonical; preflight_message_request(&streaming_request)?; let response = self.send_with_retry(&streaming_request).await?; diff --git a/rust/crates/api/tests/openai_compat_integration.rs b/rust/crates/api/tests/openai_compat_integration.rs index 4521ebed..e6edb791 100644 --- a/rust/crates/api/tests/openai_compat_integration.rs +++ b/rust/crates/api/tests/openai_compat_integration.rs @@ -548,12 +548,13 @@ async fn openai_compatible_client_honors_http_proxy_for_requests() { .with_base_url("http://origin.invalid/v1"); let response = client .send_message(&MessageRequest { - model: "gpt-4o".to_string(), + model: "openai/gpt-4.1-mini".to_string(), ..sample_request(false) }) .await .expect("proxy should return the OpenAI-compatible response"); + assert_eq!(response.model, "openai/gpt-4.1-mini"); assert_eq!(response.total_tokens(), 7); let captured = state.lock().await; let request = captured.first().expect("proxy should capture request"); @@ -562,6 +563,8 @@ async fn openai_compatible_client_honors_http_proxy_for_requests() { request.headers.get("authorization").map(String::as_str), Some("Bearer openai-test-key") ); + let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body"); + assert_eq!(body["model"], json!("openai/gpt-4.1-mini")); } #[allow(clippy::await_holding_lock)] diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index 0cd68fe1..4a789a89 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -828,18 +828,10 @@ mod tests { use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; - use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); - fn env_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .expect("env lock") - } - struct EnvVarGuard { key: &'static str, previous: Option, @@ -1320,7 +1312,7 @@ mod tests { #[test] fn latest_session_returns_all_empty_error_when_sessions_exist_but_have_no_messages() { // given — create sessions with 0 messages (empty) - let _env_guard = env_lock(); + let _env_guard = crate::test_env_lock(); let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let isolated_config_home = base.join("config-home"); diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 65d8c18e..543ab88a 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -19663,6 +19663,41 @@ mod dump_manifests_tests { #[cfg(test)] mod alias_resolution_tests { + fn ollama_env_lock() -> std::sync::MutexGuard<'static, ()> { + static LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); + LOCK.get_or_init(|| std::sync::Mutex::new(())) + .lock() + .expect("ollama env lock poisoned") + } + + struct EnvVarGuard { + key: &'static str, + previous: Option, + } + + impl EnvVarGuard { + fn unset(key: &'static str) -> Self { + let previous = std::env::var(key).ok(); + std::env::remove_var(key); + Self { key, previous } + } + + fn set(key: &'static str, value: &str) -> Self { + let previous = std::env::var(key).ok(); + std::env::set_var(key, value); + Self { key, previous } + } + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + match &self.previous { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } + } + use super::{resolve_model_alias_with_config, validate_model_syntax}; #[test] @@ -19684,6 +19719,8 @@ mod alias_resolution_tests { #[test] fn test_alias_resolution_syntax_validation() { + let _guard = ollama_env_lock(); + let _env = EnvVarGuard::unset("OLLAMA_HOST"); // Resolved aliases should pass syntax validation let resolved = resolve_model_alias_with_config("opus"); assert!(validate_model_syntax(&resolved).is_ok()); @@ -19694,6 +19731,8 @@ mod alias_resolution_tests { #[test] fn test_unknown_alias_fails_validation() { + let _guard = ollama_env_lock(); + let _env = EnvVarGuard::unset("OLLAMA_HOST"); // Unknown aliases resolve to themselves let resolved = resolve_model_alias_with_config("unknown-alias"); assert_eq!(resolved, "unknown-alias"); @@ -19713,14 +19752,13 @@ mod alias_resolution_tests { } #[test] fn test_ollama_host_bypasses_model_validation() { - // Safety: test sets and clears env var within the test. - std::env::set_var("OLLAMA_HOST", "http://127.0.0.1:11434"); + let _guard = ollama_env_lock(); + let _env = EnvVarGuard::set("OLLAMA_HOST", "http://127.0.0.1:11434"); // Ollama model names with colons pass assert!(validate_model_syntax("qwen3:8b").is_ok()); assert!(validate_model_syntax("gemma4:e2b").is_ok()); assert!(validate_model_syntax("qwen3.6:27b-nvfp4").is_ok()); // Empty model still rejected assert!(validate_model_syntax("").is_err()); - std::env::remove_var("OLLAMA_HOST"); } }