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:
parent
9e147093ec
commit
f32f00c6e3
1 changed files with 63 additions and 13 deletions
|
|
@ -528,15 +528,15 @@ enum ErrorClass {
|
|||
Permanent,
|
||||
}
|
||||
|
||||
/// True if the URL's final path segment is a version tag like `v1`, `v4`.
|
||||
/// Used to avoid appending `/v1` to an already-versioned base URL.
|
||||
fn last_segment_is_version(url: &str) -> bool {
|
||||
match url.rsplit('/').next() {
|
||||
Some(seg) if seg.len() >= 2 => {
|
||||
seg.starts_with('v') && seg[1..].chars().all(|c| c.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
/// True if the base URL carries a path past the authority. Such a base is the
|
||||
/// complete API root; only a bare host gets the OpenAI default `/v1`.
|
||||
///
|
||||
/// Measured 2026-08-18: testing only the last segment for `v<digits>` appended
|
||||
/// `/v1` to Google's `/v1beta/openai`, and a POST to the doubled path hangs
|
||||
/// (40s, zero bytes) while `GET /models` returns 200 on both — so the mangled
|
||||
/// base looks healthy on a listing.
|
||||
fn has_own_path(url: &str) -> bool {
|
||||
reqwest::Url::parse(url).is_ok_and(|u| u.path() != "/")
|
||||
}
|
||||
|
||||
impl BifrostClient {
|
||||
|
|
@ -548,10 +548,7 @@ impl BifrostClient {
|
|||
timeout_secs: u64,
|
||||
) -> Result<Self> {
|
||||
let base = base_url.trim_end_matches('/').to_string();
|
||||
// Only append "/v1" when the base URL has no version path segment.
|
||||
// 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) {
|
||||
let base_url = if has_own_path(&base) {
|
||||
base
|
||||
} else {
|
||||
format!("{}/v1", base)
|
||||
|
|
@ -922,6 +919,59 @@ mod tests {
|
|||
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]
|
||||
fn test_client_creation() {
|
||||
let client = BifrostClient::new(
|
||||
|
|
|
|||
Loading…
Reference in a new issue