mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(gateway): cover Google OpenAI-compatible roots
This commit is contained in:
@@ -13,7 +13,8 @@ use crate::claude_code::build_claude_code_messages_url;
|
||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||
use crate::url::{
|
||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||
build_openai_responses_url, build_passthrough_path_url, normalize_gemini_content_action_path,
|
||||
build_openai_responses_url, build_passthrough_path_url,
|
||||
google_openai_compat_base_includes_api_root, normalize_gemini_content_action_path,
|
||||
};
|
||||
use crate::vertex::{
|
||||
build_vertex_api_key_gemini_content_url, build_vertex_api_key_gemini_embedding_url,
|
||||
@@ -409,7 +410,9 @@ fn build_provider_v1_url(
|
||||
.map(|(base, _)| base)
|
||||
.unwrap_or_else(|| upstream_base_url.trim())
|
||||
.trim_end_matches('/');
|
||||
let path = if base_without_query.ends_with("/v1") {
|
||||
let path = if base_without_query.ends_with("/v1")
|
||||
|| google_openai_compat_base_includes_api_root(base_without_query)
|
||||
{
|
||||
v1_path
|
||||
} else {
|
||||
default_path
|
||||
@@ -997,6 +1000,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_request_url_preserves_google_openai_compat_roots() {
|
||||
let developer_api_openai = sample_transport(
|
||||
"custom",
|
||||
"openai:embedding",
|
||||
"https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
None,
|
||||
);
|
||||
let vertex_openai = sample_transport(
|
||||
"custom",
|
||||
"openai:embedding",
|
||||
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&developer_api_openai,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "openai:embedding",
|
||||
mapped_model: Some("gemini-embedding-001"),
|
||||
upstream_is_stream: false,
|
||||
request_query: Some("trace=1"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://generativelanguage.googleapis.com/v1beta/openai/embeddings?trace=1")
|
||||
);
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&vertex_openai,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "openai:embedding",
|
||||
mapped_model: Some("gemini-embedding-001"),
|
||||
upstream_is_stream: false,
|
||||
request_query: None,
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi/embeddings"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_embedding_batch_body_uses_batch_endpoint() {
|
||||
let gemini = sample_transport(
|
||||
|
||||
@@ -2,15 +2,17 @@ use std::collections::BTreeMap;
|
||||
|
||||
use super::provider_types::is_codex_cli_backend_url;
|
||||
use url::form_urlencoded;
|
||||
use url::Url;
|
||||
|
||||
pub fn build_openai_chat_url(upstream_base_url: &str, query: Option<&str>) -> String {
|
||||
let (trimmed, base_query) = split_base_url_query(upstream_base_url);
|
||||
let trimmed = trimmed.trim_end_matches('/');
|
||||
let mut url = if trimmed.ends_with("/v1") {
|
||||
format!("{trimmed}/chat/completions")
|
||||
} else {
|
||||
format!("{trimmed}/v1/chat/completions")
|
||||
};
|
||||
let mut url =
|
||||
if trimmed.ends_with("/v1") || google_openai_compat_base_includes_api_root(trimmed) {
|
||||
format!("{trimmed}/chat/completions")
|
||||
} else {
|
||||
format!("{trimmed}/v1/chat/completions")
|
||||
};
|
||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||
url
|
||||
}
|
||||
@@ -195,6 +197,33 @@ fn split_base_url_query(base_url: &str) -> (&str, Option<&str>) {
|
||||
.unwrap_or((trimmed, None))
|
||||
}
|
||||
|
||||
pub(crate) fn google_openai_compat_base_includes_api_root(base_url: &str) -> bool {
|
||||
let Ok(parsed) = Url::parse(base_url.trim()) else {
|
||||
return false;
|
||||
};
|
||||
let Some(host) = parsed.host_str().map(|value| value.to_ascii_lowercase()) else {
|
||||
return false;
|
||||
};
|
||||
let path = parsed.path().trim_end_matches('/');
|
||||
|
||||
if host == "generativelanguage.googleapis.com" {
|
||||
return path == "/v1beta/openai" || path == "/v1/openai";
|
||||
}
|
||||
|
||||
if looks_like_vertex_ai_host(&host) {
|
||||
return path.ends_with("/endpoints/openapi");
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn looks_like_vertex_ai_host(host: &str) -> bool {
|
||||
const VERTEX_AI_HOST: &str = "aiplatform.googleapis.com";
|
||||
host == VERTEX_AI_HOST
|
||||
|| host.ends_with(&format!(".{VERTEX_AI_HOST}"))
|
||||
|| host.ends_with(&format!("-{VERTEX_AI_HOST}"))
|
||||
}
|
||||
|
||||
fn split_path_query(path: &str) -> (&str, Option<&str>) {
|
||||
path.split_once('?')
|
||||
.map(|(path, query)| (path, Some(query)))
|
||||
@@ -294,6 +323,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_url_preserves_google_openai_compat_roots() {
|
||||
assert_eq!(
|
||||
build_openai_chat_url(
|
||||
"https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
Some("trace=1")
|
||||
),
|
||||
"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions?trace=1"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_chat_url(
|
||||
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi",
|
||||
None,
|
||||
),
|
||||
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi/chat/completions"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_url_preserves_codex_path_prefix() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -82,7 +82,20 @@ fn is_vertex_host_format_context(transport: &GatewayProviderTransportSnapshot) -
|
||||
}
|
||||
|
||||
let endpoint_api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
|
||||
endpoint_api_format.starts_with("gemini:") || endpoint_api_format.starts_with("claude:")
|
||||
endpoint_api_format.starts_with("gemini:")
|
||||
|| endpoint_api_format.starts_with("claude:")
|
||||
|| (endpoint_api_format.starts_with("openai:")
|
||||
&& looks_like_vertex_openai_compat_base(&transport.endpoint.base_url))
|
||||
}
|
||||
|
||||
fn looks_like_vertex_openai_compat_base(base_url: &str) -> bool {
|
||||
let Ok(parsed) = Url::parse(base_url.trim()) else {
|
||||
return false;
|
||||
};
|
||||
parsed
|
||||
.path()
|
||||
.trim_end_matches('/')
|
||||
.ends_with("/endpoints/openapi")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -198,4 +211,28 @@ mod tests {
|
||||
"claude:messages"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infers_vertex_service_account_context_for_openai_compat_endpoint_root() {
|
||||
let mut transport = sample_transport();
|
||||
transport.endpoint.api_format = "openai:chat".to_string();
|
||||
transport.endpoint.base_url =
|
||||
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi"
|
||||
.to_string();
|
||||
transport.key.auth_type = "service_account".to_string();
|
||||
|
||||
assert!(is_vertex_service_account_transport_context(&transport));
|
||||
assert!(is_vertex_transport_context(&transport));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_infer_vertex_context_for_generic_openai_format_on_aiplatform_root() {
|
||||
let mut transport = sample_transport();
|
||||
transport.endpoint.api_format = "openai:chat".to_string();
|
||||
transport.endpoint.base_url = "https://aiplatform.googleapis.com".to_string();
|
||||
transport.key.auth_type = "service_account".to_string();
|
||||
|
||||
assert!(!is_vertex_service_account_transport_context(&transport));
|
||||
assert!(!is_vertex_transport_context(&transport));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user