Watch
1
0
Fork
You've already forked souveraine
0

bifrost: a base url with a path is the complete api root

Appending /v1 by testing only the last path segment doubled Google's
/v1beta/openai into /v1beta/openai/v1, where a POST hangs with zero
bytes and GET /models still answers 200. Only a bare authority now
gets the default; /v1, /v4 and /zen/go/v1 roots pass through.
This commit is contained in:
Fimeg 2026-08-18 14:34:16 -04:00
commit f32f00c6e3

View file

@ -528,15 +528,15 @@ enum ErrorClass {
Permanent, Permanent,
} }
/// True if the URL's final path segment is a version tag like `v1`, `v4`. /// True if the base URL carries a path past the authority. Such a base is the
/// Used to avoid appending `/v1` to an already-versioned base URL. /// complete API root; only a bare host gets the OpenAI default `/v1`.
fn last_segment_is_version(url: &str) -> bool { ///
match url.rsplit('/').next() { /// Measured 2026-08-18: testing only the last segment for `v<digits>` appended
Some(seg) if seg.len() >= 2 => { /// `/v1` to Google's `/v1beta/openai`, and a POST to the doubled path hangs
seg.starts_with('v') && seg[1..].chars().all(|c| c.is_ascii_digit()) /// (40s, zero bytes) while `GET /models` returns 200 on both — so the mangled
} /// base looks healthy on a listing.
_ => false, fn has_own_path(url: &str) -> bool {
} reqwest::Url::parse(url).is_ok_and(|u| u.path() != "/")
} }
impl BifrostClient { impl BifrostClient {
@ -548,10 +548,7 @@ impl BifrostClient {
timeout_secs: u64, timeout_secs: u64,
) -> Result<Self> { ) -> Result<Self> {
let base = base_url.trim_end_matches('/').to_string(); let base = base_url.trim_end_matches('/').to_string();
// Only append "/v1" when the base URL has no version path segment. let base_url = if has_own_path(&base) {
// A URL already ending in "/v<digits>" (Bifrost's "/v1", z.ai's
// "/api/coding/paas/v4") is left untouched.
let base_url = if last_segment_is_version(&base) {
base base
} else { } else {
format!("{}/v1", base) format!("{}/v1", base)
@ -922,6 +919,59 @@ mod tests {
assert_eq!(reset_hint(None), ""); assert_eq!(reset_hint(None), "");
} }
/// Google's OpenAI-compatible root nests its version: `/v1beta/openai`.
/// Appending `/v1` there produced a path that hangs on POST.
#[test]
fn google_nested_version_path_is_not_doubled() {
let client = BifrostClient::new(
"https://generativelanguage.googleapis.com/v1beta/openai",
"",
"",
"gemini-3.6-flash",
300,
)
.unwrap();
assert_eq!(
client.base_url,
"https://generativelanguage.googleapis.com/v1beta/openai"
);
}
/// Every `[providers.*]` base URL on disk 2026-08-18 and the API root the
/// client must end up posting to.
#[test]
fn base_url_completion_across_configured_providers() {
let cases = [
// Paths of their own — taken as complete, nested version or not.
(
"https://generativelanguage.googleapis.com/v1beta/openai",
"https://generativelanguage.googleapis.com/v1beta/openai",
),
(
"https://generativelanguage.googleapis.com/v1beta/openai/",
"https://generativelanguage.googleapis.com/v1beta/openai",
),
(
"https://api.z.ai/api/coding/paas/v4",
"https://api.z.ai/api/coding/paas/v4",
),
(
"https://token-plan-sgp.xiaomimimo.com/v1",
"https://token-plan-sgp.xiaomimimo.com/v1",
),
("https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1"),
// Bare hosts — get the OpenAI default.
("https://api.deepseek.com", "https://api.deepseek.com/v1"),
("https://api.deepseek.com/", "https://api.deepseek.com/v1"),
("http://10.10.20.120:3360", "http://10.10.20.120:3360/v1"),
("http://10.10.20.19:8080", "http://10.10.20.19:8080/v1"),
];
for (base, want) in cases {
let client = BifrostClient::new(base, "", "", "m", 30).unwrap();
assert_eq!(client.base_url, want, "base {base}");
}
}
#[test] #[test]
fn test_client_creation() { fn test_client_creation() {
let client = BifrostClient::new( let client = BifrostClient::new(