fix(gemini): normalize mixed tools for same-format providers

This commit is contained in:
ZheFox
2026-08-29 01:08:01 +08:00
parent 9461b1004f
commit 1bc2287baa
3 changed files with 88 additions and 0 deletions
@@ -218,6 +218,18 @@ fn enable_server_side_tool_invocations_for_mixed_tools(
if !gemini_model_supports_mixed_tools(mapped_model) {
return None;
}
ensure_server_side_tool_invocations_for_mixed_tools(output)
}
pub fn ensure_server_side_tool_invocations_for_mixed_tools(output: &mut Value) -> Option<()> {
let output_object = output.as_object_mut()?;
let tools = output_object.get("tools").and_then(Value::as_array);
let Some(tools) = tools else {
return Some(());
};
if !gemini_tools_are_mixed(tools) {
return Some(());
}
let tool_config = output_object
.entry("toolConfig".to_string())
+1
View File
@@ -12,6 +12,7 @@ pub use formats::context::{
ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, FormatContext,
FormatError,
};
pub use formats::gemini::generate_content::request::ensure_server_side_tool_invocations_for_mixed_tools;
pub use formats::id::{
api_format_alias_matches, api_format_defaults_to_client_error_failover,
api_format_defaults_to_non_stream, api_format_permission_covers,
@@ -413,6 +413,27 @@ fn build_same_format_provider_request_body_inner(
"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",
)
{
let before_mixed_tool_compatibility = compatibility_edits
.is_some()
.then(|| provider_request_body.clone());
aether_ai_formats::ensure_server_side_tool_invocations_for_mixed_tools(
&mut provider_request_body,
)?;
if before_mixed_tool_compatibility.is_some_and(|before| before != provider_request_body) {
record_compatibility_edit(
&mut compatibility_edits,
"toolConfig.includeServerSideToolInvocations",
SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite,
"enabled Gemini server-side tool invocations for mixed built-in and function tools",
);
}
}
if matches!(input.family, SameFormatProviderFamily::Gemini)
&& aether_ai_formats::api_format_alias_matches(
input.provider_api_format,
@@ -2057,6 +2078,60 @@ mod tests {
assert_eq!(function_response["name"], "lookup_snake");
}
#[test]
fn same_format_gemini_body_enables_mixed_tool_invocations() {
let output = build_same_format_provider_request_body_with_compatibility_report(
SameFormatProviderRequestBodyInput {
body_json: &json!({
"contents": [{
"role": "user",
"parts": [{"text": "Search, then save the result."}]
}],
"tools": [
{"googleSearch": {}},
{
"functionDeclarations": [{
"name": "save_result",
"parameters": {"type": "object"}
}]
}
],
"toolConfig": {
"functionCallingConfig": {"mode": "AUTO"}
}
}),
mapped_model: "gemini-3.7-flash",
client_api_format: "gemini:generate_content",
provider_api_format: "gemini:generate_content",
source_model: Some("gemini-3.7-flash"),
family: SameFormatProviderFamily::Gemini,
body_rules: None,
request_headers: None,
upstream_is_stream: true,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
},
)
.expect("same-format Gemini body should build");
assert_eq!(output.body["tools"][0]["googleSearch"], json!({}));
assert_eq!(
output.body["tools"][1]["functionDeclarations"][0]["name"],
"save_result"
);
assert_eq!(
output.body["toolConfig"]["includeServerSideToolInvocations"],
true
);
assert!(output.compatibility_edits.iter().any(|edit| {
edit.field == "toolConfig.includeServerSideToolInvocations"
&& edit.action
== SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite
}));
}
#[test]
fn same_format_body_report_records_provider_compatibility_edits() {
let output = build_same_format_provider_request_body_with_compatibility_report(