mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Improve request trace upstream diagnostics
This commit is contained in:
@@ -12,7 +12,7 @@ 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,
|
||||
build_openai_responses_url, build_passthrough_path_url, normalize_gemini_content_action_path,
|
||||
};
|
||||
use crate::vertex::{
|
||||
build_vertex_api_key_gemini_content_url, resolve_local_vertex_api_key_query_auth,
|
||||
@@ -36,6 +36,8 @@ pub fn build_transport_request_url(
|
||||
}
|
||||
|
||||
let provider_api_format = params.provider_api_format.trim().to_ascii_lowercase();
|
||||
let normalized_provider_api_format =
|
||||
aether_ai_formats::normalize_api_format_alias(&provider_api_format);
|
||||
let custom_path = transport
|
||||
.endpoint
|
||||
.custom_path
|
||||
@@ -45,14 +47,19 @@ pub fn build_transport_request_url(
|
||||
.map(|path| expand_custom_path_template(path, build_path_params(params)));
|
||||
|
||||
if let Some(path) = custom_path.as_deref() {
|
||||
let blocked_keys = if provider_api_format.starts_with("gemini:") {
|
||||
let blocked_keys = if normalized_provider_api_format.starts_with("gemini:") {
|
||||
&["key"][..]
|
||||
} else {
|
||||
&[][..]
|
||||
};
|
||||
let normalized_path = if normalized_provider_api_format == "gemini:generate_content" {
|
||||
normalize_gemini_content_action_path(path, params.upstream_is_stream)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
let url = build_passthrough_path_url(
|
||||
&transport.endpoint.base_url,
|
||||
path,
|
||||
normalized_path.as_str(),
|
||||
params.request_query,
|
||||
blocked_keys,
|
||||
)?;
|
||||
@@ -63,7 +70,7 @@ pub fn build_transport_request_url(
|
||||
));
|
||||
}
|
||||
|
||||
let url = match aether_ai_formats::normalize_api_format_alias(&provider_api_format).as_str() {
|
||||
let url = match normalized_provider_api_format.as_str() {
|
||||
"openai:chat" => Some(build_openai_chat_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
@@ -570,6 +577,81 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrites_hardcoded_gemini_custom_path_action_to_match_stream_mode() {
|
||||
let stream_transport = sample_transport(
|
||||
"custom",
|
||||
"gemini:generate_content",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
Some("/v1beta/models/{model}:generateContent"),
|
||||
);
|
||||
|
||||
let stream_url = build_transport_request_url(
|
||||
&stream_transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: true,
|
||||
request_query: Some("key=client-key&foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("stream custom path url");
|
||||
|
||||
assert_eq!(
|
||||
stream_url,
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?foo=bar&alt=sse"
|
||||
);
|
||||
|
||||
let sync_transport = sample_transport(
|
||||
"custom",
|
||||
"gemini:generate_content",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
Some("/v1beta/models/{model}:streamGenerateContent"),
|
||||
);
|
||||
|
||||
let sync_url = build_transport_request_url(
|
||||
&sync_transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: false,
|
||||
request_query: Some("foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("sync custom path url");
|
||||
|
||||
assert_eq!(
|
||||
sync_url,
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?foo=bar"
|
||||
);
|
||||
|
||||
let v1_transport = sample_transport(
|
||||
"custom",
|
||||
"gemini:generate_content",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
Some("/v1/models/{model}:generateContent"),
|
||||
);
|
||||
|
||||
let v1_stream_url = build_transport_request_url(
|
||||
&v1_transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: true,
|
||||
request_query: None,
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.expect("v1 stream custom path url");
|
||||
|
||||
assert_eq!(
|
||||
v1_stream_url,
|
||||
"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_original_custom_path_when_template_params_are_missing() {
|
||||
let transport = sample_transport(
|
||||
|
||||
@@ -69,9 +69,10 @@ pub fn build_gemini_content_url(
|
||||
} else {
|
||||
"generateContent"
|
||||
};
|
||||
let mut url = if trimmed_base_url.ends_with("/v1beta") {
|
||||
let mut url = if trimmed_base_url.ends_with("/v1") || trimmed_base_url.ends_with("/v1beta") {
|
||||
format!("{trimmed_base_url}/models/{trimmed_model}:{operation}")
|
||||
} else if trimmed_base_url.contains("/v1beta/models/") {
|
||||
} else if gemini_content_base_url_contains_model_path(trimmed_base_url) {
|
||||
let trimmed_base_url = strip_gemini_content_action(trimmed_base_url);
|
||||
format!("{trimmed_base_url}:{operation}")
|
||||
} else {
|
||||
format!("{trimmed_base_url}/v1beta/models/{trimmed_model}:{operation}")
|
||||
@@ -80,6 +81,37 @@ pub fn build_gemini_content_url(
|
||||
Some(url)
|
||||
}
|
||||
|
||||
pub fn normalize_gemini_content_action_path(path: &str, stream: bool) -> String {
|
||||
let trimmed = path.trim();
|
||||
let (path, query) = split_path_query(trimmed);
|
||||
let action = if stream {
|
||||
"streamGenerateContent"
|
||||
} else {
|
||||
"generateContent"
|
||||
};
|
||||
let normalized = strip_gemini_content_action(path);
|
||||
let normalized = if normalized.len() == path.len() {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("{normalized}:{action}")
|
||||
};
|
||||
match query {
|
||||
Some(query) => format!("{normalized}?{query}"),
|
||||
None => normalized,
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_gemini_content_action(value: &str) -> &str {
|
||||
value
|
||||
.strip_suffix(":streamGenerateContent")
|
||||
.or_else(|| value.strip_suffix(":generateContent"))
|
||||
.unwrap_or(value)
|
||||
}
|
||||
|
||||
fn gemini_content_base_url_contains_model_path(value: &str) -> bool {
|
||||
value.contains("/v1/models/") || value.contains("/v1beta/models/")
|
||||
}
|
||||
|
||||
pub fn build_gemini_video_predict_long_running_url(
|
||||
upstream_base_url: &str,
|
||||
model: &str,
|
||||
@@ -92,9 +124,9 @@ pub fn build_gemini_video_predict_long_running_url(
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut url = if trimmed_base_url.ends_with("/v1beta") {
|
||||
let mut url = if trimmed_base_url.ends_with("/v1") || trimmed_base_url.ends_with("/v1beta") {
|
||||
format!("{trimmed_base_url}/models/{trimmed_model}:predictLongRunning")
|
||||
} else if trimmed_base_url.contains("/v1beta/models/") {
|
||||
} else if gemini_content_base_url_contains_model_path(trimmed_base_url) {
|
||||
format!("{trimmed_base_url}:predictLongRunning")
|
||||
} else {
|
||||
format!("{trimmed_base_url}/v1beta/models/{trimmed_model}:predictLongRunning")
|
||||
@@ -248,6 +280,7 @@ mod tests {
|
||||
build_gemini_content_url, build_gemini_files_passthrough_url,
|
||||
build_gemini_video_predict_long_running_url, build_openai_chat_url,
|
||||
build_openai_responses_url, build_passthrough_path_url,
|
||||
normalize_gemini_content_action_path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -289,6 +322,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_content_urls_rewrite_existing_base_action_for_stream_mode() {
|
||||
assert_eq!(
|
||||
build_gemini_content_url(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent",
|
||||
"ignored-model",
|
||||
true,
|
||||
Some("foo=bar")
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?foo=bar"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
build_gemini_content_url(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
||||
"ignored-model",
|
||||
false,
|
||||
Some("foo=bar")
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?foo=bar"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
build_gemini_content_url(
|
||||
"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:generateContent",
|
||||
"ignored-model",
|
||||
true,
|
||||
Some("foo=bar")
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:streamGenerateContent?foo=bar"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_gemini_content_action_in_custom_paths() {
|
||||
assert_eq!(
|
||||
normalize_gemini_content_action_path(
|
||||
"/v1beta/models/gemini-2.5-pro:generateContent?alt=sse",
|
||||
true
|
||||
),
|
||||
"/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_gemini_content_action_path(
|
||||
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
||||
false
|
||||
),
|
||||
"/v1beta/models/gemini-2.5-pro:generateContent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_base_path_and_request_query_for_passthrough_paths() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user