diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index d7cbd34..2312741 100644 --- a/src/providers/compatible.rs +++ b/src/providers/compatible.rs @@ -67,13 +67,42 @@ impl OpenAiCompatibleProvider { } } + fn path_ends_with(&self, suffix: &str) -> bool { + if let Ok(url) = reqwest::Url::parse(&self.base_url) { + return url.path().trim_end_matches('/').ends_with(suffix); + } + + self.base_url.trim_end_matches('/').ends_with(suffix) + } + + fn has_explicit_api_path(&self) -> bool { + let Ok(url) = reqwest::Url::parse(&self.base_url) else { + return false; + }; + + let path = url.path().trim_end_matches('/'); + !path.is_empty() && path != "/" + } + /// Build the full URL for responses API, detecting if base_url already includes the path. fn responses_url(&self) -> String { - // If base_url already contains "responses", use it as-is - if self.base_url.contains("responses") { - self.base_url.clone() + if self.path_ends_with("/responses") { + return self.base_url.clone(); + } + + let normalized_base = self.base_url.trim_end_matches('/'); + + // If chat endpoint is explicitly configured, derive sibling responses endpoint. + if let Some(prefix) = normalized_base.strip_suffix("/chat/completions") { + return format!("{prefix}/responses"); + } + + // If an explicit API path already exists (e.g. /v1, /openai, /api/coding/v3), + // append responses directly to avoid duplicate /v1 segments. + if self.has_explicit_api_path() { + format!("{normalized_base}/responses") } else { - format!("{}/v1/responses", self.base_url) + format!("{normalized_base}/v1/responses") } } } @@ -663,6 +692,47 @@ mod tests { ); } + #[test] + fn responses_url_requires_exact_suffix_match() { + let p = make_provider( + "custom", + "https://my-api.example.com/api/v2/responses-proxy", + None, + ); + assert_eq!( + p.responses_url(), + "https://my-api.example.com/api/v2/responses-proxy/responses" + ); + } + + #[test] + fn responses_url_derives_from_chat_endpoint() { + let p = make_provider( + "custom", + "https://my-api.example.com/api/v2/chat/completions", + None, + ); + assert_eq!( + p.responses_url(), + "https://my-api.example.com/api/v2/responses" + ); + } + + #[test] + fn responses_url_base_with_v1_no_duplicate() { + let p = make_provider("test", "https://api.example.com/v1", None); + assert_eq!(p.responses_url(), "https://api.example.com/v1/responses"); + } + + #[test] + fn responses_url_non_v1_api_path_uses_raw_suffix() { + let p = make_provider("test", "https://api.example.com/api/coding/v3", None); + assert_eq!( + p.responses_url(), + "https://api.example.com/api/coding/v3/responses" + ); + } + #[test] fn chat_completions_url_without_v1() { // Provider configured without /v1 in base URL