fix(translator): coerce Gemini functionDeclaration parameters to an OBJECT schema (#3357) (#3360)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-07 07:02:23 -03:00
committed by GitHub
parent 8f7172122c
commit 95cdde5f90
3 changed files with 104 additions and 9 deletions

View File

@@ -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 <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)

View File

@@ -25,6 +25,29 @@ function isRecord(value: unknown): value is Record<string, unknown> {
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<string, unknown> {
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)),
});
}
}

View File

@@ -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<typeof buildGeminiTools>): 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"]);
});
});