Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions open-sse/translator/formats/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions scripts/test-gemini-tool-response-sanitization.mjs
Original file line number Diff line number Diff line change
@@ -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!");