diff --git a/Cargo.lock b/Cargo.lock index 3cacd114b3..7c656642c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3112,9 +3112,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.33.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e16c5073773ccf057c282be832a59ee53ef5ff98db3aeff7f8314f52ffc196" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ "fnv", "hashbrown 0.16.1", diff --git a/src/agent/attachments.rs b/src/agent/attachments.rs index cb52291286..f0a0e40884 100644 --- a/src/agent/attachments.rs +++ b/src/agent/attachments.rs @@ -43,7 +43,7 @@ pub fn augment_with_attachments( image_parts.push(ContentPart::ImageUrl { image_url: ImageUrl { url: data_url, - detail: None, + detail: Some("auto".to_string()), }, }); } @@ -242,6 +242,26 @@ mod tests { } } + #[test] + fn image_url_includes_detail_auto() { + let mut att = make_attachment(AttachmentKind::Image); + att.mime_type = "image/png".to_string(); + att.data = vec![0x89, 0x50, 0x4E, 0x47]; // fake PNG header + + let result = augment_with_attachments("check", &[att]).unwrap(); + assert_eq!(result.image_parts.len(), 1); + match &result.image_parts[0] { + ContentPart::ImageUrl { image_url } => { + assert_eq!( + image_url.detail.as_deref(), + Some("auto"), + "detail field must be set to 'auto' for provider compatibility" + ); + } + other => panic!("Expected ImageUrl, got: {:?}", other), + } + } + #[test] fn document_with_extracted_text() { let mut att = make_attachment(AttachmentKind::Document); diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 479ce27df3..6ad14ce664 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -285,7 +285,8 @@ fn create_openai_compat_from_registry( let mut builder = openai::Client::builder().api_key(&api_key); if !config.base_url.is_empty() { - builder = builder.base_url(&config.base_url); + let base_url = normalize_openai_base_url(&config.base_url); + builder = builder.base_url(&base_url); } if !extra_headers.is_empty() { builder = builder.http_headers(extra_headers); @@ -707,6 +708,34 @@ pub fn create_gemini_oauth_provider(config: &LlmConfig) -> Result String { + let trimmed = url.trim_end_matches('/'); + if trimmed.to_ascii_lowercase().ends_with("/v1") { + return trimmed.to_string(); + } + match url::Url::parse(trimmed) { + Ok(parsed) if parsed.path().is_empty() || parsed.path() == "/" => { + format!("{trimmed}/v1") + } + _ => trimmed.to_string(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -867,4 +896,59 @@ mod tests { let config = test_llm_config(); assert_eq!(config.cheap_model_name(), None); } + + #[test] + fn test_normalize_openai_base_url_appends_v1_for_bare_hosts() { + assert_eq!( + normalize_openai_base_url("http://localhost:8080"), + "http://localhost:8080/v1" + ); + assert_eq!( + normalize_openai_base_url("http://localhost:8080/"), + "http://localhost:8080/v1" + ); + assert_eq!( + normalize_openai_base_url("https://my-server.example.com"), + "https://my-server.example.com/v1" + ); + } + + #[test] + fn test_normalize_openai_base_url_leaves_v1_alone() { + assert_eq!( + normalize_openai_base_url("http://localhost:8080/v1"), + "http://localhost:8080/v1" + ); + assert_eq!( + normalize_openai_base_url("http://localhost:8080/v1/"), + "http://localhost:8080/v1" + ); + assert_eq!( + normalize_openai_base_url("https://api.openai.com/v1"), + "https://api.openai.com/v1" + ); + // Case-insensitive: /V1 should not get double-suffixed + assert_eq!( + normalize_openai_base_url("http://localhost:8080/V1"), + "http://localhost:8080/V1" + ); + } + + #[test] + fn test_normalize_openai_base_url_preserves_existing_paths() { + // Non-/v1 versioned paths from real providers must stay unchanged + assert_eq!( + normalize_openai_base_url("https://api.z.ai/api/paas/v4"), + "https://api.z.ai/api/paas/v4" + ); + assert_eq!( + normalize_openai_base_url("https://generativelanguage.googleapis.com/v1beta/openai"), + "https://generativelanguage.googleapis.com/v1beta/openai" + ); + // Custom subpaths should also stay unchanged + assert_eq!( + normalize_openai_base_url("https://api.example.com/custom"), + "https://api.example.com/custom" + ); + } }