From 34b20e3083cd04bce467b53a822b99a994004fd3 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Tue, 16 Jun 2026 20:40:40 -0400 Subject: [PATCH] fix(providers/openai): preserve custom API path in base_url derivation (#9649) Signed-off-by: Douwe M Osinga Co-authored-by: Douwe M Osinga --- crates/goose/src/providers/openai.rs | 45 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index 2a57848a7..1b4d29e4d 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -475,13 +475,21 @@ impl OpenAiProvider { let normalized = stripped.trim_end_matches('/'); if normalized.is_empty() { "v1/chat/completions".to_string() - } else if normalized == "v1" || normalized.ends_with("/v1") { + } else if normalized.ends_with("chat/completions") { + stripped.to_string() + } else if Self::ends_with_version_segment(normalized) { format!("{}/chat/completions", normalized) } else { - stripped.to_string() + format!("{}/v1/chat/completions", normalized) } } + fn ends_with_version_segment(path: &str) -> bool { + let last = path.rsplit('/').next().unwrap_or(path); + last.strip_prefix('v') + .is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())) + } + fn normalize_base_path(base_path: &str) -> String { if let Some(path) = base_path.strip_prefix('/') { format!("/{}", path.trim_end_matches('/')) @@ -1280,6 +1288,39 @@ mod tests { assert!(!r.has_v1); } + #[test] + fn derive_base_path_not_removing_api_path() { + let r = OpenAiProvider::derive_base_path("https://opencode.ai/zen/go"); + assert_eq!(r, "https://opencode.ai/zen/go/v1/chat/completions"); + } + + #[test] + fn derive_base_path_should_support_v1() { + let r = OpenAiProvider::derive_base_path("https://opencode.ai/zen/go/v1"); + assert_eq!(r, "https://opencode.ai/zen/go/v1/chat/completions"); + } + + #[test] + fn derive_base_path_should_support_no_base_path() { + let r = OpenAiProvider::derive_base_path("https://opencode.ai/"); + assert_eq!(r, "https://opencode.ai/v1/chat/completions"); + } + + #[test] + fn derive_base_path_preserves_non_v1_version_prefix() { + // Zhipu's default base_url is https://open.bigmodel.cn/api/paas/v4 and + // from_custom_config passes url.path() ("/api/paas/v4") here. The + // existing /api/paas/v4 version must not gain an extra /v1 segment. + let r = OpenAiProvider::derive_base_path("/api/paas/v4"); + assert_eq!(r, "api/paas/v4/chat/completions"); + } + + #[test] + fn derive_base_path_does_not_treat_v_word_as_version() { + let r = OpenAiProvider::derive_base_path("/api/voice"); + assert_eq!(r, "api/voice/v1/chat/completions"); + } + #[test] fn parse_base_url_preserves_query_params() { let r = OpenAiProvider::parse_base_url("https://gw.example.com/v1?api-version=2024-02-01")