mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +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:
@@ -224,9 +224,11 @@ pub use crate::formats::{
|
||||
standard_normalize::{
|
||||
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_provider_context,
|
||||
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_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_with_model_directives,
|
||||
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,
|
||||
};
|
||||
|
||||
/// 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)]
|
||||
pub fn build_standard_request_body(
|
||||
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_upstream_stream(upstream_is_stream);
|
||||
format_context.preserve_gemini_tool_schemas =
|
||||
provider_type.trim().eq_ignore_ascii_case("antigravity")
|
||||
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
||||
== "gemini:generate_content";
|
||||
preserves_gemini_tool_schemas(provider_type, provider_api_format);
|
||||
if let Some(history_scope) = user_api_key_id {
|
||||
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(
|
||||
body_json: &Value,
|
||||
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(
|
||||
body_json: &Value,
|
||||
mapped_model: &str,
|
||||
@@ -433,6 +505,145 @@ mod tests {
|
||||
};
|
||||
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> {
|
||||
value
|
||||
.as_object()
|
||||
|
||||
Reference in New Issue
Block a user