feat(format): audit same-format compatibility rewrites

This commit is contained in:
elky
2026-06-08 16:12:37 +08:00
parent 6155ffefcc
commit 84f41dae77
8 changed files with 297 additions and 23 deletions
@@ -107,6 +107,11 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
json!(crate::ai_serving::transport::GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME),
);
}
if !resolved.compatibility_edits.is_empty() {
if let Ok(value) = serde_json::to_value(&resolved.compatibility_edits) {
extra_fields.insert("request_body_compatibility_edits".to_string(), value);
}
}
let provider_api_format = resolved.provider_api_format.clone();
let effective_headers = input.effective_headers(&parts.headers);
let report_context = append_local_failover_policy_to_value(
@@ -175,6 +180,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
provider_request_headers,
provider_request_body,
transport_profile: _,
compatibility_edits: _,
request_redacted: _,
} = resolved;
@@ -19,7 +19,9 @@ use crate::ai_serving::transport::{
build_gemini_cli_v1internal_request, build_grok_browser_headers, build_grok_upstream_url,
build_same_format_provider_headers, resolve_local_gemini_cli_request_auth,
GeminiCliRequestAuth, GeminiCliRequestAuthSupport, GeminiCliRequestEnvelopeSupport,
GrokHeaderInput, SameFormatProviderHeadersInput, GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH,
GrokHeaderInput, SameFormatProviderCompatibilityEdit,
SameFormatProviderCompatibilityEditAction, SameFormatProviderHeadersInput,
GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH,
};
use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::{AppState, GatewayError};
@@ -107,6 +109,7 @@ pub(crate) struct LocalSameFormatProviderCandidatePayloadParts {
pub(super) provider_request_headers: BTreeMap<String, String>,
pub(super) provider_request_body: Value,
pub(super) transport_profile: Option<ResolvedTransportProfile>,
pub(super) compatibility_edits: Vec<SameFormatProviderCompatibilityEdit>,
pub(super) request_redacted: bool,
}
@@ -153,8 +156,8 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
let body_json = redaction.body_json.as_ref();
let mut transport = Arc::clone(&prepared.transport);
let Some(mut base_provider_request_body) =
super::super::request::build_same_format_provider_request_body(
let Some(base_provider_request) =
super::super::request::build_same_format_provider_request_body_with_compatibility_report(
body_json,
prepared.provider_api_format.as_str(),
&prepared.mapped_model,
@@ -190,6 +193,8 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
.await;
return Ok(None);
};
let mut base_provider_request_body = base_provider_request.body;
let mut compatibility_edits = base_provider_request.compatibility_edits;
if let Some(mapping) =
crate::system_features::reasoning_model_directive_mapping_for_api_format_and_model(
state,
@@ -198,10 +203,18 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
)
.await
{
let before_mapping = base_provider_request_body.clone();
crate::ai_serving::apply_model_directive_mapping_patch(
&mut base_provider_request_body,
&mapping,
);
if before_mapping != base_provider_request_body {
compatibility_edits.push(SameFormatProviderCompatibilityEdit {
field: "model_directive_mapping".to_string(),
action: SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
detail: "applied configured model directive mapping patch".to_string(),
});
}
// Directive mapping is a deep-merge patch and may overwrite/add `stream`;
// re-enforce stream-field policy afterward.
// Kiro behavior classification already hard-requires upstream streaming,
@@ -452,6 +465,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
provider_request_headers,
provider_request_body,
transport_profile,
compatibility_edits,
request_redacted: redaction.redacted,
}))
}
@@ -2,4 +2,5 @@ mod body;
mod url;
pub(super) use self::body::build_same_format_provider_request_body;
pub(super) use self::body::build_same_format_provider_request_body_with_compatibility_report;
pub(super) use self::url::build_same_format_upstream_url;
@@ -3,7 +3,9 @@ use serde_json::Value;
use super::super::LocalSameFormatProviderSpec;
use crate::ai_serving::transport::{
build_same_format_provider_request_body as build_same_format_provider_request_body_impl,
build_same_format_provider_request_body_with_compatibility_report as build_same_format_provider_request_body_with_compatibility_report_impl,
SameFormatProviderFamily, SameFormatProviderRequestBodyInput,
SameFormatProviderRequestBodyOutput,
};
pub(crate) fn build_same_format_provider_request_body(
@@ -36,6 +38,38 @@ pub(crate) fn build_same_format_provider_request_body(
})
}
pub(crate) fn build_same_format_provider_request_body_with_compatibility_report(
body_json: &Value,
provider_api_format: &str,
mapped_model: &str,
spec: LocalSameFormatProviderSpec,
body_rules: Option<&Value>,
request_headers: Option<&http::HeaderMap>,
upstream_is_stream: bool,
force_body_stream_field: bool,
kiro_auth: Option<&crate::ai_serving::transport::kiro::KiroRequestAuth>,
is_claude_code: bool,
enable_model_directives: bool,
) -> Option<SameFormatProviderRequestBodyOutput> {
build_same_format_provider_request_body_with_compatibility_report_impl(
SameFormatProviderRequestBodyInput {
body_json,
mapped_model,
client_api_format: spec.api_format,
provider_api_format,
source_model: body_json.get("model").and_then(Value::as_str),
family: same_format_provider_family(spec.family),
body_rules,
request_headers,
upstream_is_stream,
force_body_stream_field,
kiro_auth_config: kiro_auth.map(|auth| &auth.auth_config),
is_claude_code,
enable_model_directives,
},
)
}
fn same_format_provider_family(
family: super::super::LocalSameFormatProviderFamily,
) -> SameFormatProviderFamily {
@@ -72,8 +72,10 @@ pub(crate) use aether_provider_transport::{
build_local_openai_chat_upstream_url, build_local_openai_responses_upstream_url,
build_openai_image_headers, build_openai_image_upstream_url, build_passthrough_headers,
build_request_trace_proxy_value, build_same_format_provider_headers,
build_same_format_provider_request_body, build_same_format_provider_upstream_url,
build_standard_plan_fallback_headers, build_standard_plan_fallback_openai_chat_url,
build_same_format_provider_request_body,
build_same_format_provider_request_body_with_compatibility_report,
build_same_format_provider_upstream_url, build_standard_plan_fallback_headers,
build_standard_plan_fallback_openai_chat_url,
build_standard_plan_fallback_openai_responses_url, build_standard_provider_request_headers,
build_transport_request_url, build_transport_request_url_for_request_body,
build_video_create_headers, build_video_create_request_body, build_video_create_upstream_url,
@@ -106,12 +108,14 @@ pub(crate) use aether_provider_transport::{
GeminiCliRequestAuthUnsupportedReason, GeminiCliRequestEnvelopeSupport,
GeminiFilesHeadersInput, GeminiFilesRequestBodyError, GeminiFilesRequestBodyParts,
GrokHeaderInput, LocalResolvedOAuthRequestAuth, ProviderOpenAiImageHeadersInput,
ProviderVideoCreateFamily, ProviderVideoCreateHeadersInput, SameFormatProviderFamily,
SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior,
ProviderVideoCreateFamily, ProviderVideoCreateHeadersInput,
SameFormatProviderCompatibilityEdit, SameFormatProviderCompatibilityEditAction,
SameFormatProviderFamily, SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior,
SameFormatProviderRequestBehaviorParams, SameFormatProviderRequestBodyInput,
SameFormatProviderUpstreamUrlParams, StandardPlanFallbackAcceptPolicy,
StandardPlanFallbackHeadersInput, StandardProviderRequestHeaders,
StandardProviderRequestHeadersInput, TransportRequestBodySemanticsError,
TransportRequestUrlParams, GEMINI_CLI_USER_AGENT, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
GROK_CHAT_PATH, GROK_INTERNAL_HEADER, GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME,
SameFormatProviderRequestBodyOutput, SameFormatProviderUpstreamUrlParams,
StandardPlanFallbackAcceptPolicy, StandardPlanFallbackHeadersInput,
StandardProviderRequestHeaders, StandardProviderRequestHeadersInput,
TransportRequestBodySemanticsError, TransportRequestUrlParams, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH, GROK_INTERNAL_HEADER,
GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME,
};
@@ -2506,7 +2506,7 @@ fn ai_serving_same_format_provider_root_request_separates_body_and_url_policy()
"apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs",
);
for pattern in [
"super::super::request::build_same_format_provider_request_body(",
"super::super::request::build_same_format_provider_request_body_with_compatibility_report(",
"super::super::request::build_same_format_upstream_url(",
] {
assert!(
+4 -2
View File
@@ -112,14 +112,16 @@ pub use rules::{
};
pub use same_format_provider::{
build_same_format_provider_headers, build_same_format_provider_request_body,
build_same_format_provider_request_body_with_compatibility_report,
build_same_format_provider_upstream_url, classify_same_format_provider_request_behavior,
resolve_same_format_provider_direct_auth, same_format_provider_transport_supported,
same_format_provider_transport_unsupported_reason,
same_format_provider_transport_unsupported_reason_for_trace,
should_try_same_format_provider_oauth_auth, SameFormatProviderFamily,
should_try_same_format_provider_oauth_auth, SameFormatProviderCompatibilityEdit,
SameFormatProviderCompatibilityEditAction, SameFormatProviderFamily,
SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior,
SameFormatProviderRequestBehaviorParams, SameFormatProviderRequestBodyInput,
SameFormatProviderUpstreamUrlParams,
SameFormatProviderRequestBodyOutput, SameFormatProviderUpstreamUrlParams,
};
pub use snapshot::{
read_provider_transport_snapshot, GatewayProviderTransportSnapshot,
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Value;
use crate::antigravity::is_antigravity_provider_transport;
@@ -75,6 +76,29 @@ pub struct SameFormatProviderRequestBodyInput<'a> {
pub enable_model_directives: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SameFormatProviderRequestBodyOutput {
pub body: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub compatibility_edits: Vec<SameFormatProviderCompatibilityEdit>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SameFormatProviderCompatibilityEdit {
pub field: String,
pub action: SameFormatProviderCompatibilityEditAction,
pub detail: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SameFormatProviderCompatibilityEditAction {
RuntimeRewrite,
ProviderCompatibilityRewrite,
ProviderEnvelope,
OperatorRule,
}
#[derive(Debug, Clone, Copy)]
pub struct SameFormatProviderUpstreamUrlParams<'a> {
pub provider_api_format: &'a str,
@@ -158,15 +182,41 @@ pub fn classify_same_format_provider_request_behavior(
pub fn build_same_format_provider_request_body(
input: SameFormatProviderRequestBodyInput<'_>,
) -> Option<Value> {
build_same_format_provider_request_body_inner(input, None)
}
pub fn build_same_format_provider_request_body_with_compatibility_report(
input: SameFormatProviderRequestBodyInput<'_>,
) -> Option<SameFormatProviderRequestBodyOutput> {
let mut compatibility_edits = Vec::new();
let body =
build_same_format_provider_request_body_inner(input, Some(&mut compatibility_edits))?;
Some(SameFormatProviderRequestBodyOutput {
body,
compatibility_edits,
})
}
fn build_same_format_provider_request_body_inner(
input: SameFormatProviderRequestBodyInput<'_>,
mut compatibility_edits: Option<&mut Vec<SameFormatProviderCompatibilityEdit>>,
) -> Option<Value> {
if let Some(kiro_auth_config) = input.kiro_auth_config {
return build_kiro_provider_request_body(
let body = build_kiro_provider_request_body(
input.body_json,
input.mapped_model,
kiro_auth_config,
input.body_rules,
input.request_headers,
)?;
record_compatibility_edit(
&mut compatibility_edits,
"$",
SameFormatProviderCompatibilityEditAction::ProviderEnvelope,
"wrapped same-format request in Kiro provider envelope",
);
return Some(body);
}
if embedding_multimodal_input_requires_aliyun_provider(
@@ -200,29 +250,70 @@ pub fn build_same_format_provider_request_body(
};
match input.family {
SameFormatProviderFamily::Standard => {
let previous_model = provider_request_body.get("model").cloned();
provider_request_body.insert(
"model".to_string(),
Value::String(input.mapped_model.to_string()),
);
if previous_model != Some(Value::String(input.mapped_model.to_string())) {
record_compatibility_edit(
&mut compatibility_edits,
"model",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"rewrote request model to mapped upstream model",
);
}
}
SameFormatProviderFamily::Gemini => {
provider_request_body.remove("model");
if provider_request_body.remove("model").is_some() {
record_compatibility_edit(
&mut compatibility_edits,
"model",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"removed top-level model because Gemini model is carried by the upstream URL",
);
}
}
}
let mut provider_request_body = Value::Object(provider_request_body);
if input.is_claude_code {
let before = compatibility_edits
.is_some()
.then(|| provider_request_body.clone());
crate::claude_code::sanitize_claude_code_request_body(&mut provider_request_body);
if before.is_some_and(|before| before != provider_request_body) {
record_compatibility_edit(
&mut compatibility_edits,
"body",
SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite,
"sanitized Claude Code request body for provider compatibility",
);
}
}
if input.enable_model_directives {
if let Some(source_model) = input.source_model {
let before = compatibility_edits
.is_some()
.then(|| provider_request_body.clone());
aether_ai_formats::apply_model_directive_overrides_from_model(
&mut provider_request_body,
input.provider_api_format,
input.mapped_model,
source_model,
);
if before.is_some_and(|before| before != provider_request_body) {
record_compatibility_edit(
&mut compatibility_edits,
"model_directives",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"applied model directive request body overrides",
);
}
}
}
let before_body_rules = compatibility_edits
.is_some()
.then(|| provider_request_body.clone());
if !apply_local_body_rules_with_request_headers(
&mut provider_request_body,
input.body_rules,
@@ -231,28 +322,73 @@ pub fn build_same_format_provider_request_body(
) {
return None;
}
if before_body_rules.is_some_and(|before| before != provider_request_body) {
record_compatibility_edit(
&mut compatibility_edits,
"body_rules",
SameFormatProviderCompatibilityEditAction::OperatorRule,
"applied configured provider body rules",
);
}
if matches!(input.family, SameFormatProviderFamily::Gemini)
&& aether_ai_formats::api_format_alias_matches(
input.provider_api_format,
"gemini:generate_content",
)
{
strip_gemini_function_response_ids(&mut provider_request_body);
let stripped = strip_gemini_function_response_ids(&mut provider_request_body);
if stripped > 0 {
record_compatibility_edit(
&mut compatibility_edits,
"contents[].parts[].functionResponse.id",
SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite,
format!(
"stripped {stripped} Gemini functionResponse id field(s) rejected by upstreams"
),
);
}
}
let require_body_stream_field = input.force_body_stream_field
|| input
.body_json
.as_object()
.is_some_and(|object| object.contains_key("stream"));
let previous_stream = provider_request_body.get("stream").cloned();
aether_ai_formats::enforce_request_body_stream_field(
&mut provider_request_body,
input.provider_api_format,
input.upstream_is_stream,
require_body_stream_field,
);
if previous_stream != provider_request_body.get("stream").cloned() {
record_compatibility_edit(
&mut compatibility_edits,
"stream",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
format!(
"enforced provider stream policy with upstream_is_stream={}",
input.upstream_is_stream
),
);
}
Some(provider_request_body)
}
fn record_compatibility_edit(
edits: &mut Option<&mut Vec<SameFormatProviderCompatibilityEdit>>,
field: impl Into<String>,
action: SameFormatProviderCompatibilityEditAction,
detail: impl Into<String>,
) {
if let Some(edits) = edits.as_mut() {
edits.push(SameFormatProviderCompatibilityEdit {
field: field.into(),
action,
detail: detail.into(),
});
}
}
fn embedding_multimodal_input_requires_aliyun_provider(
client_api_format: &str,
provider_api_format: &str,
@@ -278,25 +414,31 @@ fn embedding_content_is_multimodal(value: &Value) -> bool {
})
}
fn strip_gemini_function_response_ids(value: &mut Value) {
fn strip_gemini_function_response_ids(value: &mut Value) -> usize {
match value {
Value::Object(object) => {
let mut stripped = 0;
for key in ["functionResponse", "function_response"] {
if let Some(function_response) = object.get_mut(key).and_then(Value::as_object_mut)
{
function_response.remove("id");
if function_response.remove("id").is_some() {
stripped += 1;
}
}
}
for child in object.values_mut() {
strip_gemini_function_response_ids(child);
stripped += strip_gemini_function_response_ids(child);
}
stripped
}
Value::Array(items) => {
let mut stripped = 0;
for item in items {
strip_gemini_function_response_ids(item);
stripped += strip_gemini_function_response_ids(item);
}
stripped
}
_ => {}
_ => 0,
}
}
@@ -1128,6 +1270,77 @@ mod tests {
assert_eq!(function_response["name"], "lookup_snake");
}
#[test]
fn same_format_body_report_records_provider_compatibility_edits() {
let output = build_same_format_provider_request_body_with_compatibility_report(
SameFormatProviderRequestBodyInput {
body_json: &json!({
"model": "client-model",
"contents": [
{
"role": "user",
"parts": [
{
"functionResponse": {
"id": "call_123",
"name": "lookup",
"response": {"ok": true}
}
},
{
"function_response": {
"id": "call_456",
"name": "lookup_snake",
"response": {"ok": true}
}
}
]
}
],
"stream": true
}),
mapped_model: "gemini-upstream",
client_api_format: "gemini:generate_content",
provider_api_format: "gemini:generate_content",
source_model: Some("client-model"),
family: SameFormatProviderFamily::Gemini,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
},
)
.expect("same-format body should build");
assert!(output.body.get("model").is_none());
assert!(output.body.get("stream").is_none());
assert!(output
.body
.pointer("/contents/0/parts/0/functionResponse/id")
.is_none());
assert!(output
.body
.pointer("/contents/0/parts/1/function_response/id")
.is_none());
assert!(output.compatibility_edits.iter().any(|edit| {
edit.field == "model"
&& edit.action == SameFormatProviderCompatibilityEditAction::RuntimeRewrite
}));
assert!(output.compatibility_edits.iter().any(|edit| {
edit.field == "stream"
&& edit.action == SameFormatProviderCompatibilityEditAction::RuntimeRewrite
}));
assert!(output.compatibility_edits.iter().any(|edit| {
edit.field == "contents[].parts[].functionResponse.id"
&& edit.action
== SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite
&& edit.detail.contains("2")
}));
}
#[test]
fn same_format_stream_policy_wins_after_body_rules() {
let body_rules = json!([