fix(ai): keep ignored schema branches out of Moonshot validation

This commit is contained in:
Aiden Cline
2026-09-24 11:16:29 -05:00
parent 9f284fb1c7
commit 203fab66d0
3 changed files with 49 additions and 2 deletions
@@ -25,6 +25,7 @@ const ANNOTATIONS = new Set([
])
const TYPES = new Set(["null", "boolean", "object", "array", "number", "integer", "string"])
const NULLABLE_ENUM_TYPES = new Set(["string", "number", "integer", "boolean"])
const RESERVED_PROPERTIES = new Set(["$defs", "$ref", "anyOf", "required", "additionalProperties"])
const mapValues = (record: Record<string, unknown>, map: (value: unknown, key: string) => unknown) =>
Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value, key)]))
@@ -110,6 +111,9 @@ const mergeObjects = (schema: Record<string, unknown>): Record<string, unknown>
!isRecord(branch) ||
branch.type !== "object" ||
!isRecord(branch.properties) ||
Object.entries(branch.properties).some(([name, value]) => RESERVED_PROPERTIES.has(name) || !isRecord(value)) ||
("required" in branch &&
(!Array.isArray(branch.required) || branch.required.some((name) => typeof name !== "string"))) ||
Object.keys(branch).some((key) => !["type", "properties", "required"].includes(key)),
)
)
@@ -123,7 +127,11 @@ const mergeObjects = (schema: Record<string, unknown>): Record<string, unknown>
...omit(schema, "allOf"),
type: "object",
properties,
required: [...new Set(branches.flatMap((branch) => (Array.isArray(branch.required) ? branch.required : [])))],
...(branches.some((branch) => "required" in branch)
? {
required: [...new Set(branches.flatMap((branch) => (Array.isArray(branch.required) ? branch.required : [])))],
}
: {}),
}
}
@@ -136,7 +144,16 @@ const taggedUnion = (schema: Record<string, unknown>, root: JsonSchema) => {
if (typeof branch.$ref !== "string") return branch
return resolve(branch.$ref, root)
})
if (branches.some((branch) => !branch || branch.type !== "object" || !isRecord(branch.properties))) return schema
if (
branches.some(
(branch) =>
!branch ||
branch.type !== "object" ||
!isRecord(branch.properties) ||
Object.entries(branch.properties).some(([name, value]) => RESERVED_PROPERTIES.has(name) || !isRecord(value)),
)
)
return schema
const first = branches[0]
if (!first || !isRecord(first.properties)) return schema
const tag = Object.keys(first.properties).find((name) => {
@@ -273,6 +290,9 @@ const typeList = (schema: Record<string, unknown>) => {
const constraints = omit(schema, "type", "$defs", "$id", "description", "title", "default")
if (Object.keys(constraints).length === 0 && !("default" in schema)) return schema
if (Object.keys(constraints).length === 1 && "enum" in constraints && !("default" in schema)) return schema
// Walle accepts a null bound on a type list but rejects it on its individual branches.
if (["minLength", "maxLength", "minItems", "maxItems", "minimum", "maximum"].some((key) => constraints[key] === null))
return schema
const branches = schema.type.flatMap((type) => {
if (!Array.isArray(constraints.enum)) return [integerBounds({ type, ...constraints })]
const values = constraints.enum.filter(
@@ -1,4 +1,5 @@
import type { JsonSchema, LanguageModel, LanguageModelSanitizerCompatibility } from "../../schema/index.js"
import { isRecord } from "../../utils/record.js"
import { GeminiJsonSchema } from "./gemini-json-schema.js"
import { MoonshotJsonSchema } from "./moonshot-json-schema.js"
@@ -273,6 +273,32 @@ describe("tool schema projections", () => {
})
})
test("moonshot does not expose ignored invalid branches to validation", () => {
const schema = {
type: "object",
properties: {
nullable: { type: ["string", "null"], minLength: null },
intersection: {
allOf: [
{ type: "object", properties: { disabled: false } },
{ type: "object", properties: { enabled: { type: "string" } } },
],
},
tagged: {
oneOf: [
{
type: "object",
properties: { kind: { type: "string", enum: ["a"] }, disabled: false },
required: ["kind"],
},
{ type: "object", properties: { kind: { type: "string", enum: ["b"] } }, required: ["kind"] },
],
},
},
}
expect(ToolSchemaProjection.moonshot(schema)).toEqual(schema)
})
test("moonshot handles boolean branches and tuple bounds", () => {
expect(
ToolSchemaProjection.moonshot({