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:
Diego Rodrigues de Sa e Souza
2026-06-25 23:48:43 -03:00
committed by GitHub
parent f4a9e65342
commit 4f8a986905
3 changed files with 160 additions and 8 deletions

View File

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

View File

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

View File

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