mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 12:40:20 +08:00
feat(format): audit same-format compatibility rewrites
This commit is contained in:
@@ -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!(
|
||||
|
||||
@@ -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!([
|
||||
|
||||
Reference in New Issue
Block a user