From 17b8a01da6cf1d8712b2a8f279e694d858ae2e62 Mon Sep 17 00:00:00 2001 From: Bryan K Crisler Jr Date: Wed, 24 Jun 2026 12:54:05 -0500 Subject: [PATCH 1/3] fix: flatten top-level anyOf/oneOf/allOf in tool schemas for Anthropic API compatibility --- utils/schema.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/utils/schema.ts b/utils/schema.ts index da7665fc..fa16db92 100644 --- a/utils/schema.ts +++ b/utils/schema.ts @@ -107,18 +107,25 @@ 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; + } + } + } + delete fixedSchema[combiner]; + } } } From fffa6d75dacf439025e1596c412caf1aa0374831 Mon Sep 17 00:00:00 2001 From: Bryan K Crisler Jr Date: Wed, 24 Jun 2026 13:12:45 -0500 Subject: [PATCH 2/3] fix: preserve shared required fields when flattening union schemas Use intersection of variant required arrays so only fields required in ALL variants remain globally required after flattening. This correctly handles mutually exclusive unions (e.g., id OR full_path) without erroneously requiring both. --- utils/schema.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/utils/schema.ts b/utils/schema.ts index fa16db92..3d10e13b 100644 --- a/utils/schema.ts +++ b/utils/schema.ts @@ -124,6 +124,20 @@ export const toJSONSchema = (schema: z.ZodTypeAny) => { } } } + // Collect required fields shared across ALL variants (intersection) + const requiredSets = variants.map( + (v: any) => new Set(Array.isArray(v.required) ? v.required : []) + ); + const sharedRequired = [...requiredSets[0]].filter( + field => requiredSets.every((s: Set) => s.has(field)) + ); + if (sharedRequired.length > 0) { + const existing = new Set(Array.isArray(fixedSchema.required) ? fixedSchema.required : []); + for (const field of sharedRequired) { + existing.add(field); + } + fixedSchema.required = [...existing]; + } delete fixedSchema[combiner]; } } From 717814206770b8c7343bccd400f788b15f927cce Mon Sep 17 00:00:00 2001 From: Bryan K Crisler Jr Date: Wed, 24 Jun 2026 13:19:21 -0500 Subject: [PATCH 3/3] fix: use union semantics for allOf required fields, intersection for anyOf/oneOf allOf means all schemas apply simultaneously so all their requirements apply (union). anyOf/oneOf means one schema applies so only shared requirements are universal (intersection). --- utils/schema.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/utils/schema.ts b/utils/schema.ts index 3d10e13b..2ca2a3b4 100644 --- a/utils/schema.ts +++ b/utils/schema.ts @@ -124,16 +124,29 @@ export const toJSONSchema = (schema: z.ZodTypeAny) => { } } } - // Collect required fields shared across ALL variants (intersection) + // 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 : []) ); - const sharedRequired = [...requiredSets[0]].filter( - field => requiredSets.every((s: Set) => s.has(field)) - ); - if (sharedRequired.length > 0) { + 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 sharedRequired) { + for (const field of mergedRequired) { existing.add(field); } fixedSchema.required = [...existing];