diff --git a/utils/schema.ts b/utils/schema.ts index da7665fc..2ca2a3b4 100644 --- a/utils/schema.ts +++ b/utils/schema.ts @@ -107,18 +107,52 @@ export const toJSONSchema = (schema: z.ZodTypeAny) => { const fixedSchema = fixNullableOptional(jsonSchema, true); - if (!fixedSchema.properties && Array.isArray(fixedSchema.anyOf)) { - const variants = fixedSchema.anyOf.filter( - (item: any) => item?.type === "object" && item.properties - ); - if (variants.length === fixedSchema.anyOf.length) { - fixedSchema.type = "object"; - fixedSchema.properties = variants.reduce((properties: any, item: any) => { - Object.entries(item.properties).forEach(([key, value]) => { - if (!properties[key]) properties[key] = value; - }); - return properties; - }, {}); + // Flatten top-level anyOf/oneOf into a single object schema for Anthropic API compatibility. + // The Anthropic API rejects tool input_schema with top-level oneOf/allOf/anyOf. + for (const combiner of ["anyOf", "oneOf", "allOf"] as const) { + if (Array.isArray(fixedSchema[combiner])) { + const variants = fixedSchema[combiner].filter( + (item: any) => item?.type === "object" && item.properties + ); + if (variants.length > 0 && variants.length === fixedSchema[combiner].length) { + fixedSchema.type = "object"; + fixedSchema.properties = fixedSchema.properties || {}; + for (const variant of variants) { + for (const [key, value] of Object.entries(variant.properties)) { + if (!fixedSchema.properties[key]) { + fixedSchema.properties[key] = value; + } + } + } + // Compute required fields based on combiner semantics: + // - allOf: union (all schemas apply, so all requirements apply) + // - anyOf/oneOf: intersection (only shared requirements are universal) + const requiredSets = variants.map( + (v: any) => new Set(Array.isArray(v.required) ? v.required : []) + ); + let mergedRequired: string[]; + if (combiner === "allOf") { + // Union: any field required in any variant is required + const all = new Set(); + for (const s of requiredSets) { + for (const field of s) all.add(field); + } + mergedRequired = [...all]; + } else { + // Intersection: only fields required in ALL variants + mergedRequired = [...requiredSets[0]].filter( + field => requiredSets.every((s: Set) => s.has(field)) + ); + } + if (mergedRequired.length > 0) { + const existing = new Set(Array.isArray(fixedSchema.required) ? fixedSchema.required : []); + for (const field of mergedRequired) { + existing.add(field); + } + fixedSchema.required = [...existing]; + } + delete fixedSchema[combiner]; + } } }