fix(antigravity): send googleSearch instead of the Gemini 1.5 retrieval tool

The transport boundary rewrote `googleSearch` into the Gemini 1.5-era
`googleSearchRetrieval` spelling before every v1internal call, on the stated
grounds that the private backend rejects `googleSearch` when it is combined
with function declarations. That rewrite breaks grounding on Gemini 3.

Observed on stabey-124 against daily-cloudcode-pa.googleapis.com. A controlled
pair, same model and keys, 5 seconds apart:

- no `web_search_options` -> 200
- with `web_search_options` -> 502 on all three candidates

The outgoing body carried `tools: [{"googleSearchRetrieval": {}}]` and no
function declarations at all, so the documented mixed-tool rationale did not
apply. `request_candidates.error_message` holds what the backend actually
said:

    Malformed function call: call:google_search{query:current UTC date time}
    Malformed function call: call:google:search{query:current UTC date}
    Malformed function call: call:google_search{queries:[current UTC date]}

The model reaches for `google_search`, the legacy declaration binds nothing,
and the turn dies unparsed. CLIProxyAPI sends `googleSearch` to this same
v1internal surface, including alongside function declarations.

Keep folding the snake_case `google_search` alias into the canonical
`googleSearch` key, and leave a request that already spells the tool
`googleSearchRetrieval` untouched.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
stabey
2026-09-17 09:23:01 +08:00
co-authored by Claude Opus 5
parent fdf55525f5
commit 6c92db2ba5
@@ -120,11 +120,18 @@ pub fn build_antigravity_safe_v1internal_request(
AntigravityRequestEnvelopeSupport::Supported(envelope) AntigravityRequestEnvelopeSupport::Supported(envelope)
} }
/// Antigravity's private v1internal Gemini surface still uses the legacy /// Antigravity's private v1internal Gemini surface takes the same
/// `googleSearchRetrieval` spelling. The public Gemini converter emits the /// `googleSearch` grounding tool as the public one. Only the snake_case alias
/// newer `googleSearch` spelling, which the private backend rejects when it is /// needs folding into the canonical camelCase key.
/// combined with function declarations. Normalize only at this transport ///
/// boundary so public Gemini requests retain their native shape. /// This used to rewrite `googleSearch` into the Gemini 1.5-era
/// `googleSearchRetrieval` spelling. Gemini 3 rejects that: the model emits a
/// `google_search` call the backend cannot bind to any declared tool, and the
/// turn dies with `MALFORMED_FUNCTION_CALL`, e.g.
/// `Malformed function call: call:google_search{query:current UTC date}`
/// observed against `daily-cloudcode-pa.googleapis.com` with
/// `tools: [{"googleSearchRetrieval": {}}]` and no function declarations.
/// CLIProxyAPI sends `googleSearch` to the same v1internal surface.
fn normalize_antigravity_builtin_tool_names(request: &mut Map<String, Value>) { fn normalize_antigravity_builtin_tool_names(request: &mut Map<String, Value>) {
let Some(tools) = request.get_mut("tools").and_then(Value::as_array_mut) else { let Some(tools) = request.get_mut("tools").and_then(Value::as_array_mut) else {
return; return;
@@ -135,14 +142,9 @@ fn normalize_antigravity_builtin_tool_names(request: &mut Map<String, Value>) {
continue; continue;
}; };
if let Some(payload) = tool_object.remove("googleSearch") {
tool_object
.entry("googleSearchRetrieval".to_string())
.or_insert(payload);
}
if let Some(payload) = tool_object.remove("google_search") { if let Some(payload) = tool_object.remove("google_search") {
tool_object tool_object
.entry("googleSearchRetrieval".to_string()) .entry("googleSearch".to_string())
.or_insert(payload); .or_insert(payload);
} }
} }
@@ -222,6 +224,43 @@ mod tests {
} }
} }
#[test]
fn search_only_request_keeps_the_modern_google_search_spelling() {
// Reproduces the live failure: a grounding-only request (no function
// declarations) that went out as `googleSearchRetrieval` came back as
// `Malformed function call: call:google_search{query:current UTC date}`
// from daily-cloudcode-pa.googleapis.com.
let request_body = json!({
"contents": [
{ "role": "user", "parts": [{ "text": "today's UTC date?" }] }
],
"tools": [{ "googleSearch": {} }]
});
let envelope = match build_antigravity_safe_v1internal_request(
&sample_auth(),
"request-ant-search-1",
"gemini-3.8-flash-high",
&request_body,
AntigravityEnvelopeRequestType::Agent,
) {
AntigravityRequestEnvelopeSupport::Supported(envelope) => envelope,
AntigravityRequestEnvelopeSupport::Unsupported(reason) => {
panic!("search-only envelope should be supported: {reason:?}")
}
};
let tools = envelope["request"]["tools"]
.as_array()
.expect("tools should survive");
assert_eq!(tools.len(), 1, "{tools:?}");
assert_eq!(tools[0]["googleSearch"], json!({}));
assert!(
tools[0].get("googleSearchRetrieval").is_none(),
"the Gemini 1.5 spelling must not be reintroduced: {tools:?}"
);
}
#[test] #[test]
fn real_agent_request_preserves_antigravity_agent_fields() { fn real_agent_request_preserves_antigravity_agent_fields() {
let request_body = json!({ let request_body = json!({
@@ -328,12 +367,9 @@ mod tests {
.get("include_server_side_tool_invocations") .get("include_server_side_tool_invocations")
.is_none()); .is_none());
assert!(envelope["request"]["tools"][0] assert!(envelope["request"]["tools"][0]
.get("googleSearch") .get("googleSearchRetrieval")
.is_none()); .is_none());
assert_eq!( assert_eq!(envelope["request"]["tools"][0]["googleSearch"], json!({}));
envelope["request"]["tools"][0]["googleSearchRetrieval"],
json!({})
);
assert_eq!( assert_eq!(
envelope["request"]["tools"][1]["functionDeclarations"][0]["name"], envelope["request"]["tools"][1]["functionDeclarations"][0]["name"],
"run_command" "run_command"
@@ -471,7 +507,7 @@ mod tests {
.get("google_search") .get("google_search")
.is_none()); .is_none());
assert_eq!( assert_eq!(
envelope["request"]["tools"][0]["googleSearchRetrieval"], envelope["request"]["tools"][0]["googleSearch"],
json!({ json!({
"dynamicRetrievalConfig": { "dynamicRetrievalConfig": {
"mode": "MODE_UNSPECIFIED" "mode": "MODE_UNSPECIFIED"