mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(ai-serving): route provider-aware normalization through root seams
Move Antigravity schema-preservation policy into the format crate and expose provider-aware Chat and Responses builders through the existing gateway root seam. Preserve legacy conversion and scoped Responses history behavior without weakening architecture tests. Validated: 61 standalone architecture tests, 947 format tests, 527 transport tests, 2 actual-source planner tests, and gateway cargo check.
This commit is contained in:
@@ -4,7 +4,7 @@ use crate::ai_serving::transport::apply_standard_provider_request_body_rules_wit
|
|||||||
use crate::ai_serving::{
|
use crate::ai_serving::{
|
||||||
apply_codex_openai_responses_chat_body_edits,
|
apply_codex_openai_responses_chat_body_edits,
|
||||||
apply_openai_responses_compact_special_body_edits,
|
apply_openai_responses_compact_special_body_edits,
|
||||||
build_cross_format_openai_chat_request_body_with_model_directives as surface_build_cross_format_openai_chat_request_body,
|
build_cross_format_openai_chat_request_body_with_provider_context as surface_build_cross_format_openai_chat_request_body,
|
||||||
build_local_openai_chat_request_body_with_model_directives as surface_build_local_openai_chat_request_body,
|
build_local_openai_chat_request_body_with_model_directives as surface_build_local_openai_chat_request_body,
|
||||||
GatewayProviderTransportSnapshot,
|
GatewayProviderTransportSnapshot,
|
||||||
};
|
};
|
||||||
@@ -70,23 +70,15 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
|
|||||||
request_headers: &http::HeaderMap,
|
request_headers: &http::HeaderMap,
|
||||||
enable_model_directives: bool,
|
enable_model_directives: bool,
|
||||||
) -> Option<Value> {
|
) -> Option<Value> {
|
||||||
let provider_request_body = if provider_type.trim().eq_ignore_ascii_case("antigravity")
|
let provider_request_body = surface_build_cross_format_openai_chat_request_body(
|
||||||
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
body_json,
|
||||||
== "gemini:generate_content"
|
mapped_model,
|
||||||
{
|
provider_type,
|
||||||
aether_ai_formats::formats::shared::standard_matrix::build_standard_request_body_with_model_directives(
|
provider_api_format,
|
||||||
body_json, "openai:chat", mapped_model, provider_type, provider_api_format,
|
upstream_is_stream,
|
||||||
"", upstream_is_stream, None, user_api_key_id, enable_model_directives,
|
enable_model_directives,
|
||||||
)?
|
user_api_key_id,
|
||||||
} else {
|
)?;
|
||||||
surface_build_cross_format_openai_chat_request_body(
|
|
||||||
body_json,
|
|
||||||
mapped_model,
|
|
||||||
provider_api_format,
|
|
||||||
upstream_is_stream,
|
|
||||||
enable_model_directives,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
let mut provider_request_body =
|
let mut provider_request_body =
|
||||||
apply_standard_provider_request_body_rules_with_request_headers(
|
apply_standard_provider_request_body_rules_with_request_headers(
|
||||||
provider_request_body,
|
provider_request_body,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde_json::Value;
|
|||||||
use crate::ai_serving::transport::apply_standard_provider_request_body_rules_with_request_headers;
|
use crate::ai_serving::transport::apply_standard_provider_request_body_rules_with_request_headers;
|
||||||
use crate::ai_serving::{
|
use crate::ai_serving::{
|
||||||
apply_openai_responses_compact_special_body_edits,
|
apply_openai_responses_compact_special_body_edits,
|
||||||
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope as surface_build_cross_format_openai_responses_request_body,
|
build_cross_format_openai_responses_request_body_with_provider_context as surface_build_cross_format_openai_responses_request_body,
|
||||||
build_local_openai_responses_request_body_with_model_directives as surface_build_local_openai_responses_request_body,
|
build_local_openai_responses_request_body_with_model_directives as surface_build_local_openai_responses_request_body,
|
||||||
GatewayProviderTransportSnapshot,
|
GatewayProviderTransportSnapshot,
|
||||||
};
|
};
|
||||||
@@ -214,25 +214,16 @@ pub(crate) fn build_cross_format_openai_responses_request_body_with_codex_model_
|
|||||||
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
|
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
|
||||||
enable_model_directives: bool,
|
enable_model_directives: bool,
|
||||||
) -> Option<Value> {
|
) -> Option<Value> {
|
||||||
let provider_request_body = if provider_type.trim().eq_ignore_ascii_case("antigravity")
|
let provider_request_body = surface_build_cross_format_openai_responses_request_body(
|
||||||
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
body_json,
|
||||||
== "gemini:generate_content"
|
mapped_model,
|
||||||
{
|
client_api_format,
|
||||||
aether_ai_formats::formats::shared::standard_matrix::build_standard_request_body_with_model_directives(
|
provider_type,
|
||||||
body_json, client_api_format, mapped_model, provider_type, provider_api_format,
|
provider_api_format,
|
||||||
"", upstream_is_stream, None, history_scope, enable_model_directives,
|
upstream_is_stream,
|
||||||
)?
|
enable_model_directives,
|
||||||
} else {
|
history_scope,
|
||||||
surface_build_cross_format_openai_responses_request_body(
|
)?;
|
||||||
body_json,
|
|
||||||
mapped_model,
|
|
||||||
client_api_format,
|
|
||||||
provider_api_format,
|
|
||||||
upstream_is_stream,
|
|
||||||
enable_model_directives,
|
|
||||||
history_scope,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
let mut provider_request_body =
|
let mut provider_request_body =
|
||||||
apply_standard_provider_request_body_rules_with_request_headers(
|
apply_standard_provider_request_body_rules_with_request_headers(
|
||||||
provider_request_body,
|
provider_request_body,
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ pub(crate) use aether_ai_formats::api::{
|
|||||||
build_codex_model_catalog_metadata, build_codex_openai_image_api_provider_request_body,
|
build_codex_model_catalog_metadata, build_codex_openai_image_api_provider_request_body,
|
||||||
build_core_error_body_for_client_format, build_cross_format_openai_chat_request_body,
|
build_core_error_body_for_client_format, build_cross_format_openai_chat_request_body,
|
||||||
build_cross_format_openai_chat_request_body_with_model_directives,
|
build_cross_format_openai_chat_request_body_with_model_directives,
|
||||||
|
build_cross_format_openai_chat_request_body_with_provider_context,
|
||||||
build_cross_format_openai_responses_request_body,
|
build_cross_format_openai_responses_request_body,
|
||||||
build_cross_format_openai_responses_request_body_with_model_directives,
|
build_cross_format_openai_responses_request_body_with_model_directives,
|
||||||
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope,
|
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope,
|
||||||
|
build_cross_format_openai_responses_request_body_with_provider_context,
|
||||||
build_gemini_image_request_body_from_openai_image_request,
|
build_gemini_image_request_body_from_openai_image_request,
|
||||||
build_gemini_image_response_from_openai_image_response,
|
build_gemini_image_response_from_openai_image_response,
|
||||||
build_gemini_image_response_from_openai_responses_image_response, build_generated_tool_call_id,
|
build_gemini_image_response_from_openai_responses_image_response, build_generated_tool_call_id,
|
||||||
|
|||||||
@@ -224,9 +224,11 @@ pub use crate::formats::{
|
|||||||
standard_normalize::{
|
standard_normalize::{
|
||||||
build_cross_format_openai_chat_request_body,
|
build_cross_format_openai_chat_request_body,
|
||||||
build_cross_format_openai_chat_request_body_with_model_directives,
|
build_cross_format_openai_chat_request_body_with_model_directives,
|
||||||
|
build_cross_format_openai_chat_request_body_with_provider_context,
|
||||||
build_cross_format_openai_responses_request_body,
|
build_cross_format_openai_responses_request_body,
|
||||||
build_cross_format_openai_responses_request_body_with_model_directives,
|
build_cross_format_openai_responses_request_body_with_model_directives,
|
||||||
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope,
|
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope,
|
||||||
|
build_cross_format_openai_responses_request_body_with_provider_context,
|
||||||
build_local_openai_chat_request_body,
|
build_local_openai_chat_request_body,
|
||||||
build_local_openai_chat_request_body_with_model_directives,
|
build_local_openai_chat_request_body_with_model_directives,
|
||||||
build_local_openai_responses_request_body,
|
build_local_openai_responses_request_body,
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ use crate::formats::shared::standard_normalize::{
|
|||||||
is_claude_messages_shaped_body_on_openai_chat_endpoint,
|
is_claude_messages_shaped_body_on_openai_chat_endpoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Tool schema preservation is a format-conversion policy, shared by the
|
||||||
|
/// standard matrix and provider-aware Chat/Responses entry points.
|
||||||
|
pub(super) fn preserves_gemini_tool_schemas(
|
||||||
|
provider_type: &str,
|
||||||
|
provider_api_format: &str,
|
||||||
|
) -> bool {
|
||||||
|
provider_type.trim().eq_ignore_ascii_case("antigravity")
|
||||||
|
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
||||||
|
== "gemini:generate_content"
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn build_standard_request_body(
|
pub fn build_standard_request_body(
|
||||||
body_json: &Value,
|
body_json: &Value,
|
||||||
@@ -132,9 +143,7 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers_and
|
|||||||
.with_request_path(request_path)
|
.with_request_path(request_path)
|
||||||
.with_upstream_stream(upstream_is_stream);
|
.with_upstream_stream(upstream_is_stream);
|
||||||
format_context.preserve_gemini_tool_schemas =
|
format_context.preserve_gemini_tool_schemas =
|
||||||
provider_type.trim().eq_ignore_ascii_case("antigravity")
|
preserves_gemini_tool_schemas(provider_type, provider_api_format);
|
||||||
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
|
||||||
== "gemini:generate_content";
|
|
||||||
if let Some(history_scope) = user_api_key_id {
|
if let Some(history_scope) = user_api_key_id {
|
||||||
format_context = format_context.with_history_scope(history_scope);
|
format_context = format_context.with_history_scope(history_scope);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,40 @@ pub fn build_cross_format_openai_chat_request_body(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provider-aware entry point for gateway Chat planners. Keep private schema
|
||||||
|
/// conversion policy in the format crate while retaining legacy behavior elsewhere.
|
||||||
|
pub fn build_cross_format_openai_chat_request_body_with_provider_context(
|
||||||
|
body_json: &Value,
|
||||||
|
mapped_model: &str,
|
||||||
|
provider_type: &str,
|
||||||
|
provider_api_format: &str,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
enable_model_directives: bool,
|
||||||
|
history_scope: Option<&str>,
|
||||||
|
) -> Option<Value> {
|
||||||
|
if super::standard_matrix::preserves_gemini_tool_schemas(provider_type, provider_api_format) {
|
||||||
|
return super::standard_matrix::build_standard_request_body_with_model_directives(
|
||||||
|
body_json,
|
||||||
|
"openai:chat",
|
||||||
|
mapped_model,
|
||||||
|
provider_type,
|
||||||
|
provider_api_format,
|
||||||
|
"",
|
||||||
|
upstream_is_stream,
|
||||||
|
None,
|
||||||
|
history_scope,
|
||||||
|
enable_model_directives,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
build_cross_format_openai_chat_request_body_with_model_directives(
|
||||||
|
body_json,
|
||||||
|
mapped_model,
|
||||||
|
provider_api_format,
|
||||||
|
upstream_is_stream,
|
||||||
|
enable_model_directives,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_cross_format_openai_chat_request_body_with_model_directives(
|
pub fn build_cross_format_openai_chat_request_body_with_model_directives(
|
||||||
body_json: &Value,
|
body_json: &Value,
|
||||||
mapped_model: &str,
|
mapped_model: &str,
|
||||||
@@ -342,6 +376,44 @@ pub fn build_cross_format_openai_responses_request_body_with_model_directives(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provider-aware Responses entry point; preserve history scoping and defer
|
||||||
|
/// private tool schema lowering without exposing provider policy to the gateway.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn build_cross_format_openai_responses_request_body_with_provider_context(
|
||||||
|
body_json: &Value,
|
||||||
|
mapped_model: &str,
|
||||||
|
client_api_format: &str,
|
||||||
|
provider_type: &str,
|
||||||
|
provider_api_format: &str,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
enable_model_directives: bool,
|
||||||
|
history_scope: Option<&str>,
|
||||||
|
) -> Option<Value> {
|
||||||
|
if super::standard_matrix::preserves_gemini_tool_schemas(provider_type, provider_api_format) {
|
||||||
|
return super::standard_matrix::build_standard_request_body_with_model_directives(
|
||||||
|
body_json,
|
||||||
|
client_api_format,
|
||||||
|
mapped_model,
|
||||||
|
provider_type,
|
||||||
|
provider_api_format,
|
||||||
|
"",
|
||||||
|
upstream_is_stream,
|
||||||
|
None,
|
||||||
|
history_scope,
|
||||||
|
enable_model_directives,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope(
|
||||||
|
body_json,
|
||||||
|
mapped_model,
|
||||||
|
client_api_format,
|
||||||
|
provider_api_format,
|
||||||
|
upstream_is_stream,
|
||||||
|
enable_model_directives,
|
||||||
|
history_scope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope(
|
pub fn build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope(
|
||||||
body_json: &Value,
|
body_json: &Value,
|
||||||
mapped_model: &str,
|
mapped_model: &str,
|
||||||
@@ -433,6 +505,145 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_context_builders_preserve_private_schemas_and_legacy_routes() {
|
||||||
|
use crate::api::{
|
||||||
|
build_cross_format_openai_chat_request_body_with_provider_context as chat,
|
||||||
|
build_cross_format_openai_responses_request_body_with_provider_context as responses,
|
||||||
|
};
|
||||||
|
let schema = json!({"type":"object", "properties":{"mode":{"const":"fast"}}});
|
||||||
|
let chat_input = json!({"model":"client", "messages":[{"role":"user","content":"hi"}],
|
||||||
|
"tools":[{"type":"function","function":{"name":"probe","parameters":schema}}]});
|
||||||
|
let responses_input = json!({"model":"client", "input":"hi",
|
||||||
|
"tools":[{"type":"function","name":"probe","parameters":schema}]});
|
||||||
|
for provider in ["antigravity", " AnTiGrAvItY ", "gemini", "openai"] {
|
||||||
|
for target in [
|
||||||
|
"gemini:generate_content",
|
||||||
|
"claude:messages",
|
||||||
|
"openai:responses",
|
||||||
|
] {
|
||||||
|
for stream in [false, true] {
|
||||||
|
for directives in [false, true] {
|
||||||
|
for input in [&chat_input, &responses_input] {
|
||||||
|
let actual = chat(
|
||||||
|
input,
|
||||||
|
"claude-test",
|
||||||
|
provider,
|
||||||
|
target,
|
||||||
|
stream,
|
||||||
|
directives,
|
||||||
|
Some("seam-test"),
|
||||||
|
);
|
||||||
|
let expected =
|
||||||
|
if super::super::standard_matrix::preserves_gemini_tool_schemas(
|
||||||
|
provider, target,
|
||||||
|
) {
|
||||||
|
super::super::standard_matrix::build_standard_request_body_with_model_directives(
|
||||||
|
input, "openai:chat", "claude-test", provider, target, "", stream, None, Some("seam-test"), directives)
|
||||||
|
} else {
|
||||||
|
super::build_cross_format_openai_chat_request_body_with_model_directives(
|
||||||
|
input, "claude-test", target, stream, directives)
|
||||||
|
};
|
||||||
|
assert!(actual.is_some(), "chat {provider} {target}");
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
if target == "gemini:generate_content" {
|
||||||
|
assert_eq!(
|
||||||
|
actual.unwrap()["tools"][0]["functionDeclarations"][0]
|
||||||
|
["parameters"]
|
||||||
|
== schema,
|
||||||
|
provider.trim().eq_ignore_ascii_case("antigravity")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let actual = responses(
|
||||||
|
&responses_input,
|
||||||
|
"claude-test",
|
||||||
|
"openai:responses",
|
||||||
|
provider,
|
||||||
|
target,
|
||||||
|
stream,
|
||||||
|
directives,
|
||||||
|
Some("seam-test"),
|
||||||
|
);
|
||||||
|
let expected =
|
||||||
|
if super::super::standard_matrix::preserves_gemini_tool_schemas(
|
||||||
|
provider, target,
|
||||||
|
) {
|
||||||
|
super::super::standard_matrix::build_standard_request_body_with_model_directives(
|
||||||
|
&responses_input, "openai:responses", "claude-test", provider, target, "", stream, None, Some("seam-test"), directives)
|
||||||
|
} else {
|
||||||
|
super::build_cross_format_openai_responses_request_body_with_model_directives_and_history_scope(
|
||||||
|
&responses_input, "claude-test", "openai:responses", target, stream, directives, Some("seam-test"))
|
||||||
|
};
|
||||||
|
// Same-format Responses uses the local builder, not this cross-format API.
|
||||||
|
assert_eq!(
|
||||||
|
actual.is_some(),
|
||||||
|
target != "openai:responses",
|
||||||
|
"responses {provider} {target}"
|
||||||
|
);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_context_builders_keep_scoped_responses_history() {
|
||||||
|
use crate::api::{
|
||||||
|
build_cross_format_openai_chat_request_body_with_provider_context as chat,
|
||||||
|
build_cross_format_openai_responses_request_body_with_provider_context as responses,
|
||||||
|
record_converted_response_history,
|
||||||
|
};
|
||||||
|
let response_id = "resp_provider_context_seam_history";
|
||||||
|
let scope = "provider-context-seam-history";
|
||||||
|
record_converted_response_history(&json!({
|
||||||
|
"needs_conversion":true, "client_api_format":"openai:responses",
|
||||||
|
"provider_api_format":"openai:chat", "api_key_id":scope,
|
||||||
|
"original_request_body":{"model":"client", "input":"first"}
|
||||||
|
}), &json!({"id":response_id, "status":"completed", "output":[{
|
||||||
|
"type":"message", "role":"assistant", "content":[{"type":"output_text", "text":"remembered"}]
|
||||||
|
}]})).expect("seed scoped history");
|
||||||
|
let input = json!({"model":"client", "previous_response_id":response_id, "input":"second"});
|
||||||
|
for use_chat in [false, true] {
|
||||||
|
let build = |history_scope| {
|
||||||
|
if use_chat {
|
||||||
|
chat(
|
||||||
|
&input,
|
||||||
|
"claude-test",
|
||||||
|
"antigravity",
|
||||||
|
"gemini:generate_content",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
history_scope,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
responses(
|
||||||
|
&input,
|
||||||
|
"claude-test",
|
||||||
|
"openai:responses",
|
||||||
|
"antigravity",
|
||||||
|
"gemini:generate_content",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
history_scope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if use_chat {
|
||||||
|
// The legacy Chat alternate-shape path does not hydrate scoped
|
||||||
|
// Responses history. Preserve that behavior during this refactor.
|
||||||
|
assert!(build(Some(scope)).is_none());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let output = build(Some(scope)).expect("expand scoped history");
|
||||||
|
assert_eq!(output["contents"][0]["parts"][0]["text"], "first");
|
||||||
|
assert_eq!(output["contents"][1]["parts"][0]["text"], "remembered");
|
||||||
|
assert_eq!(output["contents"][2]["parts"][0]["text"], "second");
|
||||||
|
assert!(build(Some("different-seam-key")).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn object_keys(value: &Value) -> Vec<&str> {
|
fn object_keys(value: &Value) -> Vec<&str> {
|
||||||
value
|
value
|
||||||
.as_object()
|
.as_object()
|
||||||
|
|||||||
Reference in New Issue
Block a user