mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-03 16:07:46 +08:00
fix(ai-formats): keep client-declared search tools as Gemini function declarations
canonical_tools_to_gemini promoted any tool whose name normalized to "websearch" / "googlesearch" / "websearchpreview" into Gemini's server-side builtin, dropping it from functionDeclarations. Claude Code declares an ordinary client-side `WebSearch` tool with a full input_schema, so every /v1/messages request routed to a Gemini model lost that declaration and gained `googleSearch` (rewritten to `googleSearchRetrieval` at the Anti Gravity transport boundary) instead. Two consequences, both observed on stabey-124 against gemini-3.8-flash: - the model can never emit a `WebSearch` tool_use, so the client's own web search is dead on that route; - when the model does reach for the injected server-side search, the v1internal backend answers `finishReason: MALFORMED_FUNCTION_CALL` / "Function call is empty - no input to parse." and the turn fails. Promote a tool to a builtin only when it is a bare marker carrying no schema. A declared schema means the caller intends to execute the call itself, which matches CLIProxyAPI: it keys builtins off Claude's `type: web_search_*` or an explicit `google_search` tool key and never off a function name. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -717,7 +717,7 @@ fn canonical_tools_to_gemini(canonical: &CanonicalRequest) -> Option<Value> {
|
|||||||
let mut url_context = false;
|
let mut url_context = false;
|
||||||
|
|
||||||
for tool in &canonical.tools {
|
for tool in &canonical.tools {
|
||||||
match normalize_gemini_builtin_tool_name(&tool.name) {
|
match canonical_tool_builtin_gemini_name(tool) {
|
||||||
Some("googleSearch") => {
|
Some("googleSearch") => {
|
||||||
google_search = true;
|
google_search = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -980,6 +980,25 @@ fn compact_gemini_contents(contents: Vec<Value>) -> Vec<Value> {
|
|||||||
compact
|
compact
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Promote a canonical tool to a Gemini builtin only when it is a bare marker.
|
||||||
|
///
|
||||||
|
/// Clients declare ordinary function tools whose names collide with the builtin
|
||||||
|
/// spellings — Claude Code ships a client-side `WebSearch` tool with a full
|
||||||
|
/// `input_schema`. Matching on the name alone dropped those declarations and
|
||||||
|
/// replaced them with server-side grounding, so the model could never call the
|
||||||
|
/// tool the client actually implements. A declared schema means the caller
|
||||||
|
/// expects to execute the call itself, so such tools stay function declarations.
|
||||||
|
fn canonical_tool_builtin_gemini_name(tool: &CanonicalToolDefinition) -> Option<&'static str> {
|
||||||
|
if tool
|
||||||
|
.parameters
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|parameters| !parameters.is_null())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
normalize_gemini_builtin_tool_name(&tool.name)
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_gemini_builtin_tool_name(name: &str) -> Option<&'static str> {
|
fn normalize_gemini_builtin_tool_name(name: &str) -> Option<&'static str> {
|
||||||
match name
|
match name
|
||||||
.trim()
|
.trim()
|
||||||
@@ -1323,4 +1342,56 @@ mod tests {
|
|||||||
assert!(to_raw(&canonical, "gemini-2.5-pro", false).is_none());
|
assert!(to_raw(&canonical, "gemini-2.5-pro", false).is_none());
|
||||||
assert!(to_raw(&canonical, "gemini-3-flash-preview", false).is_some());
|
assert!(to_raw(&canonical, "gemini-3-flash-preview", false).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_declared_web_search_tool_stays_a_function_declaration() {
|
||||||
|
let canonical = CanonicalRequest {
|
||||||
|
model: "gemini-3-flash-preview".to_string(),
|
||||||
|
tools: vec![CanonicalToolDefinition {
|
||||||
|
name: "WebSearch".to_string(),
|
||||||
|
description: Some("Search the web".to_string()),
|
||||||
|
parameters: Some(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"query": {"type": "string"}},
|
||||||
|
"required": ["query"],
|
||||||
|
})),
|
||||||
|
strict: None,
|
||||||
|
extensions: BTreeMap::new(),
|
||||||
|
}],
|
||||||
|
..CanonicalRequest::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tools = canonical_tools_to_gemini(&canonical).expect("tools should be emitted");
|
||||||
|
let tools = tools.as_array().expect("tools should be an array");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
tools.iter().all(|tool| tool.get("googleSearch").is_none()),
|
||||||
|
"a client tool named WebSearch must not become server-side grounding: {tools:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tools[0]["functionDeclarations"][0]["name"], "WebSearch",
|
||||||
|
"the client declaration must survive: {tools:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schemaless_builtin_tool_name_still_maps_to_google_search() {
|
||||||
|
let canonical = CanonicalRequest {
|
||||||
|
model: "gemini-3-flash-preview".to_string(),
|
||||||
|
tools: vec![CanonicalToolDefinition {
|
||||||
|
name: "google_search".to_string(),
|
||||||
|
description: None,
|
||||||
|
parameters: None,
|
||||||
|
strict: None,
|
||||||
|
extensions: BTreeMap::new(),
|
||||||
|
}],
|
||||||
|
..CanonicalRequest::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tools = canonical_tools_to_gemini(&canonical).expect("tools should be emitted");
|
||||||
|
let tools = tools.as_array().expect("tools should be an array");
|
||||||
|
|
||||||
|
assert_eq!(tools.len(), 1, "{tools:?}");
|
||||||
|
assert_eq!(tools[0]["googleSearch"], json!({}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1951,6 +1951,66 @@ mod tests {
|
|||||||
assert_eq!(converted["tools"][0]["googleSearch"], json!({}));
|
assert_eq!(converted["tools"][0]["googleSearch"], json!({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claude_client_web_search_tool_survives_conversion_to_gemini() {
|
||||||
|
let request = json!({
|
||||||
|
"model": "gemini-3-flash-preview",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"messages": [{"role": "user", "content": "find the release notes"}],
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "WebSearch",
|
||||||
|
"description": "Search the web and use the results to inform responses",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"query": {"type": "string"}},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read",
|
||||||
|
"description": "Read a file",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"file_path": {"type": "string"}},
|
||||||
|
"required": ["file_path"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let converted = build_standard_request_body(
|
||||||
|
&request,
|
||||||
|
"claude:messages",
|
||||||
|
"gemini-3-flash-preview",
|
||||||
|
"google",
|
||||||
|
"gemini:generate_content",
|
||||||
|
"/v1/messages",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("claude messages should convert to gemini");
|
||||||
|
|
||||||
|
let tools = converted["tools"]
|
||||||
|
.as_array()
|
||||||
|
.expect("tools should be an array");
|
||||||
|
assert!(
|
||||||
|
tools.iter().all(|tool| tool.get("googleSearch").is_none()
|
||||||
|
&& tool.get("googleSearchRetrieval").is_none()),
|
||||||
|
"a client-declared WebSearch tool must not become server-side grounding: {tools:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let declared: Vec<&str> = tools
|
||||||
|
.iter()
|
||||||
|
.filter_map(|tool| tool.get("functionDeclarations"))
|
||||||
|
.filter_map(Value::as_array)
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|declaration| declaration.get("name").and_then(Value::as_str))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(declared, vec!["WebSearch", "Read"], "{tools:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_claude_request_from_openai_chat_with_thinking_and_data_url_image() {
|
fn builds_claude_request_from_openai_chat_with_thinking_and_data_url_image() {
|
||||||
let request = json!({
|
let request = json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user