From d06dcfe7c5dceb460b418e06036d8142a8cf8d8b Mon Sep 17 00:00:00 2001 From: sunss Date: Thu, 22 Oct 2026 11:22:54 +0800 Subject: [PATCH] fix(ai-providers): rewrite const schema literals to enum for Cloud Code Assist TypeBox serializes Type.Union([Type.Literal(...)]) tool parameters as anyOf branches carrying "const". Cloud Code Assist validates function declarations against OpenAPI 3.0, which has no "const", so every request from a session with such a tool 400s with "Unknown name \"const\" at ...parameters.properties[N].value.any_of[M]" (e.g. subagent_check.evidence since #652; setup's role/browser unions are affected too). enum-carrying parameters in the same payload pass, so rewrite const to a single-value enum in sanitizeSchemaForCca instead of dropping the constraint. Verified live: pi -ne -a -e ai-providers -e subagents against google-antigravity/gemini-3.8-flash-high 400s before, replies after the rewrite; broader load with workflows+setup passes as well. Unit tests cover anyOf-branch literals, enum merging, and a property named const. --- .../ai-providers/antigravity/provider.ts | 20 ++++++ .../ai-providers/antigravity.test.ts | 68 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/extensions/ai-providers/antigravity/provider.ts b/extensions/ai-providers/antigravity/provider.ts index 9c5fefae7..3ab9a6748 100644 --- a/extensions/ai-providers/antigravity/provider.ts +++ b/extensions/ai-providers/antigravity/provider.ts @@ -148,6 +148,8 @@ function sanitizeSchemaForCcaValue( if (value === null || typeof value !== "object") return value; const out: Record = {}; const spill: Array<[string, unknown]> = []; + let constEntry: unknown; + let hasConst = false; for (const [key, entry] of Object.entries(value)) { // Keys below `properties` are user-defined parameter names, not JSON // Schema keywords. A tool parameter named `pattern`, for example, must be @@ -156,6 +158,11 @@ function sanitizeSchemaForCcaValue( out[key] = sanitizeSchemaForCcaValue(entry, false); continue; } + if (key === "const") { + hasConst = true; + constEntry = entry; + continue; + } if (Object.hasOwn(CCA_UNSUPPORTED_SCHEMA_FIELDS, key)) { if ( entry !== undefined && @@ -167,6 +174,19 @@ function sanitizeSchemaForCcaValue( } out[key] = sanitizeSchemaForCcaValue(entry, key === "properties"); } + if (hasConst) { + // OpenAPI 3.0 — the schema dialect CCA validates against — has no + // "const"; TypeBox literal unions (`anyOf` of `{type, const}`) 400 the + // whole request. Rewrite the literal as a single-value enum, which CCA + // accepts (enum-carrying parameters in the same request pass). + const enumValues = Array.isArray(out.enum) + ? [...(out.enum as unknown[])] + : []; + if (!enumValues.includes(constEntry)) { + enumValues.push(constEntry); + } + out.enum = enumValues; + } if (spill.length > 0) { const formatted = `{${spill.map(([key, entry]) => `${key}: ${JSON.stringify(entry)}`).join(", ")}}`; const existing = typeof out.description === "string" ? out.description : ""; diff --git a/tests/extensions/ai-providers/antigravity.test.ts b/tests/extensions/ai-providers/antigravity.test.ts index ee74a90b0..4833746b0 100644 --- a/tests/extensions/ai-providers/antigravity.test.ts +++ b/tests/extensions/ai-providers/antigravity.test.ts @@ -555,6 +555,74 @@ test("sanitizeSchemaForCca preserves property names that match schema keywords", ); }); +test("sanitizeSchemaForCca rewrites const literals to enum inside anyOf branches", () => { + // TypeBox serializes Type.Union([Type.Literal(...)]) this way; CCA 400s the + // whole request on "const" while accepting enum in the same payload. + assert.deepEqual( + sanitizeSchemaForCca({ + type: "object", + properties: { + evidence: { + description: 'Evidence to read: "transcript", "final", "structured".', + anyOf: [ + { type: "string", const: "transcript" }, + { type: "string", const: "final" }, + { type: "string", const: "structured" }, + ], + }, + }, + }), + { + type: "object", + properties: { + evidence: { + description: 'Evidence to read: "transcript", "final", "structured".', + anyOf: [ + { type: "string", enum: ["transcript"] }, + { type: "string", enum: ["final"] }, + { type: "string", enum: ["structured"] }, + ], + }, + }, + }, + ); +}); + +test("sanitizeSchemaForCca merges a bare const into enum without duplicates", () => { + assert.deepEqual( + sanitizeSchemaForCca({ type: "string", const: "chromium" }), + { + type: "string", + enum: ["chromium"], + }, + ); + assert.deepEqual( + sanitizeSchemaForCca({ + type: "string", + const: "chrome", + enum: ["chrome", "edge"], + }), + { type: "string", enum: ["chrome", "edge"] }, + ); +}); + +test("sanitizeSchemaForCca preserves a property named const", () => { + assert.deepEqual( + sanitizeSchemaForCca({ + type: "object", + properties: { + const: { type: "string", description: "Value to pin" }, + }, + }), + { + type: "object", + properties: { + const: { type: "string", description: "Value to pin" }, + }, + }, + ); +}); + test("buildRequestBody strips CCA-rejected keywords and spills constraints into description", () => { const contextWithTools = { ...SIMPLE_CONTEXT,