mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-16 04:03:02 +03:00
Compare commits
1 Commits
fix/10365-
...
fix/9617-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c126108a15 |
@@ -1 +0,0 @@
|
||||
- fix(providers): GitLab Duo falls back to the public Code Suggestions endpoint when direct_access returns 401 (#10365)
|
||||
1
changelog.d/fixes/9617-gemini-uniqueitems-strip.md
Normal file
1
changelog.d/fixes/9617-gemini-uniqueitems-strip.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): strip uniqueItems from Gemini tool schemas (Gemini rejects it with 400 'Unknown name uniqueItems') (#9617)
|
||||
@@ -583,20 +583,10 @@ export class GitlabExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
if (input.log) {
|
||||
input.log.warn(
|
||||
"GITLAB-DUO",
|
||||
"direct_access exchange rejected (401); falling back to public completions endpoint"
|
||||
);
|
||||
}
|
||||
return {
|
||||
target: {
|
||||
mode: "monolith",
|
||||
url: endpoints.publicCompletionsUrl,
|
||||
headers: buildMonolithHeaders(credentials.accessToken || null),
|
||||
},
|
||||
target: null,
|
||||
credentials,
|
||||
errorResponse: null,
|
||||
errorResponse: toOpenAIError(401, "GitLab Duo direct access token request was rejected"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([
|
||||
"contains",
|
||||
"minContains",
|
||||
"maxContains",
|
||||
// #9617: array uniqueness keyword — agentic-CLI tool schemas (JSON-Schema
|
||||
// generators) set this routinely and Gemini's schema parser has no field for
|
||||
// it, rejecting the whole request with "Unknown name \"uniqueItems\"".
|
||||
// Upstream 9router already strips it alongside `contains` for the same error.
|
||||
"uniqueItems",
|
||||
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
|
||||
77
tests/unit/9617-gemini-uniqueitems.test.ts
Normal file
77
tests/unit/9617-gemini-uniqueitems.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts";
|
||||
|
||||
// Issue #9617: Gemini rejects `uniqueItems` in function_declarations parameter schemas
|
||||
// with HTTP 400 "Unknown name \"uniqueItems\" ... Cannot find field" (Gemini's protobuf-JSON
|
||||
// schema parser only accepts a subset of JSON Schema/OpenAPI 3.0 — the same class of error
|
||||
// already fixed for `multipleOf`, `minItems`, `maxItems`, `strict`, `encrypted` in
|
||||
// GEMINI_UNSUPPORTED_SCHEMA_KEYS, open-sse/translator/helpers/geminiHelper.ts).
|
||||
test("buildGeminiTools strips uniqueItems from array schemas (issue #9617)", () => {
|
||||
const tools = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "exit_worktree",
|
||||
description: "test tool with an array-of-objects parameter",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
items: {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
action: { type: "string" },
|
||||
},
|
||||
required: ["name", "action"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["items"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const geminiTools = buildGeminiTools(tools);
|
||||
const serialized = JSON.stringify(geminiTools);
|
||||
|
||||
assert.ok(geminiTools, "expected buildGeminiTools to return a tools array");
|
||||
assert.equal(
|
||||
serialized.includes("uniqueItems"),
|
||||
false,
|
||||
`uniqueItems leaked into the Gemini payload (would trigger upstream 400 "Unknown name \\"uniqueItems\\""): ${serialized}`
|
||||
);
|
||||
});
|
||||
|
||||
// Companion: a top-level (non-nested) array property with uniqueItems is also stripped —
|
||||
// matches the reporter's deeply-nested case with extra path coverage.
|
||||
test("buildGeminiTools strips uniqueItems from a top-level array parameter schema (issue #9617)", () => {
|
||||
const tools = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_worktrees",
|
||||
description: "test tool with a top-level array parameter",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["paths"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const serialized = JSON.stringify(buildGeminiTools(tools));
|
||||
assert.equal(serialized.includes("uniqueItems"), false);
|
||||
});
|
||||
@@ -261,54 +261,3 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
// #10365: a 401 from the direct_access exchange must ALSO fall back to the public
|
||||
// Code Suggestions completions endpoint (same resilience as the 403-disabled case
|
||||
// above), instead of surfacing an opaque 401 token error with no fallback.
|
||||
test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access returns 401", async () => {
|
||||
const executor = getExecutor("gitlab-duo") as GitlabExecutor;
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
calls.push(String(url));
|
||||
|
||||
if (String(url) === "https://gitlab.example.com/api/v4/code_suggestions/direct_access") {
|
||||
return jsonResponse({ error: "invalid_token" }, 401);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
model: { name: "code-gecko" },
|
||||
choices: [{ text: "monolith fallback works" }],
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gitlab-duo-code-suggestions",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "Say hello" }],
|
||||
},
|
||||
stream: false,
|
||||
credentials: {
|
||||
accessToken: "oauth-access",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://gitlab.example.com",
|
||||
},
|
||||
},
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"https://gitlab.example.com/api/v4/code_suggestions/direct_access",
|
||||
"https://gitlab.example.com/api/v4/code_suggestions/completions",
|
||||
]);
|
||||
|
||||
const body = (await result.response.json()) as any;
|
||||
assert.equal(body.model, "code-gecko");
|
||||
assert.match(body.choices[0].message.content, /monolith fallback works/i);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user