mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): preserve literal data in Gemini tool schemas
Restrict const rewriting to schema-bearing keywords and preserve __proto__ names when copying schema maps. Add regression coverage. Closes #11308
This commit is contained in:
parent
7314819f0f
commit
a7b94a03f0
2 changed files with 121 additions and 5 deletions
|
|
@ -372,7 +372,34 @@ function sanitizeForOpenApi(schema: unknown): unknown {
|
|||
}
|
||||
|
||||
/** Schema keywords whose values map names to subschemas, so their keys are not schema keywords. */
|
||||
const JSON_SCHEMA_NAME_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]);
|
||||
const JSON_SCHEMA_NAME_MAPS = new Set([
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"dependencies",
|
||||
]);
|
||||
|
||||
/** Keywords containing a subschema or an array of subschemas, not literal data. */
|
||||
const JSON_SCHEMA_SUBSCHEMAS = new Set([
|
||||
"additionalProperties",
|
||||
"unevaluatedProperties",
|
||||
"propertyNames",
|
||||
"items",
|
||||
"prefixItems",
|
||||
"additionalItems",
|
||||
"unevaluatedItems",
|
||||
"contains",
|
||||
"allOf",
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
"not",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"contentSchema",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Rewrite string and number `const` values to single-value `enum`s.
|
||||
|
|
@ -382,10 +409,14 @@ function rewriteConstAsEnum(schema: unknown, isNameMap = false): unknown {
|
|||
if (Array.isArray(schema)) return schema.map((item) => rewriteConstAsEnum(item));
|
||||
if (typeof schema !== "object" || schema === null) return schema;
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
result[key] = isNameMap ? rewriteConstAsEnum(value) : rewriteConstAsEnum(value, JSON_SCHEMA_NAME_MAPS.has(key));
|
||||
}
|
||||
// fromEntries preserves names such as __proto__ as own properties.
|
||||
const result: Record<string, unknown> = Object.fromEntries(
|
||||
Object.entries(schema).map(([key, value]) => {
|
||||
if (isNameMap || JSON_SCHEMA_SUBSCHEMAS.has(key)) return [key, rewriteConstAsEnum(value)];
|
||||
if (JSON_SCHEMA_NAME_MAPS.has(key)) return [key, rewriteConstAsEnum(value, true)];
|
||||
return [key, value];
|
||||
}),
|
||||
);
|
||||
if (!isNameMap && "const" in result && result.enum === undefined) {
|
||||
const value = result.const;
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
|
|
|
|||
|
|
@ -202,7 +202,92 @@ describe("google-shared convertTools", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Regression coverage for #11308.
|
||||
describe("google-shared convertTools const handling", () => {
|
||||
it.each(["default", "examples", "enum", "const", "x-custom"])("preserves literal data under %s", (key) => {
|
||||
const literal = { const: "literal-data", properties: { nested: { const: 1 } } };
|
||||
const parameters = {
|
||||
type: "object",
|
||||
properties: { value: { type: "object", [key]: key === "enum" || key === "examples" ? [literal] : literal } },
|
||||
};
|
||||
const original = structuredClone(parameters);
|
||||
|
||||
expect(convertTools([makeTool(parameters)])?.[0]?.functionDeclarations?.[0]?.parametersJsonSchema).toEqual(
|
||||
original,
|
||||
);
|
||||
expect(parameters).toEqual(original);
|
||||
});
|
||||
|
||||
it.each(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"])(
|
||||
"preserves names while rewriting schemas in %s",
|
||||
(key) => {
|
||||
const parameters = {
|
||||
type: "object",
|
||||
[key]: {
|
||||
["__proto__"]: { const: "x" },
|
||||
const: { const: "y" },
|
||||
properties: { const: "z" },
|
||||
},
|
||||
};
|
||||
const original = structuredClone(parameters);
|
||||
const expected = {
|
||||
type: "object",
|
||||
[key]: {
|
||||
["__proto__"]: { enum: ["x"] },
|
||||
const: { enum: ["y"] },
|
||||
properties: { enum: ["z"] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = convertTools([makeTool(parameters)])?.[0]?.functionDeclarations?.[0]?.parametersJsonSchema;
|
||||
expect(result).toEqual(expected);
|
||||
expect(JSON.stringify(result)).toBe(JSON.stringify(expected));
|
||||
expect(parameters).toEqual(original);
|
||||
},
|
||||
);
|
||||
|
||||
it("rewrites only schema-bearing keywords", () => {
|
||||
const parameters = {
|
||||
type: "object",
|
||||
additionalProperties: { const: "x" },
|
||||
unevaluatedProperties: false,
|
||||
propertyNames: { const: "name" },
|
||||
// biome-ignore lint/suspicious/noThenProperty: JSON Schema conditional keyword, not a thenable.
|
||||
allOf: [{ if: { const: "x" }, then: { const: "y" }, else: { not: { const: "z" } } }],
|
||||
properties: {
|
||||
list: {
|
||||
type: "array",
|
||||
items: [{ const: 1 }],
|
||||
prefixItems: [{ const: 2 }],
|
||||
additionalItems: { const: 3 },
|
||||
contains: { const: 4 },
|
||||
unevaluatedItems: { const: 5 },
|
||||
},
|
||||
},
|
||||
dependencies: { list: ["other"] },
|
||||
};
|
||||
|
||||
expect(convertTools([makeTool(parameters)])?.[0]?.functionDeclarations?.[0]?.parametersJsonSchema).toEqual({
|
||||
type: "object",
|
||||
additionalProperties: { enum: ["x"] },
|
||||
unevaluatedProperties: false,
|
||||
propertyNames: { enum: ["name"] },
|
||||
// biome-ignore lint/suspicious/noThenProperty: JSON Schema conditional keyword, not a thenable.
|
||||
allOf: [{ if: { enum: ["x"] }, then: { enum: ["y"] }, else: { not: { enum: ["z"] } } }],
|
||||
properties: {
|
||||
list: {
|
||||
type: "array",
|
||||
items: [{ enum: [1] }],
|
||||
prefixItems: [{ enum: [2] }],
|
||||
additionalItems: { enum: [3] },
|
||||
contains: { enum: [4] },
|
||||
unevaluatedItems: { enum: [5] },
|
||||
},
|
||||
},
|
||||
dependencies: { list: ["other"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("rewrites string and number const values to single-value enums in parametersJsonSchema", () => {
|
||||
const parameters = {
|
||||
type: "object",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue