fix(formats): enable mixed Gemini tool calls

This commit is contained in:
ZheFox
2026-08-28 16:55:38 +08:00
parent 64e5725331
commit f0b0064f3d
2 changed files with 99 additions and 0 deletions
@@ -187,9 +187,60 @@ pub fn to_raw(
) -> Option<Value> {
let mut output = canonical_to_gemini_request_body(canonical, mapped_model, upstream_is_stream)?;
apply_gemini_request_extensions(&mut output, &canonical.extensions)?;
if !canonical_has_raw_gemini_tools(canonical) {
enable_server_side_tool_invocations_for_mixed_tools(&mut output)?;
}
Some(output)
}
fn canonical_has_raw_gemini_tools(canonical: &CanonicalRequest) -> bool {
canonical
.extensions
.get("gemini")
.and_then(Value::as_object)
.is_some_and(|gemini| gemini.contains_key("raw_tools"))
}
fn enable_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(());
};
let has_function_declarations = tools.iter().any(|tool| {
tool.as_object().is_some_and(|tool| {
tool.get("functionDeclarations")
.or_else(|| tool.get("function_declarations"))
.and_then(Value::as_array)
.is_some_and(|declarations| !declarations.is_empty())
})
});
let has_builtin_tools = tools.iter().any(|tool| {
tool.as_object().is_some_and(|tool| {
tool.keys().any(|key| {
!matches!(
key.as_str(),
"functionDeclarations" | "function_declarations"
)
})
})
});
if !has_function_declarations || !has_builtin_tools {
return Some(());
}
let tool_config = output_object
.entry("toolConfig".to_string())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()?;
tool_config.remove("include_server_side_tool_invocations");
tool_config.insert(
"includeServerSideToolInvocations".to_string(),
Value::Bool(true),
);
Some(())
}
fn canonical_to_gemini_request_body(
canonical: &CanonicalRequest,
mapped_model: &str,
@@ -2029,4 +2029,52 @@ mod tests {
"surface conversion should preserve the Claude tool schema before transport envelopes"
);
}
#[test]
fn openai_responses_builtin_and_function_tools_enable_gemini_server_invocations() {
let request = json!({
"model": "gpt-5",
"input": "Search first, then save the result.",
"tools": [
{"type": "web_search_preview"},
{
"type": "function",
"name": "save_result",
"description": "Save a search result",
"parameters": {
"type": "object",
"properties": {
"result": {"type": "string"}
},
"required": ["result"]
}
}
],
"tool_choice": "required"
});
let gemini = build_standard_request_body(
&request,
"openai:responses",
"gemini-2.5-pro",
"google",
"gemini:generate_content",
"/v1/responses",
true,
None,
None,
)
.expect("openai responses should convert to gemini generate content");
assert_eq!(gemini["tools"][0]["googleSearch"], json!({}));
assert_eq!(
gemini["tools"][1]["functionDeclarations"][0]["name"],
"save_result"
);
assert_eq!(
gemini["toolConfig"]["includeServerSideToolInvocations"],
true
);
assert_eq!(gemini["toolConfig"]["functionCallingConfig"]["mode"], "ANY");
}
}