use wiremux_auth::{ResolvedProfile, Wire};
pub(crate) const MISSING_BASE_URL: &str =
"profile has no base_url; set top-level `base_url` on the profile";
pub fn upstream_url_for_model(
profile: &ResolvedProfile,
model: Option<&str>,
stream: bool,
) -> Result<String, String> {
let base = profile.http.base_url.as_deref().ok_or(MISSING_BASE_URL)?;
let path = profile
.http
.chat_path
.as_deref()
.or_else(|| profile.dialect.wire.map(Wire::default_chat_path))
.unwrap_or("/");
let mut path = if let Some(model) = model.filter(|m| !m.is_empty()) {
path.replace("{model}", &encode_model_segment(model))
} else {
path.to_string()
};
if path.contains("{model}") {
return Err("request has no model and the profile path still contains {model}".to_string());
}
path = rewrite_stream_method(profile.dialect.wire, &path, stream);
let path = path.trim().to_string();
if is_absolute_http_url(&path) {
return Ok(rewrite_vertex_global_host(path));
}
Ok(rewrite_vertex_global_host(format!(
"{}{}",
base.trim_end_matches('/'),
if path.starts_with('/') {
path
} else {
format!("/{path}")
}
)))
}
fn rewrite_stream_method(wire: Option<Wire>, path: &str, stream: bool) -> String {
let (path_only, suffix) = split_path_suffix(path);
if !stream {
return format!("{path_only}{suffix}");
}
if matches!(wire, Some(Wire::Gemini))
&& let Some(prefix) = path_only.strip_suffix(":generateContent")
{
return format!("{prefix}:streamGenerateContent{}", with_alt_sse(suffix));
}
if matches!(wire, Some(Wire::Messages))
&& let Some(prefix) = path_only.strip_suffix(":rawPredict")
{
return format!("{prefix}:streamRawPredict{suffix}");
}
if matches!(wire, Some(Wire::Converse)) && path_only.ends_with("/converse") {
return format!("{path_only}-stream{suffix}");
}
format!("{path_only}{suffix}")
}
fn split_path_suffix(path: &str) -> (&str, &str) {
match path.find(['?', '#']) {
Some(index) => (&path[..index], &path[index..]),
None => (path, ""),
}
}
fn with_alt_sse(suffix: &str) -> String {
if suffix.is_empty() {
return "?alt=sse".to_string();
}
let (query, fragment) = match suffix.split_once('#') {
Some((query, fragment)) => (query, Some(fragment)),
None => (suffix, None),
};
let mut out = if query.is_empty() {
"?alt=sse".to_string()
} else if query.split(['?', '&']).any(|part| part == "alt=sse") {
query.to_string()
} else {
format!("{query}&alt=sse")
};
if let Some(fragment) = fragment {
out.push('#');
out.push_str(fragment);
}
out
}
fn is_absolute_http_url(path: &str) -> bool {
let Some((scheme, _)) = path.split_once("://") else {
return false;
};
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
}
pub(crate) fn encode_model_segment(model: &str) -> String {
let mut out = String::with_capacity(model.len());
for byte in model.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b':' => {
out.push(byte as char)
}
_ => {
out.push('%');
out.push(char::from(b"0123456789ABCDEF"[(byte >> 4) as usize]));
out.push(char::from(b"0123456789ABCDEF"[(byte & 0x0F) as usize]));
}
}
}
out
}
fn rewrite_vertex_global_host(url: String) -> String {
url.replacen(
"://global-aiplatform.googleapis.com",
"://aiplatform.googleapis.com",
1,
)
}