fix(ai): strip JSON Schema meta keys for Cloud Code Assist (#3412)
Some checks are pending
CI / build-check-test (push) Waiting to run

This commit is contained in:
Vladyslav Tkachenko 2026-04-19 17:54:49 +03:00 committed by GitHub
parent 9b28e185db
commit f732f5e858
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 217 additions and 1 deletions

View file

@ -239,6 +239,33 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
return contents;
}
const JSON_SCHEMA_META_DECLARATIONS = new Set([
"$schema",
"$id",
"$anchor",
"$dynamicAnchor",
"$vocabulary",
"$comment",
"$defs",
"definitions", // pre-draft-2019-09 equivalent of $defs
]);
/**
* Strip meta-declarations from a schema obj
*/
function sanitizeForOpenApi(schema: unknown): unknown {
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
return schema;
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(schema)) {
if (JSON_SCHEMA_META_DECLARATIONS.has(key)) continue;
result[key] = sanitizeForOpenApi(value);
}
return result;
}
/**
* Convert tools to Gemini function declarations format.
*
@ -257,7 +284,9 @@ export function convertTools(
functionDeclarations: tools.map((tool) => ({
name: tool.name,
description: tool.description,
...(useParameters ? { parameters: tool.parameters } : { parametersJsonSchema: tool.parameters }),
...(useParameters
? { parameters: sanitizeForOpenApi(tool.parameters as unknown) }
: { parametersJsonSchema: tool.parameters }),
})),
},
];

View file

@ -0,0 +1,187 @@
import { describe, expect, it } from "vitest";
import { convertTools } from "../src/providers/google-shared.js";
import type { Tool } from "../src/types.js";
function makeTool(parameters: Record<string, unknown>): Tool {
return {
name: "test_tool",
description: "A test tool",
parameters: parameters as Tool["parameters"],
};
}
describe("google-shared convertTools", () => {
it("strips JSON Schema meta keys from parameters when useParameters=true", () => {
const tools = [
makeTool({
$schema: "http://json-schema.org/draft-07/schema#",
$id: "urn:bash-tool",
$comment: "A bash tool for demonstration",
$defs: {
commandDef: { type: "string" },
},
definitions: {
legacyDef: { type: "number" },
},
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
}),
];
const result = convertTools(tools, true);
const decl = result?.[0]?.functionDeclarations?.[0];
expect(decl).toBeDefined();
expect(decl?.parameters).toEqual({
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
});
expect(decl?.parameters).not.toHaveProperty("$schema");
expect(decl?.parameters).not.toHaveProperty("$id");
expect(decl?.parameters).not.toHaveProperty("$comment");
expect(decl?.parameters).not.toHaveProperty("$defs");
expect(decl?.parameters).not.toHaveProperty("definitions");
});
it("recursively strips nested JSON Schema meta keys", () => {
const tools = [
makeTool({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
deep: {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "urn:nested",
type: "string",
},
},
}),
];
const result = convertTools(tools, true);
const decl = result?.[0]?.functionDeclarations?.[0];
expect(decl).toBeDefined();
expect(decl?.parameters).toEqual({
type: "object",
properties: {
deep: {
type: "string",
},
},
});
});
it("preserves $ref while stripping meta keys", () => {
const tools = [
makeTool({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
refProp: {
$ref: "#/$defs/someDef",
type: "string",
},
},
}),
];
const result = convertTools(tools, true);
const decl = result?.[0]?.functionDeclarations?.[0];
expect(decl).toBeDefined();
expect(decl?.parameters).toEqual({
type: "object",
properties: {
refProp: {
$ref: "#/$defs/someDef",
type: "string",
},
},
});
});
it("does not mutate the original Tool.parameters object", () => {
const originalParameters = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
};
const tools = [makeTool(originalParameters)];
convertTools(tools, true);
expect(originalParameters).toEqual({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
});
});
it("preserves $schema in parametersJsonSchema when useParameters=false", () => {
const tools = [
makeTool({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
}),
];
const result = convertTools(tools, false);
const decl = result?.[0]?.functionDeclarations?.[0];
expect(decl).toBeDefined();
expect(decl?.parametersJsonSchema).toEqual({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
});
});
it("handles tools without $schema gracefully", () => {
const tools = [
makeTool({
type: "object",
properties: {
path: { type: "string" },
},
required: ["path"],
}),
];
const result = convertTools(tools, true);
const decl = result?.[0]?.functionDeclarations?.[0];
expect(decl).toBeDefined();
expect(decl?.parameters).toEqual({
type: "object",
properties: {
path: { type: "string" },
},
required: ["path"],
});
});
it("returns undefined for empty tool list", () => {
expect(convertTools([])).toBeUndefined();
expect(convertTools([], true)).toBeUndefined();
});
});