From 95cdde5f90a68be8b7f531be7772f1c03579bfeb Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 7 Jun 2026 07:02:23 -0300 Subject: [PATCH] fix(translator): coerce Gemini functionDeclaration parameters to an OBJECT schema (#3357) (#3360) --- CHANGELOG.md | 1 + .../helpers/geminiToolsSanitizer.ts | 35 ++++++--- .../gemini-tool-params-object-3357.test.ts | 77 +++++++++++++++++++ 3 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 tests/unit/gemini-tool-params-object-3357.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f21716133..42d2637156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🔧 Bug Fixes +- **fix(translator):** every Gemini/Vertex `functionDeclaration.parameters` is now coerced to an OBJECT-typed schema before cleaning. Clients like GitHub Copilot send some tools (e.g. `terminal_last_command`) whose `parameters` is present but lacks a top-level `type: "object"` (just `{ properties }`, a scalar type, or `{}`); these slipped through `buildGeminiTools`' `params || default` guard and Vertex rejected them with `[400] ... functionDeclaration parameters schema should be of type OBJECT`. Hardens every OpenAI→Gemini tool request (Vertex / antigravity / agy / gemini). (#3357 — thanks @nullbytef0x) - **fix(electron):** clicking "Exit" (or applying an update) now terminates the **whole** server process tree, not just the direct child. The embedded server runs as `omniroute.exe`-as-node (`ELECTRON_RUN_AS_NODE`) and spawns grandchildren (embedded services, MITM proxy, tunnels); on Windows `ChildProcess.kill()` only terminates the direct child, so survivors kept `omniroute.exe` locked — the process "hung in memory" after Exit and updates failed with "file in use". New `killProcessTree()` helper uses `taskkill /PID /T /F` on Windows (signal-based on POSIX); wired into `stopNextServer`, the `waitForServerExit` force-kill, and `installUpdate`. (#3347 — thanks @Flexible78) - **fix(proxy):** proxy auto-selection is now **opt-in** (new `PROXY_AUTO_SELECT_ENABLED` flag, default off). Previously a single proxy in the registry silently became a global fallback for **all** provider connections (the Step-11 fallback listed every registry proxy, ignoring assignments and per-connection `proxy_enabled`). It now no-ops unless the operator enables the flag. (#3332 — thanks @hertznsk) - **fix(cli):** write the OpenCode config to `~/.config/opencode/opencode.json` on **all** platforms — on Windows OmniRoute wrote to `%APPDATA%\opencode\` but OpenCode reads from `%USERPROFILE%\.config\opencode\` (XDG), so dashboard-saved config silently had no effect. (#3330 — thanks @abdulkadirozyurt) diff --git a/open-sse/translator/helpers/geminiToolsSanitizer.ts b/open-sse/translator/helpers/geminiToolsSanitizer.ts index 3ed55b880a..4f1ae35e01 100644 --- a/open-sse/translator/helpers/geminiToolsSanitizer.ts +++ b/open-sse/translator/helpers/geminiToolsSanitizer.ts @@ -25,6 +25,29 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +/** + * Gemini/Vertex requires every functionDeclaration.parameters to be an OBJECT-typed schema + * (#3357: "functionDeclaration parameters schema should be of type OBJECT"). Some clients + * (e.g. GitHub Copilot's `terminal_last_command`) send a `parameters` that is present but + * lacks a top-level `type: "object"` — just `{ properties }`, a scalar/array type, or `{}`. + * Coerce the parameters root to an object schema before it is cleaned; a falsy/non-record + * schema becomes an empty object schema. Only the top level is touched — nested property + * schemas are left to cleanJSONSchemaForAntigravity. + */ +function toGeminiParametersSchema(raw: unknown): Record { + if (!isRecord(raw)) { + return { type: "object", properties: {} }; + } + if (raw.type === "object") { + return raw; + } + return { + ...raw, + type: "object", + properties: isRecord(raw.properties) ? raw.properties : {}, + }; +} + function normalizeGeminiToolName( name: string, options: GeminiToolSanitizationOptions = {} @@ -174,9 +197,7 @@ export function buildGeminiTools( functionDeclarations.push({ name: sanitizeGeminiToolName(fn.name, options), description: typeof fn.description === "string" ? fn.description : "", - parameters: cleanJSONSchemaForAntigravity( - fn.parameters || { type: "object", properties: {} } - ), + parameters: cleanJSONSchemaForAntigravity(toGeminiParametersSchema(fn.parameters)), }); } continue; @@ -186,9 +207,7 @@ export function buildGeminiTools( functionDeclarations.push({ name: sanitizeGeminiToolName(rawTool.name, options), description: typeof rawTool.description === "string" ? rawTool.description : "", - parameters: cleanJSONSchemaForAntigravity( - rawTool.input_schema || { type: "object", properties: {} } - ), + parameters: cleanJSONSchemaForAntigravity(toGeminiParametersSchema(rawTool.input_schema)), }); continue; } @@ -202,9 +221,7 @@ export function buildGeminiTools( functionDeclarations.push({ name: sanitizeGeminiToolName(fn.name, options), description: typeof fn.description === "string" ? fn.description : "", - parameters: cleanJSONSchemaForAntigravity( - fn.parameters || { type: "object", properties: {} } - ), + parameters: cleanJSONSchemaForAntigravity(toGeminiParametersSchema(fn.parameters)), }); } } diff --git a/tests/unit/gemini-tool-params-object-3357.test.ts b/tests/unit/gemini-tool-params-object-3357.test.ts new file mode 100644 index 0000000000..e556d64297 --- /dev/null +++ b/tests/unit/gemini-tool-params-object-3357.test.ts @@ -0,0 +1,77 @@ +/** + * Regression test for #3357 — Vertex AI function-calling fails with + * "functionDeclaration parameters schema should be of type OBJECT". + * + * Gemini/Vertex requires every functionDeclaration.parameters to be an OBJECT-typed + * schema. GitHub Copilot sends some tools (e.g. terminal_last_command) whose `parameters` + * is present but lacks a top-level `type: "object"` (just `{ properties }`, a scalar type, + * or `{}`). buildGeminiTools() must coerce the function-parameters root to an object schema + * before cleaning, otherwise the typeless schema reaches Vertex and 400s. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts"; + +function paramsOf(tools: ReturnType): any { + return (tools as any)?.[0]?.functionDeclarations?.[0]?.parameters; +} + +describe("buildGeminiTools — function parameters must be an OBJECT schema (#3357)", () => { + it("coerces a typeless parameters schema ({properties} only) to type:object, keeping props", () => { + const tools = buildGeminiTools([ + { + type: "function", + function: { + name: "terminal_last_command", + parameters: { properties: { cmd: { type: "string" } } }, + }, + }, + ]); + const params = paramsOf(tools); + assert.equal(params.type, "object"); + assert.ok(params.properties && params.properties.cmd, "original properties preserved"); + assert.equal(params.properties.cmd.type, "string"); + }); + + it("coerces a scalar-typed parameters schema to type:object", () => { + const tools = buildGeminiTools([ + { type: "function", function: { name: "weird_tool", parameters: { type: "string" } } }, + ]); + assert.equal(paramsOf(tools).type, "object"); + }); + + it("coerces an empty parameters object to type:object", () => { + const tools = buildGeminiTools([ + { type: "function", function: { name: "no_args", parameters: {} } }, + ]); + assert.equal(paramsOf(tools).type, "object"); + }); + + it("defaults missing parameters to a type:object schema", () => { + const tools = buildGeminiTools([ + { type: "function", function: { name: "bare" } }, + ]); + assert.equal(paramsOf(tools).type, "object"); + }); + + it("leaves an already-valid object parameters schema as type:object with its props", () => { + const tools = buildGeminiTools([ + { + type: "function", + function: { + name: "search", + parameters: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }, + }, + }, + ]); + const params = paramsOf(tools); + assert.equal(params.type, "object"); + assert.equal(params.properties.query.type, "string"); + assert.deepEqual(params.required, ["query"]); + }); +});