@@ -46,16 +46,33 @@ const GEMINI_SCHEMA_COMPATIBILITY_DROP_KEYS = new Set([
4646 "definitions" ,
4747] )
4848
49- function sanitizeSchemaForGemini ( schema : unknown ) : unknown {
49+ function sanitizeSchemaForGemini ( schema : unknown , defs ?: Record < string , unknown > ) : unknown {
5050 if ( ! schema || typeof schema !== "object" ) {
5151 return schema
5252 }
5353
5454 if ( Array . isArray ( schema ) ) {
55- return schema . map ( ( item ) => sanitizeSchemaForGemini ( item ) )
55+ return schema . map ( ( item ) => sanitizeSchemaForGemini ( item , defs ) )
5656 }
5757
5858 const source = schema as Record < string , unknown >
59+
60+ // Extract $defs / definitions from the root schema on the first call so
61+ // they can be used to resolve $ref entries encountered deeper in the tree.
62+ const resolvedDefs = defs ?? ( ( source . $defs ?? source . definitions ) as Record < string , unknown > | undefined )
63+
64+ // Resolve local JSON Pointer $ref before any other processing.
65+ // Without this, dropping $defs leaves dangling references that Gemini rejects.
66+ if ( typeof source . $ref === "string" && resolvedDefs ) {
67+ const match = source . $ref . match ( / ^ # \/ (?: \$ d e f s | d e f i n i t i o n s ) \/ ( .+ ) $ / )
68+ if ( match ) {
69+ const resolved = resolvedDefs [ match [ 1 ] ]
70+ if ( resolved !== undefined ) {
71+ return sanitizeSchemaForGemini ( resolved , resolvedDefs )
72+ }
73+ }
74+ }
75+
5976 const result : Record < string , unknown > = { }
6077 let nullable = source . nullable === true
6178
@@ -67,14 +84,28 @@ function sanitizeSchemaForGemini(schema: unknown): unknown {
6784 : true
6885 } )
6986 nullable = nullable || variants . length < composition . length
70- Object . assign ( result , sanitizeSchemaForGemini ( variants [ 0 ] ?? { } ) )
87+ Object . assign ( result , sanitizeSchemaForGemini ( variants [ 0 ] ?? { } , resolvedDefs ) )
7188 }
7289
7390 if ( Array . isArray ( source . allOf ) ) {
7491 for ( const variant of source . allOf ) {
75- const sanitized = sanitizeSchemaForGemini ( variant )
92+ const sanitized = sanitizeSchemaForGemini ( variant , resolvedDefs )
7693 if ( sanitized && typeof sanitized === "object" && ! Array . isArray ( sanitized ) ) {
77- Object . assign ( result , sanitized )
94+ const s = sanitized as Record < string , unknown >
95+ // Deep-merge properties so later allOf fragments don't overwrite
96+ // earlier ones (last-write-wins Object.assign drops prior keys).
97+ if ( s . properties && typeof s . properties === "object" ) {
98+ result . properties = {
99+ ...( result . properties as Record < string , unknown > | undefined ) ,
100+ ...( s . properties as Record < string , unknown > ) ,
101+ }
102+ }
103+ if ( Array . isArray ( s . required ) ) {
104+ const existing = Array . isArray ( result . required ) ? ( result . required as string [ ] ) : [ ]
105+ result . required = [ ...new Set ( [ ...existing , ...( s . required as string [ ] ) ] ) ]
106+ }
107+ const { properties : _p , required : _r , ...rest } = s
108+ Object . assign ( result , rest )
78109 }
79110 }
80111 }
@@ -93,7 +124,7 @@ function sanitizeSchemaForGemini(schema: unknown): unknown {
93124 continue
94125 }
95126
96- result [ key ] = sanitizeSchemaForGemini ( value )
127+ result [ key ] = sanitizeSchemaForGemini ( value , resolvedDefs )
97128 }
98129
99130 if ( nullable ) {
0 commit comments