test: stabilize merged PR checks

This commit is contained in:
fawney19
2026-05-19 10:18:25 +08:00
parent d2b42e91d2
commit 052de6b96e
25 changed files with 726 additions and 77 deletions

View File

@@ -138,3 +138,80 @@ pub(crate) fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<serde
pub(crate) fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_formats::api::aggregate_gemini_stream_sync_response(body)
}
pub(crate) fn gemini_generate_content_response_has_visible_output(
body: &serde_json::Value,
) -> bool {
if aether_ai_formats::formats::gemini::generate_content::response::from_raw(body).is_some() {
return true;
}
openai_chat_response_has_visible_output(body) || openai_responses_body_has_visible_output(body)
}
fn openai_chat_response_has_visible_output(body: &serde_json::Value) -> bool {
body.get("choices")
.and_then(serde_json::Value::as_array)
.is_some_and(|choices| {
choices.iter().any(|choice| {
choice
.get("message")
.or_else(|| choice.get("delta"))
.is_some_and(message_like_value_has_visible_output)
|| value_has_non_empty_text(choice.get("text"))
|| choice
.get("finish_reason")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| !value.trim().is_empty() && value != "length")
})
})
}
fn openai_responses_body_has_visible_output(body: &serde_json::Value) -> bool {
body.get("output")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| {
items.iter().any(|item| {
item.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "function_call" | "image_generation_call"))
|| item
.get("content")
.and_then(serde_json::Value::as_array)
.is_some_and(|content| {
content.iter().any(response_content_has_visible_output)
})
})
})
}
fn message_like_value_has_visible_output(value: &serde_json::Value) -> bool {
value_has_non_empty_text(value.get("content"))
|| value
.get("tool_calls")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| !items.is_empty())
}
fn response_content_has_visible_output(value: &serde_json::Value) -> bool {
value_has_non_empty_text(value.get("text"))
|| value
.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "function_call" | "output_image"))
}
fn value_has_non_empty_text(value: Option<&serde_json::Value>) -> bool {
match value {
Some(serde_json::Value::String(text)) => !text.trim().is_empty(),
Some(serde_json::Value::Array(items)) => items.iter().any(|item| {
value_has_non_empty_text(item.get("text"))
|| value_has_non_empty_text(item.get("content"))
|| item
.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "image_url" | "input_image"))
}),
_ => false,
}
}

View File

@@ -13,6 +13,7 @@ use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
pub(crate) use self::adaptation::{
maybe_build_provider_private_stream_normalizer, ProviderPrivateStreamNormalizer,
};
pub(crate) use self::api::gemini_generate_content_response_has_visible_output;
pub(crate) use self::finalize::common::LocalCoreSyncFinalizeOutcome;
pub(crate) use self::finalize::internal::{
maybe_bridge_standard_sync_json_to_stream, maybe_build_stream_response_rewriter,

View File

@@ -1249,49 +1249,6 @@ fn chatgpt_web_image_internal_url(base_url: &str) -> String {
format!("{base_url}/__aether/chatgpt-web-image")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "system", "content": "Use crisp vector-like shapes."},
{
"role": "user",
"content": [
{"type": "text", "text": "Draw a glass city"},
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}
]
}
],
"size": "1536x1024",
"output_format": "webp",
"web_model": "gpt-5-image-test"
});
let (provider_body, summary) =
build_chatgpt_web_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2")
.expect("chat image body should convert");
assert_eq!(provider_body["operation"], "edit");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["web_model"], "gpt-5-image-test");
assert_eq!(
provider_body["prompt"],
"Use crisp vector-like shapes.\nDraw a glass city"
);
assert_eq!(provider_body["size"], "1536x1024");
assert_eq!(provider_body["ratio"], "3:2");
assert_eq!(provider_body["output_format"], "webp");
assert_eq!(provider_body["images"][0], "https://example.com/ref.png");
assert_eq!(summary["operation"], "edit");
assert_eq!(summary["output_format"], "webp");
}
}
#[allow(clippy::too_many_arguments)]
async fn build_kiro_openai_chat_cross_format_payload_parts(
state: &AppState,
@@ -1515,3 +1472,46 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "system", "content": "Use crisp vector-like shapes."},
{
"role": "user",
"content": [
{"type": "text", "text": "Draw a glass city"},
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}
]
}
],
"size": "1536x1024",
"output_format": "webp",
"web_model": "gpt-5-image-test"
});
let (provider_body, summary) =
build_chatgpt_web_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2")
.expect("chat image body should convert");
assert_eq!(provider_body["operation"], "edit");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["web_model"], "gpt-5-image-test");
assert_eq!(
provider_body["prompt"],
"Use crisp vector-like shapes.\nDraw a glass city"
);
assert_eq!(provider_body["size"], "1536x1024");
assert_eq!(provider_body["ratio"], "3:2");
assert_eq!(provider_body["output_format"], "webp");
assert_eq!(provider_body["images"][0], "https://example.com/ref.png");
assert_eq!(summary["operation"], "edit");
assert_eq!(summary["output_format"], "webp");
}
}

View File

@@ -199,7 +199,7 @@ fn local_core_sync_finalize_has_invalid_provider_success(
return Ok(false);
}
let provider_api_format = resolve_local_sync_provider_api_format(payload);
if aether_ai_formats::normalize_api_format_alias(&provider_api_format)
if crate::ai_serving::normalize_api_format_alias(&provider_api_format)
!= "gemini:generate_content"
{
return Ok(false);
@@ -210,10 +210,7 @@ fn local_core_sync_finalize_has_invalid_provider_success(
if has_nested_error(&body_json) {
return Ok(false);
}
Ok(
aether_ai_formats::formats::gemini::generate_content::response::from_raw(&body_json)
.is_none(),
)
Ok(!crate::ai_serving::gemini_generate_content_response_has_visible_output(&body_json))
}
pub(crate) fn build_best_effort_local_core_error_body(

View File

@@ -363,7 +363,7 @@ fn invalid_gemini_provider_success_message(
.and_then(|value| value.get("provider_api_format"))
.and_then(Value::as_str)
.unwrap_or(plan.provider_api_format.as_str());
if aether_ai_formats::normalize_api_format_alias(provider_api_format)
if crate::ai_serving::normalize_api_format_alias(provider_api_format)
!= "gemini:generate_content"
{
return None;
@@ -375,8 +375,7 @@ fn invalid_gemini_provider_success_message(
{
return None;
}
if aether_ai_formats::formats::gemini::generate_content::response::from_raw(body_json).is_some()
{
if crate::ai_serving::gemini_generate_content_response_has_visible_output(body_json) {
return None;
}
Some("Provider returned HTTP 200 but the Gemini response did not contain visible model output; refusing to finalize it as a successful response.")

View File

@@ -1494,7 +1494,7 @@ fn provider_query_standard_execution_response_body(
if result.status_code < 400
&& provider_query_normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
&& aether_ai_formats::formats::gemini::generate_content::response::from_raw(&body).is_none()
&& !crate::ai_serving::gemini_generate_content_response_has_visible_output(&body)
{
return None;
}

View File

@@ -53,7 +53,7 @@ fn provider_query_endpoint_route_payload(
candidate: &ProviderQueryTestCandidate,
execution: &ProviderQueryExecutionOutcome,
) -> Value {
let api_format = aether_ai_formats::normalize_api_format_alias(&candidate.endpoint.api_format);
let api_format = crate::ai_serving::normalize_api_format_alias(&candidate.endpoint.api_format);
let request_url = execution.request_url.to_ascii_lowercase();
let base_url = candidate.endpoint.base_url.to_ascii_lowercase();
let is_vertex = request_url.contains("aiplatform.googleapis.com")

View File

@@ -349,7 +349,13 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,
@@ -1117,7 +1123,13 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,
@@ -1595,7 +1607,13 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Vertex Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,

View File

@@ -349,7 +349,13 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,

View File

@@ -1224,7 +1224,7 @@ async fn gateway_refresh_quota_reconciles_unsupported_fixed_provider_endpoints_b
(
"provider-vertex-ai-reconcile",
"vertex_ai",
2usize,
3usize,
"gemini:generate_content",
"https://aiplatform.googleapis.com",
"Vertex AI 暂不支持自动刷新额度",

View File

@@ -1177,7 +1177,13 @@ async fn gateway_records_gemini_sync_usage_and_pricing_with_cache_read_tokens_im
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": expected.input_tokens,
"candidatesTokenCount": expected.output_tokens,