fix(antigravity): harden tool schemas and Claude thought replay

Preserve tool schemas across provider-scoped format conversion and OpenAI gateway planners until the Antigravity boundary. Bound reference expansion, safely merge schema constraints, and lower unsupported Claude unions.

Drop unsigned historical Claude thinking without changing Gemini behavior. Add cross-format regression fixtures and retain the project's original error output policy.

Validation: 945 format tests and 527 transport tests passed; gateway cargo check passed.
This commit is contained in:
wangpengxiang
2026-09-17 15:38:27 +08:00
parent 681ce56c4f
commit 5a55116b62
10 changed files with 1730 additions and 80 deletions
@@ -70,13 +70,23 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
request_headers: &http::HeaderMap,
enable_model_directives: bool,
) -> Option<Value> {
let provider_request_body = surface_build_cross_format_openai_chat_request_body(
body_json,
mapped_model,
provider_api_format,
upstream_is_stream,
enable_model_directives,
)?;
let provider_request_body = if provider_type.trim().eq_ignore_ascii_case("antigravity")
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
{
aether_ai_formats::formats::shared::standard_matrix::build_standard_request_body_with_model_directives(
body_json, "openai:chat", mapped_model, provider_type, provider_api_format,
"", upstream_is_stream, None, user_api_key_id, enable_model_directives,
)?
} 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 =
apply_standard_provider_request_body_rules_with_request_headers(
provider_request_body,
@@ -110,6 +120,42 @@ pub(crate) fn build_cross_format_openai_chat_request_body(
Some(provider_request_body)
}
#[cfg(test)]
mod antigravity_schema_tests {
use super::*;
use serde_json::json;
#[test]
fn antigravity_chat_route_preserves_tool_schema_and_alternate_responses_shape() {
let schema = json!({"type": "object", "properties": {"mode": {"const": "fast"}}});
let body = json!({"model": "client", "messages": [{"role": "user", "content": "hi"}],
"tools": [{"type": "function", "function": {"name": "probe", "parameters": schema}}]});
let responses_body = json!({"model": "client", "input": "hi",
"tools": [{"type": "function", "name": "probe", "parameters": schema}]});
for input in [body, responses_body] {
for provider in ["antigravity", "gemini"] {
let output = build_cross_format_openai_chat_request_body(
&input,
"claude-test",
provider,
"gemini:generate_content",
true,
false,
None,
None,
&http::HeaderMap::new(),
false,
)
.unwrap();
let parameters = &output["tools"][0]["functionDeclarations"][0]["parameters"];
assert_eq!(parameters == &schema, provider == "antigravity");
assert!(output.get("stream").is_none());
assert_eq!(output["contents"][0]["parts"][0]["text"], "hi");
}
}
}
}
pub(crate) fn build_cross_format_openai_chat_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
@@ -214,15 +214,25 @@ pub(crate) fn build_cross_format_openai_responses_request_body_with_codex_model_
model_capabilities: Option<&crate::ai_serving::CodexResponsesModelCapabilities>,
enable_model_directives: bool,
) -> Option<Value> {
let provider_request_body = 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 provider_request_body = if provider_type.trim().eq_ignore_ascii_case("antigravity")
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
{
aether_ai_formats::formats::shared::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,
)?
} else {
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 =
apply_standard_provider_request_body_rules_with_request_headers(
provider_request_body,
@@ -274,6 +284,41 @@ pub(crate) fn build_local_openai_responses_upstream_url(
)
}
#[cfg(test)]
mod antigravity_schema_tests {
use super::*;
use serde_json::json;
#[test]
fn antigravity_responses_route_preserves_tool_schema_without_changing_public_gemini() {
let schema = json!({"type": "object", "properties": {"mode": {"const": "fast"}}});
let input = json!({"model": "client", "input": "hi",
"tools": [{"type": "function", "name": "probe", "parameters": schema}]});
for provider in ["antigravity", "gemini"] {
let output =
build_cross_format_openai_responses_request_body_with_codex_model_capabilities(
&input,
"claude-test",
"openai:responses",
"gemini:generate_content",
true,
false,
provider,
None,
&http::HeaderMap::new(),
Some("antigravity-schema-test"),
None,
false,
)
.unwrap();
let parameters = &output["tools"][0]["functionDeclarations"][0]["parameters"];
assert_eq!(parameters == &schema, provider == "antigravity");
assert!(output.get("stream").is_none());
assert_eq!(output["contents"][0]["parts"][0]["text"], "hi");
}
}
}
pub(crate) fn build_cross_format_openai_responses_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,