Skip to content
Open
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
20 changes: 20 additions & 0 deletions extensions/ai-providers/antigravity/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ function sanitizeSchemaForCcaValue(
if (value === null || typeof value !== "object") return value;
const out: Record<string, unknown> = {};
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
Expand All @@ -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 &&
Expand All @@ -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 : "";
Expand Down
68 changes: 68 additions & 0 deletions tests/extensions/ai-providers/antigravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading