Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 26 additions & 9 deletions open-sse/translator/helpers/geminiToolsSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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)),
});
}
}
Expand Down
77 changes: 77 additions & 0 deletions tests/unit/gemini-tool-params-object-3357.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
});