mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 11:49:50 +08:00
Normalize endpoint API root handling
This commit is contained in:
@@ -132,7 +132,7 @@ mod tests {
|
||||
api_family: None,
|
||||
endpoint_kind: None,
|
||||
is_active: true,
|
||||
base_url: "https://api.openai.com".to_string(),
|
||||
base_url: "https://api.openai.com/v1".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
|
||||
@@ -17,7 +17,6 @@ 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,
|
||||
openai_compatible_base_includes_api_root,
|
||||
};
|
||||
use crate::vertex::{
|
||||
build_vertex_api_key_gemini_content_url, build_vertex_api_key_gemini_embedding_url,
|
||||
@@ -423,32 +422,18 @@ fn normalize_gemini_embedding_action_path(path: &str, batch: bool) -> String {
|
||||
}
|
||||
|
||||
fn build_provider_embedding_v1_url(upstream_base_url: &str, query: Option<&str>) -> Option<String> {
|
||||
build_provider_v1_url(upstream_base_url, "/embeddings", "/v1/embeddings", query)
|
||||
build_provider_api_root_url(upstream_base_url, "/embeddings", query)
|
||||
}
|
||||
|
||||
fn build_provider_rerank_v1_url(upstream_base_url: &str, query: Option<&str>) -> Option<String> {
|
||||
build_provider_v1_url(upstream_base_url, "/rerank", "/v1/rerank", query)
|
||||
build_provider_api_root_url(upstream_base_url, "/rerank", query)
|
||||
}
|
||||
|
||||
fn build_provider_v1_url(
|
||||
fn build_provider_api_root_url(
|
||||
upstream_base_url: &str,
|
||||
v1_path: &str,
|
||||
default_path: &str,
|
||||
path: &str,
|
||||
query: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let base_without_query = upstream_base_url
|
||||
.trim()
|
||||
.split_once('?')
|
||||
.map(|(base, _)| base)
|
||||
.unwrap_or_else(|| upstream_base_url.trim())
|
||||
.trim_end_matches('/');
|
||||
let path = if base_without_query.ends_with("/v1")
|
||||
|| openai_compatible_base_includes_api_root(base_without_query)
|
||||
{
|
||||
v1_path
|
||||
} else {
|
||||
default_path
|
||||
};
|
||||
build_passthrough_path_url(upstream_base_url, path, query, &[])
|
||||
}
|
||||
|
||||
@@ -1016,7 +1001,12 @@ mod tests {
|
||||
"https://api.openai.example/v1",
|
||||
None,
|
||||
);
|
||||
let jina = sample_transport("jina", "jina:embedding", "https://api.jina.example", None);
|
||||
let jina = sample_transport(
|
||||
"jina",
|
||||
"jina:embedding",
|
||||
"https://api.jina.example/v1",
|
||||
None,
|
||||
);
|
||||
let gemini = sample_transport(
|
||||
"gemini",
|
||||
"gemini:embedding",
|
||||
@@ -1221,7 +1211,7 @@ mod tests {
|
||||
"https://api.openai.example/v1",
|
||||
None,
|
||||
);
|
||||
let jina = sample_transport("jina", "jina:rerank", "https://api.jina.example", None);
|
||||
let jina = sample_transport("jina", "jina:rerank", "https://api.jina.example/v1", None);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
|
||||
@@ -568,12 +568,12 @@ mod tests {
|
||||
#[test]
|
||||
fn plan_fallback_url_helpers_route_openai_surfaces() {
|
||||
assert_eq!(
|
||||
build_standard_plan_fallback_openai_chat_url("https://api.example.com", Some("x=1")),
|
||||
build_standard_plan_fallback_openai_chat_url("https://api.example.com/v1", Some("x=1")),
|
||||
"https://api.example.com/v1/chat/completions?x=1"
|
||||
);
|
||||
assert_eq!(
|
||||
build_standard_plan_fallback_openai_responses_url(
|
||||
"https://api.example.com",
|
||||
"https://api.example.com/v1",
|
||||
Some("x=1"),
|
||||
true,
|
||||
),
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
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 openai_compatible_base_includes_api_root(trimmed) {
|
||||
format!("{trimmed}/chat/completions")
|
||||
} else {
|
||||
format!("{trimmed}/v1/chat/completions")
|
||||
};
|
||||
let mut url = format!("{trimmed}/chat/completions");
|
||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||
url
|
||||
}
|
||||
@@ -28,14 +23,7 @@ pub fn build_openai_responses_url(
|
||||
} else {
|
||||
"/responses"
|
||||
};
|
||||
let mut url = if is_codex_cli_backend_url(trimmed)
|
||||
|| trimmed.ends_with("/codex")
|
||||
|| openai_compatible_base_includes_api_root(trimmed)
|
||||
{
|
||||
format!("{trimmed}{suffix}")
|
||||
} else {
|
||||
format!("{trimmed}/v1{suffix}")
|
||||
};
|
||||
let mut url = format!("{trimmed}{suffix}");
|
||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||
url
|
||||
}
|
||||
@@ -50,10 +38,8 @@ pub fn build_openai_image_url(
|
||||
let suffix = openai_image_path_suffix(request_path);
|
||||
let mut url = if openai_image_base_includes_operation_path(trimmed) {
|
||||
trimmed.to_string()
|
||||
} else if openai_compatible_base_includes_api_root(trimmed) {
|
||||
format!("{trimmed}{suffix}")
|
||||
} else {
|
||||
format!("{trimmed}/v1{suffix}")
|
||||
format!("{trimmed}{suffix}")
|
||||
};
|
||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||
url
|
||||
@@ -80,11 +66,7 @@ fn openai_image_base_includes_operation_path(base_url: &str) -> bool {
|
||||
pub fn build_claude_messages_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}/messages")
|
||||
} else {
|
||||
format!("{trimmed}/v1/messages")
|
||||
};
|
||||
let mut url = format!("{trimmed}/messages");
|
||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||
url
|
||||
}
|
||||
@@ -230,10 +212,10 @@ pub fn build_openai_compatible_models_url(upstream_base_url: &str) -> Option<Str
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut url = if openai_compatible_base_includes_api_root(trimmed_base_url) {
|
||||
format!("{trimmed_base_url}/models")
|
||||
let mut url = if trimmed_base_url.ends_with("/models") {
|
||||
trimmed_base_url.to_string()
|
||||
} else {
|
||||
format!("{trimmed_base_url}/v1/models")
|
||||
format!("{trimmed_base_url}/models")
|
||||
};
|
||||
append_merged_query(&mut url, base_query, None, None, &[]);
|
||||
Some(url)
|
||||
@@ -500,12 +482,20 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_chat_url("https://proxy.example.com", None),
|
||||
"https://proxy.example.com/v1/chat/completions"
|
||||
"https://proxy.example.com/chat/completions"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_chat_url("https://api.deepseek.com", None),
|
||||
"https://api.deepseek.com/chat/completions"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_responses_url("https://proxy.example.com/api", None, false),
|
||||
"https://proxy.example.com/api/responses"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_responses_url("https://api.deepseek.com", None, false),
|
||||
"https://api.deepseek.com/responses"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_image_url(
|
||||
"https://proxy.example.com/api",
|
||||
@@ -524,15 +514,15 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://proxy.example.com/api", None),
|
||||
"https://proxy.example.com/api/v1/messages"
|
||||
"https://proxy.example.com/api/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://proxy.example.com/anthropic", None),
|
||||
"https://proxy.example.com/anthropic/v1/messages"
|
||||
"https://proxy.example.com/anthropic/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://api.anthropic.example", None),
|
||||
"https://api.anthropic.example/v1/messages"
|
||||
"https://api.anthropic.example/messages"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -565,7 +555,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_compatible_models_url("https://proxy.example.com").as_deref(),
|
||||
Some("https://proxy.example.com/v1/models")
|
||||
Some("https://proxy.example.com/models")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -592,7 +582,11 @@ mod tests {
|
||||
"https://api.openai.example/v1/images/generations?tenant=demo&trace=1"
|
||||
);
|
||||
assert_eq!(
|
||||
build_openai_image_url("https://api.openai.example", Some("/v1/images/edits"), None),
|
||||
build_openai_image_url(
|
||||
"https://api.openai.example/v1",
|
||||
Some("/v1/images/edits"),
|
||||
None
|
||||
),
|
||||
"https://api.openai.example/v1/images/edits"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ pub fn build_video_create_upstream_url(
|
||||
match family {
|
||||
ProviderVideoCreateFamily::OpenAi => build_passthrough_path_url(
|
||||
&transport.endpoint.base_url,
|
||||
request_path,
|
||||
openai_video_api_root_request_path(request_path),
|
||||
request_query,
|
||||
&[],
|
||||
),
|
||||
@@ -183,6 +183,14 @@ pub fn build_video_create_upstream_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_video_api_root_request_path(request_path: &str) -> &str {
|
||||
if request_path.starts_with("/v1/") {
|
||||
&request_path[3..]
|
||||
} else {
|
||||
request_path
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_video_create_headers(
|
||||
input: ProviderVideoCreateHeadersInput<'_>,
|
||||
) -> Option<BTreeMap<String, String>> {
|
||||
@@ -427,6 +435,22 @@ mod tests {
|
||||
assert_eq!(body.get("model"), Some(&json!("upstream-video-model")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_openai_video_create_url_from_api_root_base() {
|
||||
let mut transport = sample_transport("openai:video", "bearer");
|
||||
transport.endpoint.base_url = "https://api.openai.example/v1".to_string();
|
||||
let url = build_video_create_upstream_url(
|
||||
&transport,
|
||||
"/v1/videos",
|
||||
Some("trace=1"),
|
||||
"sora-upstream",
|
||||
ProviderVideoCreateFamily::OpenAi,
|
||||
)
|
||||
.expect("url should build");
|
||||
|
||||
assert_eq!(url, "https://api.openai.example/v1/videos?trace=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_gemini_video_create_url_and_removes_client_key_query() {
|
||||
let transport = sample_transport("gemini:video", "api_key");
|
||||
|
||||
Reference in New Issue
Block a user