From 3498deb80531b387b2ec990458ee3d4dc0c8a647 Mon Sep 17 00:00:00 2001 From: AhooraZen Date: Tue, 18 Aug 2026 13:35:49 +0330 Subject: [PATCH] fix(gemini): sanitize schema keywords in function responses to prevent 400 --- open-sse/translator/formats/gemini.js | 24 +++++- ...test-gemini-tool-response-sanitization.mjs | 78 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 scripts/test-gemini-tool-response-sanitization.mjs diff --git a/open-sse/translator/formats/gemini.js b/open-sse/translator/formats/gemini.js index 6393a78b3a..a60a4440b7 100644 --- a/open-sse/translator/formats/gemini.js +++ b/open-sse/translator/formats/gemini.js @@ -109,9 +109,29 @@ export function extractTextContent(content, separator = "") { return ""; } -// Try parse JSON safely (null fallback on parse error; re-export keeps legacy API) +// Sanitize parsed JSON keys for Gemini function response +// Gemini rejects keys starting with $, #, /, or definitions because they get parsed as protobuf schema references +export function sanitizeFunctionResponseResult(val) { + if (val && typeof val === "object") { + if (Array.isArray(val)) { + return val.map(sanitizeFunctionResponseResult); + } + const out = {}; + for (let [k, v] of Object.entries(val)) { + if (k.startsWith("$") || k === "definitions" || k.includes("/") || k.includes("#")) { + k = k.replace(/^[$#\/]+/, "_").replace(/[\/#$]/g, "_"); + } + out[k] = sanitizeFunctionResponseResult(v); + } + return out; + } + return val; +} + +// Try parse JSON safely and sanitize keys for Gemini compatibility export function tryParseJSON(str) { - return safeParseJSON(str, null); + const res = safeParseJSON(str, null); + return res ? sanitizeFunctionResponseResult(res) : res; } // Generate request ID diff --git a/scripts/test-gemini-tool-response-sanitization.mjs b/scripts/test-gemini-tool-response-sanitization.mjs new file mode 100644 index 0000000000..dc1115bcc6 --- /dev/null +++ b/scripts/test-gemini-tool-response-sanitization.mjs @@ -0,0 +1,78 @@ +import { tryParseJSON, sanitizeFunctionResponseResult } from "../open-sse/translator/formats/gemini.js"; +import { openaiToAntigravityRequest } from "../open-sse/translator/request/openai-to-gemini.js"; + +console.log("Running Gemini/Antigravity function response sanitization test..."); + +// Test 1: tryParseJSON sanitizes $ref, $defs, #, / +const rawSchemaPayload = JSON.stringify({ + "$ref": "#/$defs/Config", + "$defs": { + "Config": { "type": "object", "properties": { "name": { "type": "string" } } } + }, + "deep/nested": { + "$schema": "http://json-schema.org/draft-07/schema#", + "field#tag": 123 + } +}); + +const sanitized = tryParseJSON(rawSchemaPayload); +if ( + sanitized._ref !== "#/$defs/Config" || + !sanitized._defs?.Config || + sanitized["deep_nested"]._schema !== "http://json-schema.org/draft-07/schema#" || + sanitized["deep_nested"].field_tag !== 123 +) { + console.error("Test 1 Failed:", sanitized); + process.exit(1); +} +console.log("✓ Test 1 Passed: tryParseJSON sanitizes forbidden protobuf keys"); + +// Test 2: openaiToAntigravityRequest handles tool responses containing schema references +const mockRequest = { + model: "gemini-3.7-flash-high", + messages: [ + { role: "user", content: "Fetch config" }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_webfetch_1", + type: "function", + function: { + name: "webfetch", + arguments: '{"url":"https://opencode.ai/config.json"}' + } + } + ] + }, + { + role: "tool", + tool_call_id: "call_webfetch_1", + name: "webfetch", + content: rawSchemaPayload + } + ] +}; + +const antigravityRequest = openaiToAntigravityRequest("gemini-3.7-flash-high", mockRequest, false); +const userTurn = antigravityRequest.request.contents.find(c => c.role === "user" && c.parts?.some(p => p.functionResponse)); +if (!userTurn) { + console.error("Test 2 Failed: user turn with functionResponse not found"); + process.exit(1); +} + +const funcResp = userTurn.parts.find(p => p.functionResponse)?.functionResponse; +if (!funcResp) { + console.error("Test 2 Failed: functionResponse part missing"); + process.exit(1); +} + +const res = funcResp.response?.result; +if (res._ref !== "#/$defs/Config" || !res._defs?.Config) { + console.error("Test 2 Failed: response.result not properly sanitized:", res); + process.exit(1); +} +console.log("✓ Test 2 Passed: openaiToAntigravityRequest produces clean functionResponse without protobuf conflict"); + +console.log("All tests passed successfully!");