From 52055403118242ca7732f07d4829acc751e2af44 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Dhodda <123675218+aravind4219@users.noreply.github.com> Date: Mon, 20 Apr 2026 06:45:31 -0500 Subject: [PATCH] fix: append /chat/completions for prefixed v1 base URLs (#8521) --- crates/goose/src/providers/openai.rs | 70 +++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index aad6ddd071..1ab2b1c52f 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -187,12 +187,7 @@ impl OpenAiProvider { let base_path = if let Some(ref explicit_path) = config.base_path { explicit_path.trim_start_matches('/').to_string() } else { - let url_path = url.path().trim_start_matches('/').to_string(); - if url_path.is_empty() || url_path == "v1" || url_path == "v1/" { - "v1/chat/completions".to_string() - } else { - url_path - } + Self::derive_base_path(url.path()) }; let timeout_secs = config.timeout_seconds.unwrap_or(600); @@ -241,6 +236,19 @@ impl OpenAiProvider { }) } + // Derive a base path from the raw URL path + fn derive_base_path(url_path: &str) -> String { + let stripped = url_path.trim_start_matches('/'); + let normalized = stripped.trim_end_matches('/'); + if normalized.is_empty() { + "v1/chat/completions".to_string() + } else if normalized == "v1" || normalized.ends_with("/v1") { + format!("{}/chat/completions", normalized) + } else { + stripped.to_string() + } + } + fn normalize_base_path(base_path: &str) -> String { if let Some(path) = base_path.strip_prefix('/') { format!("/{}", path.trim_end_matches('/')) @@ -849,4 +857,54 @@ mod tests { let models_path = OpenAiProvider::map_base_path("/custom/path", "models", "v1/models"); assert_eq!(models_path, "/v1/models"); } + + #[test] + fn derive_base_path_empty_path_gives_default_endpoint() { + assert_eq!(OpenAiProvider::derive_base_path("/"), "v1/chat/completions"); + } + + #[test] + fn derive_base_path_bare_v1_gives_chat_completions() { + assert_eq!( + OpenAiProvider::derive_base_path("/v1"), + "v1/chat/completions" + ); + } + + #[test] + fn derive_base_path_v1_with_trailing_slash() { + assert_eq!( + OpenAiProvider::derive_base_path("/v1/"), + "v1/chat/completions" + ); + } + + #[test] + fn derive_base_path_prefixed_v1_appends_chat_completions() { + assert_eq!( + OpenAiProvider::derive_base_path("/zen/go/v1"), + "zen/go/v1/chat/completions" + ); + } + + #[test] + fn derive_base_path_prefixed_v1_with_trailing_slash() { + assert_eq!( + OpenAiProvider::derive_base_path("/zen/go/v1/"), + "zen/go/v1/chat/completions" + ); + } + + #[test] + fn derive_base_path_full_chat_completions_url_unchanged() { + assert_eq!( + OpenAiProvider::derive_base_path("/openai/v1/chat/completions"), + "openai/v1/chat/completions" + ); + } + + #[test] + fn derive_base_path_non_v1_prefix_unchanged() { + assert_eq!(OpenAiProvider::derive_base_path("/anthropic"), "anthropic"); + } }