mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
fix(sse): preserve required fields in antigravity tool schemas (#4843)
Integrated into release/v3.8.37 — conflict resolved (kept #4740/#4813 enumDescriptions strip + typed normalizeSchemaTypes, added required-preservation helpers; test-tail merged keeping both enumDescriptions + required tests); 7/7 green.
This commit is contained in:
committed by
GitHub
parent
f4a9e65342
commit
4f8a986905
@@ -10,6 +10,8 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **antigravity-to-openai**: preserve the `required` array when translating Draft 2020-12 tool schemas (e.g. from OpenCode), stripping unsupported JSON Schema meta keywords while keeping mandatory arguments required so the model no longer calls tools without them. (thanks @anuragg-saxenaa)
|
||||
|
||||
- **cli(runtime)**: persist the lazily-installed native runtime deps (`better-sqlite3`, `systray2`) to the shared runtime `package.json` with `--save-exact` instead of `--no-save`, so installing one no longer prunes the other as "extraneous" — fixing a "No SQLite driver available" failure after a `--tray` install (thanks @omartuhintvs).
|
||||
|
||||
- **pricing**: add the `MiniMax-M3` cost row (canonical + lowercase alias) so the new MiniMax default model gets accurate per-request cost accounting instead of falling back to a zero/default rate (thanks @octo-patch).
|
||||
|
||||
@@ -85,7 +85,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
function: {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
parameters: normalizeSchemaTypes(func.parameters) || {
|
||||
parameters: cleanSchemaPreservingRequired(func.parameters) || {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
@@ -100,22 +100,24 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
}
|
||||
|
||||
// Recursively convert Antigravity schema types (OBJECT, STRING, etc.) to lowercase
|
||||
// and strip unsupported fields like enumDescriptions
|
||||
function normalizeSchemaTypes(schema) {
|
||||
// and strip unsupported fields like enumDescriptions.
|
||||
function normalizeSchemaTypes(schema: unknown): unknown {
|
||||
if (!schema || typeof schema !== "object") return schema;
|
||||
|
||||
const result = Array.isArray(schema) ? [...schema] : { ...schema };
|
||||
const result: JsonRecord = Array.isArray(schema)
|
||||
? ([...(schema as unknown[])] as unknown as JsonRecord)
|
||||
: { ...(schema as JsonRecord) };
|
||||
|
||||
if (typeof result.type === "string") {
|
||||
result.type = result.type.toLowerCase();
|
||||
}
|
||||
|
||||
// Strip enumDescriptions — not supported by upstream OpenAI-compatible APIs
|
||||
// Strip enumDescriptions — not supported by upstream APIs
|
||||
delete result.enumDescriptions;
|
||||
|
||||
if (result.properties) {
|
||||
const normalized = {};
|
||||
for (const [key, val] of Object.entries(result.properties)) {
|
||||
if (result.properties && typeof result.properties === "object") {
|
||||
const normalized: JsonRecord = {};
|
||||
for (const [key, val] of Object.entries(result.properties as JsonRecord)) {
|
||||
normalized[key] = normalizeSchemaTypes(val);
|
||||
}
|
||||
result.properties = normalized;
|
||||
@@ -128,6 +130,83 @@ function normalizeSchemaTypes(schema) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clean a JSON Schema for Antigravity while PRESERVING the `required` array at every level.
|
||||
// Unlike the type-lowering pass alone, this strips JSON Schema Draft 2020-12 meta keywords
|
||||
// ($schema, $defs, $ref, additionalProperties, patternProperties, title, x-*, ...) that the
|
||||
// Antigravity upstream does not accept, yet keeps `required` so the model still treats
|
||||
// mandatory tool arguments as mandatory. Clients such as OpenCode send full Draft 2020-12
|
||||
// tool schemas; dropping `required` lets the model call tools without their required args.
|
||||
function cleanSchemaPreservingRequired(schema: unknown): unknown {
|
||||
if (!schema || typeof schema !== "object") return schema;
|
||||
|
||||
// Reuse the existing recursion to lowercase types + strip enumDescriptions, then
|
||||
// remove draft-meta keywords and reconcile `required` against the surviving properties.
|
||||
const normalized = normalizeSchemaTypes(structuredClone(schema));
|
||||
stripDraftMeta(normalized);
|
||||
preserveRequired(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Draft 2020-12 / JSON Schema meta keywords the Antigravity upstream does not accept.
|
||||
const DRAFT_META_KEYS = new Set([
|
||||
"$schema",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"$ref",
|
||||
"$comment",
|
||||
"const",
|
||||
"additionalProperties",
|
||||
"propertyNames",
|
||||
"patternProperties",
|
||||
"title",
|
||||
]);
|
||||
|
||||
function stripDraftMeta(obj: unknown): void {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) stripDraftMeta(item);
|
||||
return;
|
||||
}
|
||||
const record = obj as JsonRecord;
|
||||
for (const key of Object.keys(record)) {
|
||||
if (DRAFT_META_KEYS.has(key) || key.startsWith("x-")) {
|
||||
delete record[key];
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(record)) {
|
||||
if (value && typeof value === "object") stripDraftMeta(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve `required` even when referenced fields were stripped from constraint blocks.
|
||||
// At each node where both `required` and `properties` are present, keep only the entries
|
||||
// that still exist in `properties`; drop `required` entirely if none survive. This avoids
|
||||
// emitting a `required` array that references fields removed by stripDraftMeta.
|
||||
function preserveRequired(obj: unknown): void {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) preserveRequired(item);
|
||||
return;
|
||||
}
|
||||
const record = obj as JsonRecord;
|
||||
if (Array.isArray(record.required) && record.properties && typeof record.properties === "object") {
|
||||
const properties = record.properties as JsonRecord;
|
||||
const valid = (record.required as unknown[]).filter(
|
||||
(field) =>
|
||||
typeof field === "string" &&
|
||||
Object.prototype.hasOwnProperty.call(properties, field)
|
||||
);
|
||||
if (valid.length === 0) {
|
||||
delete record.required;
|
||||
} else {
|
||||
record.required = valid;
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(record)) {
|
||||
if (value && typeof value === "object") preserveRequired(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Antigravity content to OpenAI message
|
||||
// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData
|
||||
function convertContent(content) {
|
||||
|
||||
@@ -250,3 +250,74 @@ test("Antigravity -> OpenAI strips enumDescriptions from tool schema (top-level
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Antigravity -> OpenAI preserves the required array on Draft 2020-12 tool schemas", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
request: {
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: "create_file",
|
||||
parameters: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "OBJECT",
|
||||
additionalProperties: false,
|
||||
title: "CreateFileArgs",
|
||||
$defs: { unused: { type: "STRING" } },
|
||||
properties: {
|
||||
path: { type: "STRING", pattern: "^/" },
|
||||
contents: { type: "STRING" },
|
||||
},
|
||||
required: ["path", "contents"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const params = (result.tools[0].function as any).parameters;
|
||||
// The required array must survive so the model treats mandatory args as mandatory.
|
||||
assert.deepEqual(params.required, ["path", "contents"]);
|
||||
// Types are still lowered and Draft 2020-12 meta keywords are stripped.
|
||||
assert.equal(params.type, "object");
|
||||
assert.equal(params.properties.path.type, "string");
|
||||
assert.equal(params.$schema, undefined);
|
||||
assert.equal(params.$defs, undefined);
|
||||
assert.equal(params.additionalProperties, undefined);
|
||||
assert.equal(params.title, undefined);
|
||||
});
|
||||
|
||||
test("Antigravity -> OpenAI drops required entries that no longer exist in properties", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
request: {
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: "partial",
|
||||
parameters: {
|
||||
type: "OBJECT",
|
||||
properties: { kept: { type: "STRING" } },
|
||||
required: ["kept", "ghost"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const params = (result.tools[0].function as any).parameters;
|
||||
assert.deepEqual(params.required, ["kept"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user