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 @@ -36,6 +36,7 @@ _In development — bullets added per PR; finalized at release._
- **fix(providers): Cloudflare Workers AI model discovery shows model names, not UUIDs** — importing a Cloudflare Workers AI key listed models with internal UUID identifiers (e.g. `429b9e8b-d99e-…`) instead of their usable slugs (`@cf/meta/llama-3.1-8b-instruct`). Cloudflare's `/ai/models/search` returns `{ id: "<uuid>", name: "@cf/…" }`, and discovery was passing the raw objects through — so the UUID `id` became the callable model id. The `cloudflare-ai` discovery now maps each result's `name` → id, surfacing the real `@cf/…` model ids. ([#4259](https://github.com/diegosouzapw/OmniRoute/issues/4259) — thanks @FerLuisxd)
- **fix(translator): clamp Responses API `call_id` to 64 characters** — the OpenAI Responses API rejects `call_id` values longer than 64 characters with a 400. Long upstream tool-call ids (some clients emit ids well over the limit) are now clamped deterministically on both the `function_call` item and its matching `function_call_output`, so the pair stays matched through the orphaned-output filter and the request is accepted. (thanks @anuragg-saxenaa, @ngapngap)
- **fix(oauth): GitHub Copilot token refresh now sends the public client_id** — the `github` provider config never carried a `clientId`, so GitHub OAuth `refresh_token` exchanges either omitted `client_id` or sent the literal string `undefined` (and a bogus `client_secret=undefined`), which GitHub rejects — leaving a Copilot connection stuck once its short-lived token expired and the long-lived refresh path was needed. The provider now resolves its public device-flow `client_id` from the embedded public credential and omits `client_secret` entirely (GitHub's Copilot app is a public client with no secret). (thanks @baslr)
- **fix(translator): a tool property named `pattern` survives Gemini/Antigravity schema sanitization** — the Gemini schema sanitizer strips JSON-Schema constraint keywords Gemini rejects (`pattern`, `minLength`, …) at every nesting level, but it also deleted any tool **property** literally *named* one of those keywords. glob/grep tools declare a property called `pattern`, so on `ag/*` (Antigravity) backends that argument (and its `required` entry) was silently dropped, breaking the tools. Keyword stripping is now position-aware: it only removes constraint keywords at the schema-node level and never against the user-defined names inside a `properties` map. A genuine string-level `pattern` *constraint* is still stripped. (thanks @youthanh)
- **fix(translator): MCP `namespace` tools flatten to individual functions on the Responses→Chat path** — when a Codex CLI client routes a Responses-API request to a non-Codex backend (e.g. `kr/claude-opus-4.7`), each MCP server is declared as a `namespace` tool (`{ type:"namespace", name, tools:[…] }`). The Responses→Chat translator had no `namespace` branch, so the whole group collapsed into a single empty-schema function named `mcp__<server>__` and every MCP call returned `unsupported call: mcp__<server>__`, breaking all MCP-based workflows (context7, codegraph, custom MCPs) for that combination. The translator now expands a namespace into one Chat function per sub-tool (preserving each sub-tool's name and parameters); an empty namespace yields no tools instead of a broken placeholder. The native Codex passthrough path was already correct. (thanks @V13t4nh)

---
Expand Down
32 changes: 21 additions & 11 deletions open-sse/translator/helpers/geminiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,19 +331,29 @@ function removeUnsupportedKeywords(obj: unknown, keywords: Set<string>): void {
for (const item of obj) {
removeUnsupportedKeywords(item, keywords);
}
} else {
const record = obj as JsonRecord;
// Delete unsupported keys at current level
for (const key of Object.keys(record)) {
if (keywords.has(key) || key.startsWith("x-")) {
delete record[key];
}
return;
}

const record = obj as JsonRecord;
// Delete unsupported *constraint* keywords at the current schema level.
for (const key of Object.keys(record)) {
if (keywords.has(key) || key.startsWith("x-")) {
delete record[key];
}
// Recurse into remaining values
for (const value of Object.values(record)) {
if (value && typeof value === "object") {
removeUnsupportedKeywords(value, keywords);
}
// Recurse into remaining values. `properties` is a map keyed by arbitrary,
// user-defined property NAMES — a tool may legitimately declare a property
// called `pattern`, `enum`, `minLength`, etc. Descend into each property's
// subschema, but never run keyword-deletion against the property names
// themselves, or glob/grep-style tools lose their `pattern` argument (#1368).
for (const [key, value] of Object.entries(record)) {
if (!value || typeof value !== "object") continue;
if (key === "properties" && !Array.isArray(value)) {
for (const subSchema of Object.values(value as JsonRecord)) {
removeUnsupportedKeywords(subSchema, keywords);
}
} else {
removeUnsupportedKeywords(value, keywords);
}
}
}
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/translator-gemini-schema-pattern-property.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";

// Regression for port-from-9router#1368: the Gemini/Antigravity schema sanitizer
// strips JSON-Schema *constraint* keywords (pattern, minLength, ...) that Gemini
// rejects. But it must NOT strip a tool property that is merely *named* like one of
// those keywords. glob/grep tools declare a property literally called `pattern`;
// deleting it (and then dropping it from `required`) made those tools unusable on
// `ag/*` (Antigravity) backends.
const { cleanJSONSchemaForAntigravity } = await import(
"../../open-sse/translator/helpers/geminiHelper.ts"
);

test("#1368: a property named 'pattern' survives Gemini schema sanitization", () => {
// Mirrors the grep tool's input schema (property name === constraint keyword).
const grepParams = {
type: "object",
properties: {
pattern: { type: "string", description: "The regex pattern to search for" },
path: { type: "string", description: "Directory to search in" },
},
required: ["pattern", "path"],
};

const cleaned = cleanJSONSchemaForAntigravity(grepParams) as {
properties: Record<string, unknown>;
required?: string[];
};

// The `pattern` *property* must be preserved (it is a tool argument, not a
// string-validation constraint on the object schema itself).
assert.ok(
cleaned.properties && Object.prototype.hasOwnProperty.call(cleaned.properties, "pattern"),
"expected `properties.pattern` to be preserved"
);
assert.equal((cleaned.properties.pattern as { type?: string }).type, "string");
// And it must remain in `required` (cleanupRequired drops names with no property).
assert.ok(
Array.isArray(cleaned.required) && cleaned.required.includes("pattern"),
"expected `required` to still include `pattern`"
);
});

test("#1368: a string-level `pattern` CONSTRAINT is still stripped", () => {
// When `pattern` is an actual validation constraint on a string schema node,
// Gemini does not support it, so it must still be removed.
const schema = {
type: "object",
properties: {
code: { type: "string", pattern: "^[A-Z]{3}$", description: "country code" },
},
required: ["code"],
};

const cleaned = cleanJSONSchemaForAntigravity(schema) as {
properties: { code: Record<string, unknown> };
};

assert.ok(cleaned.properties.code, "the `code` property itself survives");
assert.equal(
Object.prototype.hasOwnProperty.call(cleaned.properties.code, "pattern"),
false,
"the string `pattern` constraint must be stripped"
);
});
Loading