From c4e5d586fc9c8bb4e91669456fceefcf25ea2215 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:31:31 -0300 Subject: [PATCH] fix(sse): strip Gemini built-in tools when functionDeclarations present in Antigravity envelope (#4821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green. --- .../translator/request/openai-to-gemini.ts | 40 ++++++- ...strip-builtin-tools-funcdecls-1095.test.ts | 102 ++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/unit/antigravity-strip-builtin-tools-funcdecls-1095.test.ts diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 3042ce12e0..09f1001a16 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -36,6 +36,16 @@ import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiTools // See: https://github.com/keisksw/antigravity-output-analysis const ANTIGRAVITY_CLAUDE_MAX_OUTPUT_TOKENS = 16_384; +// Gemini built-in tool names that Antigravity's v1internal endpoint rejects with a +// 400 when they are mixed with functionDeclarations in the same request. These must +// be stripped from the Cloud Code envelope's functionDeclarations. +const GEMINI_BUILTIN_TOOL_NAMES = new Set([ + "google_search", + "web_search", + "search_web", + "googleSearch", +]); + type GeminiPart = Record; type GeminiContent = { role: string; parts: GeminiPart[] }; @@ -753,8 +763,34 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra envelope.request.systemInstruction = { role: "system", parts: [defaultPart] }; } - // Add toolConfig for Antigravity - if (geminiCLI.tools?.some((tool) => Array.isArray(tool.functionDeclarations))) { + // Strip Gemini built-in tool *names* out of functionDeclarations: Antigravity's + // v1internal endpoint returns 400 when a built-in tool (google_search etc.) is + // mixed with functionDeclarations in the same request. Native grounding entries + // (e.g. `{ googleSearch: {} }`) are left intact; only the functionDeclarations + // arrays are cleaned, and a declarations entry that becomes empty is dropped. + if (envelope.request.tools && envelope.request.tools.length > 0) { + const cleanedTools = envelope.request.tools + .map((tool) => { + if (!Array.isArray(tool.functionDeclarations)) { + return tool; + } + const customDecls = tool.functionDeclarations.filter( + (fn) => !GEMINI_BUILTIN_TOOL_NAMES.has(fn.name) + ); + return { ...tool, functionDeclarations: customDecls }; + }) + .filter( + (tool) => + !Array.isArray(tool.functionDeclarations) || tool.functionDeclarations.length > 0 + ); + envelope.request.tools = cleanedTools.length > 0 ? cleanedTools : undefined; + } + + // Add toolConfig for Antigravity only when custom functionDeclarations remain. + const hasCustomTools = envelope.request.tools?.some( + (tool) => (tool.functionDeclarations?.length ?? 0) > 0 + ); + if (hasCustomTools) { envelope.request.toolConfig = { functionCallingConfig: { mode: "VALIDATED" }, }; diff --git a/tests/unit/antigravity-strip-builtin-tools-funcdecls-1095.test.ts b/tests/unit/antigravity-strip-builtin-tools-funcdecls-1095.test.ts new file mode 100644 index 0000000000..d7ccf8e954 --- /dev/null +++ b/tests/unit/antigravity-strip-builtin-tools-funcdecls-1095.test.ts @@ -0,0 +1,102 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.ts"; + +// Regression guard for the Antigravity v1internal 400 that fires when a Gemini +// built-in tool (google_search / web_search / googleSearch ...) is sent in the +// same request as functionDeclarations. The Cloud Code envelope must strip the +// built-in tool names out of functionDeclarations before dispatch. +// Inspired-by decolua/9router#1095 (author @vanszs). + +type EnvelopeTool = { functionDeclarations?: Array<{ name: string }> }; + +function declaredToolNames(result: { request: { tools?: EnvelopeTool[] } }): string[] { + return (result.request.tools ?? []).flatMap((tool) => + (tool.functionDeclarations ?? []).map((fn) => fn.name) + ); +} + +const CREDS = { projectId: "proj-1" } as never; + +test("Antigravity envelope strips built-in tool names mixed with custom functionDeclarations (#1095)", () => { + const result = openaiToAntigravityRequest( + "gemini-2.5-pro", + { + messages: [{ role: "user", content: "Search and read" }], + tools: [ + { + type: "function", + function: { name: "google_search", parameters: { type: "object", properties: {} } }, + }, + { + type: "function", + function: { name: "read_file", parameters: { type: "object", properties: {} } }, + }, + ], + }, + false, + CREDS + ); + + const names = declaredToolNames(result); + // Built-in tool name must be gone from functionDeclarations... + assert.ok( + !names.includes("google_search") && !names.includes("googleSearch"), + `expected built-in tool name stripped, got ${JSON.stringify(names)}` + ); + // ...while the custom declaration survives. + assert.ok(names.includes("read_file"), `expected custom tool kept, got ${JSON.stringify(names)}`); + // toolConfig stays set because custom declarations remain. + assert.deepEqual(result.request.toolConfig, { + functionCallingConfig: { mode: "VALIDATED" }, + }); +}); + +test("Antigravity envelope keeps a normal custom-only tool request intact (#1095 regression)", () => { + const result = openaiToAntigravityRequest( + "gemini-2.5-pro", + { + messages: [{ role: "user", content: "Hello" }], + tools: [ + { + type: "function", + function: { name: "weather", parameters: { type: "object", properties: {} } }, + }, + ], + }, + false, + CREDS + ); + + const names = declaredToolNames(result); + assert.deepEqual(names, ["weather"]); + assert.deepEqual(result.request.toolConfig, { + functionCallingConfig: { mode: "VALIDATED" }, + }); +}); + +test("Antigravity envelope drops tools + toolConfig when only built-in tools are present (#1095)", () => { + const result = openaiToAntigravityRequest( + "gemini-2.5-pro", + { + messages: [{ role: "user", content: "Search the web" }], + tools: [ + { + type: "function", + function: { name: "google_search", parameters: { type: "object", properties: {} } }, + }, + ], + }, + false, + CREDS + ); + + const names = declaredToolNames(result); + assert.ok( + !names.includes("google_search") && !names.includes("googleSearch"), + `expected no built-in functionDeclarations, got ${JSON.stringify(names)}` + ); + // No custom declarations remain -> no VALIDATED toolConfig is forced. + assert.equal(result.request.toolConfig, undefined); +});