openai 6.48.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +7 -1
  3. package/azure.d.mts.map +1 -1
  4. package/azure.d.ts.map +1 -1
  5. package/azure.js +5 -1
  6. package/azure.js.map +1 -1
  7. package/azure.mjs +5 -1
  8. package/azure.mjs.map +1 -1
  9. package/helpers/standard-schema.d.mts +104 -0
  10. package/helpers/standard-schema.d.mts.map +1 -0
  11. package/helpers/standard-schema.d.ts +104 -0
  12. package/helpers/standard-schema.d.ts.map +1 -0
  13. package/helpers/standard-schema.js +320 -0
  14. package/helpers/standard-schema.js.map +1 -0
  15. package/helpers/standard-schema.mjs +314 -0
  16. package/helpers/standard-schema.mjs.map +1 -0
  17. package/helpers/zod.d.mts +26 -3
  18. package/helpers/zod.d.mts.map +1 -1
  19. package/helpers/zod.d.ts +26 -3
  20. package/helpers/zod.d.ts.map +1 -1
  21. package/helpers/zod.js +97 -7
  22. package/helpers/zod.js.map +1 -1
  23. package/helpers/zod.mjs +96 -7
  24. package/helpers/zod.mjs.map +1 -1
  25. package/internal/uploads.js +1 -1
  26. package/internal/uploads.js.map +1 -1
  27. package/internal/uploads.mjs +1 -1
  28. package/internal/uploads.mjs.map +1 -1
  29. package/lib/AbstractChatCompletionRunner.d.mts.map +1 -1
  30. package/lib/AbstractChatCompletionRunner.d.ts.map +1 -1
  31. package/lib/AbstractChatCompletionRunner.js +43 -1
  32. package/lib/AbstractChatCompletionRunner.js.map +1 -1
  33. package/lib/AbstractChatCompletionRunner.mjs +43 -1
  34. package/lib/AbstractChatCompletionRunner.mjs.map +1 -1
  35. package/lib/ChatCompletionStream.d.mts +2 -1
  36. package/lib/ChatCompletionStream.d.mts.map +1 -1
  37. package/lib/ChatCompletionStream.d.ts +2 -1
  38. package/lib/ChatCompletionStream.d.ts.map +1 -1
  39. package/lib/ChatCompletionStream.js +53 -14
  40. package/lib/ChatCompletionStream.js.map +1 -1
  41. package/lib/ChatCompletionStream.mjs +53 -14
  42. package/lib/ChatCompletionStream.mjs.map +1 -1
  43. package/lib/ResponsesParser.d.mts.map +1 -1
  44. package/lib/ResponsesParser.d.ts.map +1 -1
  45. package/lib/ResponsesParser.js +3 -5
  46. package/lib/ResponsesParser.js.map +1 -1
  47. package/lib/ResponsesParser.mjs +3 -5
  48. package/lib/ResponsesParser.mjs.map +1 -1
  49. package/lib/jsonschema.d.mts +3 -0
  50. package/lib/jsonschema.d.mts.map +1 -1
  51. package/lib/jsonschema.d.ts +3 -0
  52. package/lib/jsonschema.d.ts.map +1 -1
  53. package/lib/transform.d.mts +25 -1
  54. package/lib/transform.d.mts.map +1 -1
  55. package/lib/transform.d.ts +25 -1
  56. package/lib/transform.d.ts.map +1 -1
  57. package/lib/transform.js +1521 -82
  58. package/lib/transform.js.map +1 -1
  59. package/lib/transform.mjs +1515 -82
  60. package/lib/transform.mjs.map +1 -1
  61. package/package.json +4 -1
  62. package/resources/admin/organization/index.d.mts +1 -0
  63. package/resources/admin/organization/index.d.mts.map +1 -1
  64. package/resources/admin/organization/index.d.ts +1 -0
  65. package/resources/admin/organization/index.d.ts.map +1 -1
  66. package/resources/admin/organization/index.js +3 -1
  67. package/resources/admin/organization/index.js.map +1 -1
  68. package/resources/admin/organization/index.mjs +1 -0
  69. package/resources/admin/organization/index.mjs.map +1 -1
  70. package/resources/admin/organization/organization.d.mts +4 -0
  71. package/resources/admin/organization/organization.d.mts.map +1 -1
  72. package/resources/admin/organization/organization.d.ts +4 -0
  73. package/resources/admin/organization/organization.d.ts.map +1 -1
  74. package/resources/admin/organization/organization.js +4 -0
  75. package/resources/admin/organization/organization.js.map +1 -1
  76. package/resources/admin/organization/organization.mjs +4 -0
  77. package/resources/admin/organization/organization.mjs.map +1 -1
  78. package/resources/admin/organization/projects/index.d.mts +1 -0
  79. package/resources/admin/organization/projects/index.d.mts.map +1 -1
  80. package/resources/admin/organization/projects/index.d.ts +1 -0
  81. package/resources/admin/organization/projects/index.d.ts.map +1 -1
  82. package/resources/admin/organization/projects/index.js +3 -1
  83. package/resources/admin/organization/projects/index.js.map +1 -1
  84. package/resources/admin/organization/projects/index.mjs +1 -0
  85. package/resources/admin/organization/projects/index.mjs.map +1 -1
  86. package/resources/admin/organization/projects/projects.d.mts +4 -0
  87. package/resources/admin/organization/projects/projects.d.mts.map +1 -1
  88. package/resources/admin/organization/projects/projects.d.ts +4 -0
  89. package/resources/admin/organization/projects/projects.d.ts.map +1 -1
  90. package/resources/admin/organization/projects/projects.js +4 -0
  91. package/resources/admin/organization/projects/projects.js.map +1 -1
  92. package/resources/admin/organization/projects/projects.mjs +4 -0
  93. package/resources/admin/organization/projects/projects.mjs.map +1 -1
  94. package/resources/admin/organization/projects/spend-limit.d.mts +115 -0
  95. package/resources/admin/organization/projects/spend-limit.d.mts.map +1 -0
  96. package/resources/admin/organization/projects/spend-limit.d.ts +115 -0
  97. package/resources/admin/organization/projects/spend-limit.d.ts.map +1 -0
  98. package/resources/admin/organization/projects/spend-limit.js +67 -0
  99. package/resources/admin/organization/projects/spend-limit.js.map +1 -0
  100. package/resources/admin/organization/projects/spend-limit.mjs +63 -0
  101. package/resources/admin/organization/projects/spend-limit.mjs.map +1 -0
  102. package/resources/admin/organization/spend-limit.d.mts +108 -0
  103. package/resources/admin/organization/spend-limit.d.mts.map +1 -0
  104. package/resources/admin/organization/spend-limit.d.ts +108 -0
  105. package/resources/admin/organization/spend-limit.d.ts.map +1 -0
  106. package/resources/admin/organization/spend-limit.js +59 -0
  107. package/resources/admin/organization/spend-limit.js.map +1 -0
  108. package/resources/admin/organization/spend-limit.mjs +55 -0
  109. package/resources/admin/organization/spend-limit.mjs.map +1 -0
  110. package/resources/beta/responses/input-tokens.d.mts +6 -2
  111. package/resources/beta/responses/input-tokens.d.mts.map +1 -1
  112. package/resources/beta/responses/input-tokens.d.ts +6 -2
  113. package/resources/beta/responses/input-tokens.d.ts.map +1 -1
  114. package/resources/beta/responses/responses.d.mts +21 -11
  115. package/resources/beta/responses/responses.d.mts.map +1 -1
  116. package/resources/beta/responses/responses.d.ts +21 -11
  117. package/resources/beta/responses/responses.d.ts.map +1 -1
  118. package/resources/beta/responses/responses.js.map +1 -1
  119. package/resources/beta/responses/responses.mjs.map +1 -1
  120. package/resources/chat/completions/completions.d.mts +4 -3
  121. package/resources/chat/completions/completions.d.mts.map +1 -1
  122. package/resources/chat/completions/completions.d.ts +4 -3
  123. package/resources/chat/completions/completions.d.ts.map +1 -1
  124. package/resources/chat/completions/completions.js.map +1 -1
  125. package/resources/chat/completions/completions.mjs.map +1 -1
  126. package/resources/responses/input-tokens.d.mts +2 -1
  127. package/resources/responses/input-tokens.d.mts.map +1 -1
  128. package/resources/responses/input-tokens.d.ts +2 -1
  129. package/resources/responses/input-tokens.d.ts.map +1 -1
  130. package/resources/responses/responses.d.mts +9 -8
  131. package/resources/responses/responses.d.mts.map +1 -1
  132. package/resources/responses/responses.d.ts +9 -8
  133. package/resources/responses/responses.d.ts.map +1 -1
  134. package/resources/responses/responses.js.map +1 -1
  135. package/resources/responses/responses.mjs.map +1 -1
  136. package/resources/shared.d.mts +4 -1
  137. package/resources/shared.d.mts.map +1 -1
  138. package/resources/shared.d.ts +4 -1
  139. package/resources/shared.d.ts.map +1 -1
  140. package/src/azure.ts +5 -1
  141. package/src/helpers/standard-schema.ts +570 -0
  142. package/src/helpers/zod.ts +158 -29
  143. package/src/internal/uploads.ts +1 -1
  144. package/src/lib/AbstractChatCompletionRunner.ts +43 -1
  145. package/src/lib/ChatCompletionStream.ts +56 -8
  146. package/src/lib/ResponsesParser.ts +7 -5
  147. package/src/lib/jsonschema.ts +3 -0
  148. package/src/lib/transform.ts +1930 -103
  149. package/src/resources/admin/organization/index.ts +6 -0
  150. package/src/resources/admin/organization/organization.ts +16 -0
  151. package/src/resources/admin/organization/projects/index.ts +6 -0
  152. package/src/resources/admin/organization/projects/projects.ts +16 -0
  153. package/src/resources/admin/organization/projects/spend-limit.ts +156 -0
  154. package/src/resources/admin/organization/spend-limit.ts +144 -0
  155. package/src/resources/beta/responses/input-tokens.ts +6 -2
  156. package/src/resources/beta/responses/responses.ts +21 -10
  157. package/src/resources/chat/completions/completions.ts +4 -3
  158. package/src/resources/responses/input-tokens.ts +2 -1
  159. package/src/resources/responses/responses.ts +9 -7
  160. package/src/resources/shared.ts +4 -1
  161. package/src/version.ts +1 -1
  162. package/version.d.mts +1 -1
  163. package/version.d.mts.map +1 -1
  164. package/version.d.ts +1 -1
  165. package/version.d.ts.map +1 -1
  166. package/version.js +1 -1
  167. package/version.js.map +1 -1
  168. package/version.mjs +1 -1
  169. package/version.mjs.map +1 -1
@@ -1,34 +1,639 @@
1
1
  import type { JSONSchema, JSONSchemaDefinition } from './jsonschema';
2
2
 
3
+ const JSON_SCHEMA_ANNOTATION_KEYWORDS = new Set([
4
+ '$comment',
5
+ 'default',
6
+ 'description',
7
+ 'examples',
8
+ 'readOnly',
9
+ 'title',
10
+ 'writeOnly',
11
+ ]);
12
+
13
+ const JSON_SCHEMA_ROOT_METADATA_KEYWORDS = new Set(['$id', '$schema']);
14
+
15
+ const JSON_SCHEMA_OBJECT_KEYWORDS = new Set([
16
+ 'additionalProperties',
17
+ 'dependencies',
18
+ 'maxProperties',
19
+ 'minProperties',
20
+ 'patternProperties',
21
+ 'properties',
22
+ 'propertyNames',
23
+ 'required',
24
+ ]);
25
+
26
+ const JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS = [
27
+ 'additionalItems',
28
+ 'additionalProperties',
29
+ 'contains',
30
+ 'contentSchema',
31
+ 'else',
32
+ 'if',
33
+ 'not',
34
+ 'propertyNames',
35
+ 'then',
36
+ 'unevaluatedItems',
37
+ 'unevaluatedProperties',
38
+ ];
39
+
40
+ const JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS = ['allOf', 'anyOf', 'items', 'oneOf', 'prefixItems'];
41
+
42
+ const JSON_SCHEMA_MAP_SCHEMA_KEYWORDS = [
43
+ '$defs',
44
+ 'definitions',
45
+ 'dependentSchemas',
46
+ 'dependencies',
47
+ 'patternProperties',
48
+ 'properties',
49
+ ];
50
+
51
+ const JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
52
+ '$anchor',
53
+ '$dynamicAnchor',
54
+ '$dynamicRef',
55
+ '$recursiveAnchor',
56
+ '$recursiveRef',
57
+ 'allOf',
58
+ 'contains',
59
+ 'contentEncoding',
60
+ 'contentMediaType',
61
+ 'contentSchema',
62
+ 'dependentRequired',
63
+ 'dependentSchemas',
64
+ 'dependencies',
65
+ 'else',
66
+ 'if',
67
+ 'maxContains',
68
+ 'maxProperties',
69
+ 'minContains',
70
+ 'minProperties',
71
+ 'not',
72
+ 'patternProperties',
73
+ 'prefixItems',
74
+ 'propertyNames',
75
+ 'then',
76
+ 'unevaluatedItems',
77
+ 'unevaluatedProperties',
78
+ 'uniqueItems',
79
+ ]);
80
+
81
+ const MERGEABLE_OBJECT_ALL_OF_KEYWORDS = new Set([
82
+ ...JSON_SCHEMA_ANNOTATION_KEYWORDS,
83
+ 'additionalProperties',
84
+ 'properties',
85
+ 'required',
86
+ 'type',
87
+ ]);
88
+
89
+ type JSONSchemaChildVisitor = (schema: unknown, path: string[], keyword: string) => void;
90
+
91
+ /**
92
+ * Visits only values carried by JSON Schema keywords that contain schemas.
93
+ * Literal payloads such as enum, const, and default deliberately do not
94
+ * participate.
95
+ */
96
+ export function forEachJSONSchemaChild(
97
+ schema: JSONSchema | Record<string, unknown>,
98
+ path: string[],
99
+ visit: JSONSchemaChildVisitor,
100
+ ): void {
101
+ const record = schema as Record<string, unknown>;
102
+
103
+ for (const keyword of JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS) {
104
+ if (keyword in record) {
105
+ visit(record[keyword], [...path, keyword], keyword);
106
+ }
107
+ }
108
+
109
+ for (const keyword of JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS) {
110
+ const children = record[keyword];
111
+ if (Array.isArray(children)) {
112
+ for (const [index, child] of children.entries()) {
113
+ visit(child, [...path, keyword, String(index)], keyword);
114
+ }
115
+ } else if (children !== undefined) {
116
+ visit(children, [...path, keyword], keyword);
117
+ }
118
+ }
119
+
120
+ for (const keyword of JSON_SCHEMA_MAP_SCHEMA_KEYWORDS) {
121
+ const children = record[keyword];
122
+ if (!isObject(children)) continue;
123
+
124
+ for (const [key, child] of Object.entries(children)) {
125
+ // Draft 7 dependencies also permits property dependency arrays. They
126
+ // are not schemas and must not be traversed as literal JSON payloads.
127
+ if (keyword === 'dependencies' && !isSchemaDefinition(child)) continue;
128
+ visit(child, [...path, keyword, key], keyword);
129
+ }
130
+ }
131
+ }
132
+
3
133
  export function toStrictJsonSchema(schema: JSONSchema): JSONSchema {
4
- if (schema.type !== 'object') {
134
+ const schemaCopy = structuredClone(schema);
135
+ // JSON serialization omits undefined object properties. Drop optional
136
+ // placeholders before any structural checks so they cannot accidentally
137
+ // look like validation-bearing schema keywords.
138
+ stripUndefinedSchemaKeywords(schemaCopy);
139
+ normalizeSingletonTypeArrays(schemaCopy);
140
+ // Root ref/allOf normalization can promote a nested branch into the root.
141
+ // Reject separate resource scopes while their original nesting is still
142
+ // visible so branch-local definitions cannot be rebound at the root.
143
+ assertNoNestedSchemaIds(schemaCopy);
144
+ normalizeRootRefAndAllOf(schemaCopy);
145
+
146
+ if (schemaCopy.type !== 'object') {
147
+ throw new Error(
148
+ `Root schema must have type: 'object' but got type: ${
149
+ schemaCopy.type ? `'${schemaCopy.type}'` : 'undefined'
150
+ }`,
151
+ );
152
+ }
153
+ if (schemaCopy.anyOf !== undefined) {
5
154
  throw new Error(
6
- `Root schema must have type: 'object' but got type: ${schema.type ? `'${schema.type}'` : 'undefined'}`,
155
+ 'Root schema must not use `anyOf` because strict Structured Outputs requires a root object without a union.',
7
156
  );
8
157
  }
9
158
 
10
- const schemaCopy = structuredClone(schema);
11
- return ensureStrictJsonSchema(schemaCopy, [], schemaCopy);
159
+ validateRefSchemas(schemaCopy, [], schemaCopy);
160
+ preserveAllOfRefTargets(schemaCopy);
161
+ validateRefSchemas(schemaCopy, [], schemaCopy);
162
+ rewriteLocalRefsIntoFilteredAnyOfBranches(schemaCopy);
163
+ // Resolve representable object intersections before recursive
164
+ // strictification closes referenced definitions. Otherwise a definition
165
+ // reached through an allOf ref would be closed in isolation before its
166
+ // sibling object properties can be merged.
167
+ normalizeObjectAllOfBranches(schemaCopy, [], schemaCopy);
168
+ const strictSchema = ensureStrictJsonSchema(schemaCopy, [], schemaCopy);
169
+ validateRefSchemas(strictSchema, [], strictSchema);
170
+ return strictSchema;
171
+ }
172
+
173
+ function stripUndefinedSchemaKeywords(
174
+ schema: JSONSchemaDefinition,
175
+ visited: Set<JSONSchema> = new Set(),
176
+ ): void {
177
+ if (typeof schema === 'boolean' || !isObject(schema) || visited.has(schema)) {
178
+ return;
179
+ }
180
+ visited.add(schema);
181
+
182
+ const schemaRecord = schema as Record<string, unknown>;
183
+ for (const keyword of Object.keys(schemaRecord)) {
184
+ if (schemaRecord[keyword] === undefined) {
185
+ delete schemaRecord[keyword];
186
+ }
187
+ }
188
+
189
+ forEachJSONSchemaChild(schema, [], (child) => {
190
+ stripUndefinedSchemaKeywords(child as JSONSchemaDefinition, visited);
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Root ref inlining and singleton allOf flattening can expose each other.
196
+ * Iterate until flattening no longer produces another root ref so every
197
+ * exactly representable chain reaches its final object form before the root
198
+ * type check runs.
199
+ */
200
+ function normalizeRootRefAndAllOf(schema: JSONSchema): void {
201
+ const seenRefs = new Set<string>();
202
+ while (true) {
203
+ if (typeof schema.$ref === 'string') {
204
+ if (seenRefs.has(schema.$ref)) {
205
+ throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(schema.$ref));
206
+ }
207
+ seenRefs.add(schema.$ref);
208
+ }
209
+
210
+ inlineRootRefObject(schema);
211
+ preserveAllOfRefTargets(schema, true);
212
+ normalizeRootAllOf(schema);
213
+ const normalizedAnyOf = normalizeRootAnyOf(schema);
214
+
215
+ if (schema.$ref === undefined && !normalizedAnyOf) {
216
+ return;
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Some Standard Schema converters emit the root object through a local ref,
223
+ * with the referenced schema stored in a root definition map. Structured
224
+ * Outputs requires the root itself to be an object, so inline that safe,
225
+ * definition-only form while keeping the root maps available for every local
226
+ * pointer in the schema.
227
+ */
228
+ function inlineRootRefObject(schema: JSONSchema): void {
229
+ let ref = schema.$ref;
230
+ if (ref === undefined) {
231
+ return;
232
+ }
233
+
234
+ assertLocalRootRef(ref);
235
+ if (!hasOnlyRootRefAndDefinitions(schema)) {
236
+ throw new Error(
237
+ 'Schema $ref at `<root>` has non-metadata siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.',
238
+ );
239
+ }
240
+
241
+ const seenRefs = new Set<string>();
242
+ // Ref siblings are annotations in Draft 7, so keep them while following
243
+ // aliases. Add outer annotations first so they win over inner aliases and
244
+ // the final target when the effective root is assembled.
245
+ const inheritedAnnotations: Record<string, unknown> = Object.fromEntries(
246
+ Object.entries(schema).filter(([keyword]) => JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)),
247
+ );
248
+ let resolved: JSONSchema;
249
+ while (true) {
250
+ if (seenRefs.has(ref)) {
251
+ throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(ref));
252
+ }
253
+ seenRefs.add(ref);
254
+
255
+ const target = resolveLocalRef(schema, ref);
256
+ if (target === undefined) {
257
+ throw new Error(
258
+ 'Local $ref at `<root>` does not resolve to an object or boolean schema: ' + JSON.stringify(ref),
259
+ );
260
+ }
261
+ if (typeof target === 'boolean') {
262
+ throw new TypeError('Expected object schema but got boolean; path=<root>');
263
+ }
264
+
265
+ const nextRef = target.$ref;
266
+ if (nextRef === undefined) {
267
+ resolved = target;
268
+ break;
269
+ }
270
+ assertLocalRootRef(nextRef);
271
+ if (seenRefs.has(nextRef)) {
272
+ throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(nextRef));
273
+ }
274
+ if (!hasOnlyRefAndAnnotations(target)) {
275
+ throw new Error(
276
+ 'Schema $ref in root chain has non-annotation siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.',
277
+ );
278
+ }
279
+ for (const keyword of JSON_SCHEMA_ANNOTATION_KEYWORDS) {
280
+ if (!(keyword in inheritedAnnotations) && keyword in target) {
281
+ inheritedAnnotations[keyword] = (target as Record<string, unknown>)[keyword];
282
+ }
283
+ }
284
+ ref = nextRef;
285
+ }
286
+
287
+ const rootDefinitions = schema.$defs;
288
+ const legacyDefinitions = schema.definitions;
289
+ const rootMetadata = Object.fromEntries(
290
+ Object.entries(schema).filter(
291
+ ([keyword]) =>
292
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword) || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword),
293
+ ),
294
+ );
295
+ const inlined = structuredClone(resolved);
296
+ // A target's definition map lives below the target's original pointer, not
297
+ // at the document root. Keep the outer document map at the root so existing
298
+ // absolute refs retain their meaning; the retained outer map also keeps the
299
+ // target's nested map reachable at its original pointer.
300
+ for (const keyword of ['$defs', 'definitions'] as const) {
301
+ if (schema[keyword] !== undefined && inlined[keyword] !== undefined) {
302
+ delete inlined[keyword];
303
+ }
304
+ }
305
+ const schemaRecord = schema as Record<string, unknown>;
306
+
307
+ for (const keyword of Object.keys(schema)) {
308
+ delete schemaRecord[keyword];
309
+ }
310
+ Object.assign(schema, inlined, inheritedAnnotations, rootMetadata);
311
+ if (rootDefinitions !== undefined) {
312
+ schema.$defs = rootDefinitions;
313
+ }
314
+ if (legacyDefinitions !== undefined) {
315
+ schema.definitions = legacyDefinitions;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Root object validation runs before recursive strictification, so normalize
321
+ * the same exactly representable allOf forms here that the recursive pass
322
+ * handles for nested schemas.
323
+ */
324
+ function normalizeRootAllOf(schema: JSONSchema): void {
325
+ while (schema.allOf !== undefined) {
326
+ // Root normalization runs before recursive strictification so the root
327
+ // type check can see an inlined object. Normalize nested intersections in
328
+ // its branches first for the same associative allOf case handled by the
329
+ // later recursive pass.
330
+ if (Array.isArray(schema.allOf)) {
331
+ for (const [index, branch] of schema.allOf.entries()) {
332
+ normalizeObjectAllOfBranches(branch, ['allOf', String(index)], schema);
333
+ }
334
+ }
335
+
336
+ if (mergeObjectAllOf(schema, [], schema)) {
337
+ // Removing neutral true branches can leave a singleton allOf behind;
338
+ // keep normalizing until the root reaches its final object form.
339
+ continue;
340
+ }
341
+
342
+ const allOf = schema.allOf;
343
+ if (!Array.isArray(allOf) || allOf.length !== 1 || !hasOnlyRootAllOfMetadataSiblings(schema)) {
344
+ return;
345
+ }
346
+
347
+ const branch = allOf[0];
348
+ if (typeof branch === 'boolean' || !isObject(branch)) {
349
+ return;
350
+ }
351
+
352
+ const rootMetadata = { ...schema };
353
+ delete rootMetadata.allOf;
354
+ const normalized = structuredClone(branch);
355
+ const schemaRecord = schema as Record<string, unknown>;
356
+
357
+ for (const keyword of Object.keys(schema)) {
358
+ delete schemaRecord[keyword];
359
+ }
360
+ Object.assign(schema, normalized, rootMetadata);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * A singleton root anyOf with no validating siblings is equivalent to its
366
+ * only branch. Flatten it before the root-union check so converters that
367
+ * retain a redundant object wrapper can still produce a strict root object.
368
+ */
369
+ function normalizeRootAnyOf(schema: JSONSchema): boolean {
370
+ const anyOf = schema.anyOf;
371
+ if (!Array.isArray(anyOf) || !hasOnlyRootAnyOfMetadataSiblings(schema)) {
372
+ return false;
373
+ }
374
+
375
+ // `false` contributes no instances to a union. Keep the original array in
376
+ // place until promotion so refs into the surviving branch can be rewritten
377
+ // from their original index, while refs into removed false branches become
378
+ // dangling and remain fail closed after the wrapper disappears.
379
+ const realBranches = anyOf
380
+ .map((branch, index) => ({ branch, index }))
381
+ .filter(({ branch }) => branch !== false);
382
+ if (realBranches.length !== 1) {
383
+ return false;
384
+ }
385
+
386
+ const { branch, index: branchIndex } = realBranches[0]!;
387
+ if (typeof branch === 'boolean' || !isObject(branch) || !isObjectOnlySchema(branch, schema)) {
388
+ return false;
389
+ }
390
+
391
+ const definitionRenames = planPromotedRootAnyOfDefinitionRenames(schema, branch);
392
+ rewriteLocalRefsIntoPromotedRootAnyOfBranch(schema, branchIndex, definitionRenames);
393
+ const rootMetadata = { ...schema };
394
+ delete rootMetadata.anyOf;
395
+ const normalized = structuredClone(branch);
396
+
397
+ for (const keyword of ['$defs', 'definitions'] as const) {
398
+ const rootDefinitions = schema[keyword];
399
+ const branchDefinitions = normalized[keyword];
400
+ if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) {
401
+ continue;
402
+ }
403
+
404
+ const renames = definitionRenames.get(keyword);
405
+ const mergedDefinitions: Record<string, JSONSchemaDefinition> = { ...rootDefinitions };
406
+ for (const [name, definition] of Object.entries(branchDefinitions)) {
407
+ mergedDefinitions[renames?.get(name) ?? name] = definition as JSONSchemaDefinition;
408
+ }
409
+ normalized[keyword] = mergedDefinitions;
410
+ delete rootMetadata[keyword];
411
+ }
412
+
413
+ const schemaRecord = schema as Record<string, unknown>;
414
+
415
+ for (const keyword of Object.keys(schema)) {
416
+ delete schemaRecord[keyword];
417
+ }
418
+ Object.assign(schema, normalized, rootMetadata);
419
+ return true;
420
+ }
421
+
422
+ type RootDefinitionKeyword = '$defs' | 'definitions';
423
+ type PromotedRootAnyOfDefinitionRenames = Map<RootDefinitionKeyword, Map<string, string>>;
424
+
425
+ /**
426
+ * Root and promoted branch definition maps occupy the same pointer after
427
+ * promotion. Give conflicting branch definitions stable aliases before refs
428
+ * are rewritten so neither original target is rebound.
429
+ */
430
+ function planPromotedRootAnyOfDefinitionRenames(
431
+ root: JSONSchema,
432
+ branch: JSONSchema,
433
+ ): PromotedRootAnyOfDefinitionRenames {
434
+ const renames: PromotedRootAnyOfDefinitionRenames = new Map();
435
+
436
+ for (const keyword of ['$defs', 'definitions'] as const) {
437
+ const rootDefinitions = root[keyword];
438
+ const branchDefinitions = branch[keyword];
439
+ if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) {
440
+ continue;
441
+ }
442
+
443
+ const usedNames = new Set([...Object.keys(rootDefinitions), ...Object.keys(branchDefinitions)]);
444
+ const keywordRenames = new Map<string, string>();
445
+ let aliasIndex = 0;
446
+
447
+ for (const [name, definition] of Object.entries(branchDefinitions)) {
448
+ if (
449
+ !Object.prototype.hasOwnProperty.call(rootDefinitions, name) ||
450
+ schemasEqual(rootDefinitions[name], definition)
451
+ ) {
452
+ continue;
453
+ }
454
+
455
+ let alias = '__openai_strict_anyOf_definition_' + aliasIndex++;
456
+ while (usedNames.has(alias)) {
457
+ alias = '__openai_strict_anyOf_definition_' + aliasIndex++;
458
+ }
459
+ usedNames.add(alias);
460
+ keywordRenames.set(name, alias);
461
+ }
462
+
463
+ if (keywordRenames.size > 0) {
464
+ renames.set(keyword, keywordRenames);
465
+ }
466
+ }
467
+
468
+ return renames;
469
+ }
470
+
471
+ /**
472
+ * Promoting a singleton root anyOf branch removes the original anyOf/index
473
+ * pointer prefix. Rewrite refs through that prefix while the old tree still
474
+ * exists so the promoted schema keeps naming the same targets.
475
+ */
476
+ function rewriteLocalRefsIntoPromotedRootAnyOfBranch(
477
+ root: JSONSchema,
478
+ branchIndex: number,
479
+ definitionRenames: PromotedRootAnyOfDefinitionRenames,
480
+ ): void {
481
+ const rewriteRef = (ref: string): string => {
482
+ const parts = parseLocalRef(ref);
483
+ if (parts === undefined || parts[0] !== 'anyOf' || parts[1] !== String(branchIndex)) {
484
+ return ref;
485
+ }
486
+
487
+ const promotedParts = parts.slice(2);
488
+ const definitionKeyword = promotedParts[0];
489
+ if (promotedParts.length > 1 && (definitionKeyword === '$defs' || definitionKeyword === 'definitions')) {
490
+ const renamed = definitionRenames.get(definitionKeyword)?.get(promotedParts[1]!);
491
+ if (renamed !== undefined) {
492
+ promotedParts[1] = renamed;
493
+ }
494
+ }
495
+
496
+ return promotedParts.length === 0 ?
497
+ '#'
498
+ : '#/' + promotedParts.map(encodeJSONPointerTokenForURIFragment).join('/');
499
+ };
500
+
501
+ const rewriteRefs = (value: JSONSchemaDefinition): void => {
502
+ if (typeof value === 'boolean' || !isObject(value)) {
503
+ return;
504
+ }
505
+
506
+ if (typeof value.$ref === 'string') {
507
+ value.$ref = rewriteRef(value.$ref);
508
+ }
509
+
510
+ forEachJSONSchemaChild(value, [], (child) => {
511
+ rewriteRefs(child as JSONSchemaDefinition);
512
+ });
513
+ };
514
+
515
+ rewriteRefs(root);
516
+ }
517
+
518
+ function assertLocalRootRef(ref: unknown): asserts ref is string {
519
+ if (typeof ref !== 'string') {
520
+ throw new TypeError('Received non-string $ref - ' + String(ref) + '; path=<root>');
521
+ }
522
+ if (!ref.startsWith('#')) {
523
+ throw new Error(
524
+ 'External $ref at `<root>` is not supported in strict Structured Outputs: ' + JSON.stringify(ref),
525
+ );
526
+ }
527
+ }
528
+
529
+ function hasOnlyRootRefAndDefinitions(schema: JSONSchema): boolean {
530
+ return Object.keys(schema).every(
531
+ (keyword) =>
532
+ keyword === '$ref' ||
533
+ keyword === '$defs' ||
534
+ keyword === 'definitions' ||
535
+ JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
536
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
537
+ );
538
+ }
539
+
540
+ /**
541
+ * Draft 7 permits `type` to be either a string or an array of strings. A
542
+ * singleton array has exactly the same validation semantics as its scalar
543
+ * form, so canonicalize it before root validation and recursive strictifying.
544
+ * Multi-type arrays carry real union semantics and must remain unchanged.
545
+ */
546
+ function normalizeSingletonTypeArrays(schema: JSONSchemaDefinition): void {
547
+ if (typeof schema === 'boolean' || !isObject(schema)) {
548
+ return;
549
+ }
550
+
551
+ if (Array.isArray(schema.type) && schema.type.length === 1) {
552
+ schema.type = schema.type[0]!;
553
+ }
554
+
555
+ forEachJSONSchemaChild(schema, [], (child) => {
556
+ normalizeSingletonTypeArrays(child as JSONSchemaDefinition);
557
+ });
12
558
  }
13
559
 
14
- function isNullable(schema: JSONSchemaDefinition): boolean {
560
+ function isNullable(
561
+ schema: JSONSchemaDefinition,
562
+ root: JSONSchema,
563
+ seenRefs: Set<string> = new Set(),
564
+ ): boolean {
15
565
  if (typeof schema === 'boolean') {
566
+ return schema;
567
+ }
568
+
569
+ const ref = schema.$ref;
570
+ if (ref !== undefined) {
571
+ // Annotation keywords do not constrain validation, so they are safe beside
572
+ // a local ref. Keep the proof conservative for every other sibling because
573
+ // resolving those correctly would require intersecting the referenced
574
+ // schema and its sibling constraints.
575
+ if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(ref)) {
576
+ return false;
577
+ }
578
+
579
+ const resolved = resolveLocalRef(root, ref);
580
+ if (resolved === undefined) {
581
+ return false;
582
+ }
583
+
584
+ return isNullable(resolved, root, new Set([...seenRefs, ref]));
585
+ }
586
+
587
+ if (
588
+ schema.type !== undefined &&
589
+ schema.type !== 'null' &&
590
+ !(Array.isArray(schema.type) && schema.type.includes('null'))
591
+ ) {
16
592
  return false;
17
593
  }
18
- if (schema.type === 'null') {
19
- return true;
594
+
595
+ if ('const' in schema && schema.const !== null) {
596
+ return false;
20
597
  }
21
- for (const oneOfVariant of schema.oneOf ?? []) {
22
- if (isNullable(oneOfVariant)) {
23
- return true;
598
+
599
+ if (schema.enum !== undefined && (!Array.isArray(schema.enum) || !schema.enum.includes(null))) {
600
+ return false;
601
+ }
602
+
603
+ if (schema.allOf !== undefined) {
604
+ if (!Array.isArray(schema.allOf) || !schema.allOf.every((variant) => isNullable(variant, root))) {
605
+ return false;
24
606
  }
25
607
  }
26
- for (const allOfVariant of schema.anyOf ?? []) {
27
- if (isNullable(allOfVariant)) {
28
- return true;
608
+
609
+ if (schema.anyOf !== undefined) {
610
+ if (!Array.isArray(schema.anyOf) || !schema.anyOf.some((variant) => isNullable(variant, root))) {
611
+ return false;
29
612
  }
30
613
  }
31
- return false;
614
+
615
+ if (schema.oneOf !== undefined) {
616
+ if (
617
+ !Array.isArray(schema.oneOf) ||
618
+ schema.oneOf.filter((variant) => isNullable(variant, root)).length !== 1
619
+ ) {
620
+ return false;
621
+ }
622
+ }
623
+
624
+ // Conditional and negated schemas need a full JSON Schema evaluator to prove
625
+ // that null is allowed. Treat them conservatively instead of accepting an
626
+ // optional field that may not actually accept null.
627
+ if (
628
+ schema.not !== undefined ||
629
+ schema.if !== undefined ||
630
+ schema.then !== undefined ||
631
+ schema.else !== undefined
632
+ ) {
633
+ return false;
634
+ }
635
+
636
+ return true;
32
637
  }
33
638
 
34
639
  /**
@@ -48,146 +653,1368 @@ function ensureStrictJsonSchema(
48
653
  throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be an object; path=${path.join('/')}`);
49
654
  }
50
655
 
51
- // Handle $defs (non-standard but sometimes used)
52
- const defs = (jsonSchema as any).$defs;
53
- if (isObject(defs)) {
54
- for (const [defName, defSchema] of Object.entries(defs)) {
55
- ensureStrictJsonSchema(defSchema as JSONSchema, [...path, '$defs', defName], root);
56
- }
656
+ // Closing each object branch in an allOf independently changes the
657
+ // intersection: sibling branches' properties become forbidden extras. Merge
658
+ // the small object-intersection subset that can be represented exactly before
659
+ // applying strict object closure, and fail closed for the rest.
660
+ if (mergeObjectAllOf(jsonSchema, path, root)) {
661
+ return ensureStrictJsonSchema(jsonSchema, path, root);
57
662
  }
58
663
 
59
- // Handle definitions (draft-04 style, deprecated in draft-07 but still used)
60
- const definitions = (jsonSchema as any).definitions;
61
- if (isObject(definitions)) {
62
- for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
63
- ensureStrictJsonSchema(definitionSchema as JSONSchema, [...path, 'definitions', definitionName], root);
64
- }
65
- }
664
+ // false is the identity element for a union. Remove it before proving
665
+ // whether an outer object/array wrapper is redundant so an impossible
666
+ // alternative cannot hide the shape of every real branch. Keep all-false
667
+ // and boolean-only unions intact so the existing boolean-schema rejection
668
+ // remains fail closed.
669
+ normalizeAnyOfFalseBranches(jsonSchema);
66
670
 
67
- // Add additionalProperties: false to object types
68
- const typ = jsonSchema.type;
69
- if (typ === 'object' && !('additionalProperties' in jsonSchema)) {
70
- jsonSchema.additionalProperties = false;
671
+ // Closing a type: object wrapper around object union branches without any
672
+ // own properties would turn it into an empty object and forbid every branch
673
+ // property. A bare object type is redundant when every branch already proves
674
+ // the value is an object, so remove only that exact constraint; fail closed
675
+ // for wrappers whose object constraints cannot be preserved mechanically.
676
+ normalizeObjectUnionWrapper(jsonSchema, path, root);
677
+
678
+ // Add additionalProperties: false to object schemas. Draft 7 permits object
679
+ // keywords without an explicit type, so those implicit object shapes need
680
+ // the same strict handling as type: 'object'. Explicitly open object schemas
681
+ // cannot be represented in Structured Outputs strict mode.
682
+ if (hasObjectShape(jsonSchema)) {
683
+ if (!('additionalProperties' in jsonSchema)) {
684
+ jsonSchema.additionalProperties = false;
685
+ } else if (jsonSchema.additionalProperties !== false) {
686
+ throw new Error(
687
+ `Object schema at \`${
688
+ path.join('/') || '<root>'
689
+ }\` must set \`additionalProperties: false\` to be compatible with strict Structured Outputs.`,
690
+ );
691
+ }
71
692
  }
72
693
 
73
694
  const required = jsonSchema.required ?? [];
695
+ if (!Array.isArray(required) || required.some((key) => typeof key !== 'string')) {
696
+ throw new TypeError(
697
+ `Expected \`required\` to be an array of strings; path=${path.join('/') || '<root>'}`,
698
+ );
699
+ }
74
700
 
75
701
  // Handle object properties
76
702
  const properties = jsonSchema.properties;
703
+ if (hasObjectShape(jsonSchema)) {
704
+ for (const key of required) {
705
+ if (!isObject(properties) || !Object.prototype.hasOwnProperty.call(properties, key)) {
706
+ throw new Error(
707
+ `Object schema at \`${
708
+ path.join('/') || '<root>'
709
+ }\` requires property \`${key}\` but does not declare it in \`properties\`.`,
710
+ );
711
+ }
712
+ }
713
+ }
77
714
  if (isObject(properties)) {
78
715
  for (const [key, value] of Object.entries(properties)) {
79
- if (!isNullable(value) && !required.includes(key)) {
716
+ if (!isNullable(value, root) && !required.includes(key)) {
80
717
  throw new Error(
81
- `Zod field at \`${[...path, 'properties', key].join(
718
+ `Schema field at \`${[...path, 'properties', key].join(
82
719
  '/',
83
720
  )}\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required`,
84
721
  );
85
722
  }
86
723
  }
87
724
  jsonSchema.required = Object.keys(properties);
88
- jsonSchema.properties = Object.fromEntries(
89
- Object.entries(properties).map(([key, propSchema]) => [
90
- key,
91
- ensureStrictJsonSchema(propSchema, [...path, 'properties', key], root),
92
- ]),
93
- );
94
725
  }
95
726
 
96
- // Handle arrays
727
+ // Structured Outputs accepts one schema for every array item and does not
728
+ // support Draft 7 tuples or additionalItems. Reject both forms rather than
729
+ // advertising a schema whose array validation the API cannot preserve.
97
730
  const items = jsonSchema.items;
98
- if (isObject(items)) {
99
- jsonSchema.items = ensureStrictJsonSchema(items, [...path, 'items'], root);
731
+ const additionalItems = jsonSchema.additionalItems;
732
+ if (Array.isArray(items)) {
733
+ throw new Error(
734
+ `Schema at \`${
735
+ path.join('/') || '<root>'
736
+ }\` uses tuple-form \`items\`, which cannot be represented in strict Structured Outputs.`,
737
+ );
100
738
  }
101
-
102
- // Handle unions (anyOf)
103
- const anyOf = jsonSchema.anyOf;
104
- if (Array.isArray(anyOf)) {
105
- jsonSchema.anyOf = anyOf.map((variant, i) =>
106
- ensureStrictJsonSchema(variant, [...path, 'anyOf', String(i)], root),
739
+ if (additionalItems !== undefined) {
740
+ throw new Error(
741
+ `Schema at \`${
742
+ path.join('/') || '<root>'
743
+ }\` uses unsupported keyword \`additionalItems\` and cannot be represented in strict Structured Outputs.`,
107
744
  );
108
745
  }
109
746
 
110
747
  // Handle intersections (allOf)
111
748
  const allOf = jsonSchema.allOf;
112
749
  if (Array.isArray(allOf)) {
113
- if (allOf.length === 1) {
114
- const resolved = ensureStrictJsonSchema(allOf[0]!, [...path, 'allOf', '0'], root);
115
- Object.assign(jsonSchema, resolved);
116
- delete jsonSchema.allOf;
117
- } else {
118
- jsonSchema.allOf = allOf.map((entry, i) =>
119
- ensureStrictJsonSchema(entry, [...path, 'allOf', String(i)], root),
120
- );
750
+ if (allOf.length === 1 && hasOnlyAnnotationSiblings(jsonSchema, 'allOf')) {
751
+ const branch = allOf[0]!;
752
+ if (branch === false) {
753
+ throw new Error(
754
+ `Schema at \`${
755
+ path.join('/') || '<root>'
756
+ }\` uses \`allOf: [false]\`, which cannot be represented in strict Structured Outputs.`,
757
+ );
758
+ }
759
+ if (branch === true) {
760
+ // true is the neutral schema for an intersection, so removing this
761
+ // branch preserves validation while retaining the parent annotations.
762
+ delete jsonSchema.allOf;
763
+ } else {
764
+ const resolved = ensureStrictJsonSchema(branch, [...path, 'allOf', '0'], root);
765
+ const annotations = { ...jsonSchema };
766
+ delete annotations.allOf;
767
+ Object.assign(jsonSchema, resolved, annotations);
768
+ delete jsonSchema.allOf;
769
+ }
121
770
  }
122
771
  }
123
772
 
124
- // Strip `null` defaults as there's no meaningful distinction
125
- if (jsonSchema.default === null) {
126
- delete jsonSchema.default;
127
- }
128
-
129
- // Handle $ref with additional properties
130
- const ref = (jsonSchema as any).$ref;
131
- if (ref && hasMoreThanNKeys(jsonSchema, 1)) {
132
- if (typeof ref !== 'string') {
133
- throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
134
- }
773
+ normalizeArrayUnionWrapper(jsonSchema, root);
135
774
 
136
- const resolved = resolveRef(root, ref);
137
- if (typeof resolved === 'boolean') {
138
- throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`);
139
- }
140
- if (!isObject(resolved)) {
775
+ const schemaRecord = jsonSchema as Record<string, unknown>;
776
+ for (const keyword of JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS) {
777
+ // Optional converter output often keeps undefined placeholders on the
778
+ // JavaScript object even though JSON serialization omits them. They carry
779
+ // no validation semantics, so treat only undefined as absent; every
780
+ // defined value remains unsupported.
781
+ if (schemaRecord[keyword] !== undefined) {
141
782
  throw new Error(
142
- `Expected \`$ref: ${ref}\` to resolve to an object but got ${JSON.stringify(resolved)}`,
783
+ `Schema at \`${
784
+ path.join('/') || '<root>'
785
+ }\` uses unsupported keyword \`${keyword}\` and cannot be represented in strict Structured Outputs.`,
143
786
  );
144
787
  }
788
+ delete schemaRecord[keyword];
789
+ }
790
+
791
+ const type = jsonSchema.type;
792
+ const currentItems = jsonSchema.items;
793
+ if ((type === 'array' || (Array.isArray(type) && type.includes('array'))) && currentItems === undefined) {
794
+ throw new Error(
795
+ `Schema at \`${
796
+ path.join('/') || '<root>'
797
+ }\` declares an array without \`items\`, which cannot be represented in strict Structured Outputs.`,
798
+ );
799
+ }
800
+
801
+ forEachJSONSchemaChild(jsonSchema, path, (child, childPath, keyword) => {
802
+ // These boolean forms are already handled as parent-keyword semantics:
803
+ // additionalProperties: false closes objects, while boolean
804
+ // additionalItems does not contain a nested schema to strictify.
805
+ if (typeof child === 'boolean' && (keyword === 'additionalProperties' || keyword === 'additionalItems')) {
806
+ return;
807
+ }
145
808
 
146
- // Properties from the json schema take priority over the ones on the `$ref`
147
- Object.assign(jsonSchema, { ...resolved, ...jsonSchema });
148
- delete (jsonSchema as any).$ref;
809
+ ensureStrictJsonSchema(child as JSONSchemaDefinition, childPath, root);
810
+ });
149
811
 
150
- // Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,
151
- // we call `ensureStrictJsonSchema` again to fix the inlined schema and ensure it's valid.
152
- return ensureStrictJsonSchema(jsonSchema, path, root);
812
+ // Strip `null` defaults as there's no meaningful distinction
813
+ if (jsonSchema.default === null) {
814
+ delete jsonSchema.default;
153
815
  }
154
816
 
155
817
  return jsonSchema;
156
818
  }
157
819
 
158
- function resolveRef(root: JSONSchema, ref: string): JSONSchemaDefinition {
159
- if (!ref.startsWith('#/')) {
160
- throw new Error(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`);
820
+ function parseLocalRef(ref: string): string[] | undefined {
821
+ if (!ref.startsWith('#')) {
822
+ return undefined;
161
823
  }
162
824
 
163
- const pathParts = ref.slice(2).split('/');
164
- let resolved: any = root;
825
+ let pointer: string;
826
+ try {
827
+ // A local $ref is a URI fragment containing a JSON Pointer. RFC 6901
828
+ // decodes the complete fragment before tokenizing the pointer, so an
829
+ // encoded slash is a separator rather than part of a literal key.
830
+ pointer = decodeURIComponent(ref.slice(1));
831
+ } catch {
832
+ return undefined;
833
+ }
165
834
 
166
- for (const key of pathParts) {
167
- if (!isObject(resolved)) {
168
- throw new Error(`encountered non-object entry while resolving ${ref} - ${JSON.stringify(resolved)}`);
169
- }
170
- const value = resolved[key];
171
- if (value === undefined) {
172
- throw new Error(`Key ${key} not found while resolving ${ref}`);
835
+ if (pointer === '') {
836
+ return [];
837
+ }
838
+ if (!pointer.startsWith('/')) {
839
+ return undefined;
840
+ }
841
+
842
+ const parts: string[] = [];
843
+ for (const encodedPart of pointer.slice(1).split('/')) {
844
+ // JSON Pointer only defines ~0 and ~1 escapes. Reject malformed escape
845
+ // sequences instead of looking up a different literal key.
846
+ if (/~(?:[^01]|$)/.test(encodedPart)) {
847
+ return undefined;
173
848
  }
174
- resolved = value;
849
+ parts.push(encodedPart.replace(/~1/g, '/').replace(/~0/g, '~'));
175
850
  }
176
851
 
177
- return resolved;
852
+ return parts;
178
853
  }
179
854
 
180
- function isObject<T>(obj: T | Array<any>): obj is Extract<T, Record<string, any>> {
181
- return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
182
- }
855
+ function resolvePointerPart(resolved: unknown, part: string): unknown | undefined {
856
+ if (Array.isArray(resolved)) {
857
+ if (!/^(?:0|[1-9]\d*)$/.test(part)) {
858
+ return undefined;
859
+ }
183
860
 
184
- function hasMoreThanNKeys(obj: Record<string, any>, n: number): boolean {
185
- let i = 0;
186
- for (const _ in obj) {
187
- i++;
188
- if (i > n) {
189
- return true;
861
+ const index = Number(part);
862
+ if (!Object.prototype.hasOwnProperty.call(resolved, index)) {
863
+ return undefined;
190
864
  }
865
+ return resolved[index];
191
866
  }
192
- return false;
867
+
868
+ if (!isObject(resolved) || !Object.prototype.hasOwnProperty.call(resolved, part)) {
869
+ return undefined;
870
+ }
871
+ return resolved[part];
872
+ }
873
+
874
+ export function resolveLocalRef(root: JSONSchema, ref: string): JSONSchemaDefinition | undefined {
875
+ const parts = parseLocalRef(ref);
876
+ if (parts === undefined) {
877
+ return undefined;
878
+ }
879
+
880
+ // Literal payloads such as `default`, `enum`, and `const` can contain
881
+ // object-shaped values, but they are not schemas and are never traversed by
882
+ // strictification. Resolve only the schema-bearing locations visited by
883
+ // forEachJSONSchemaChild so every accepted target is normalized before we
884
+ // advertise the result as strict.
885
+ let resolved: unknown = root;
886
+ for (let index = 0; index < parts.length; ) {
887
+ if (!isObject(resolved)) {
888
+ return undefined;
889
+ }
890
+
891
+ const keyword = parts[index]!;
892
+ if (JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS.includes(keyword)) {
893
+ resolved = resolvePointerPart(resolved, keyword);
894
+ index += 1;
895
+ continue;
896
+ }
897
+
898
+ if (JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS.includes(keyword)) {
899
+ resolved = resolvePointerPart(resolved, keyword);
900
+ index += 1;
901
+ if (Array.isArray(resolved)) {
902
+ if (index >= parts.length) {
903
+ return undefined;
904
+ }
905
+ resolved = resolvePointerPart(resolved, parts[index]!);
906
+ index += 1;
907
+ }
908
+ continue;
909
+ }
910
+
911
+ if (JSON_SCHEMA_MAP_SCHEMA_KEYWORDS.includes(keyword)) {
912
+ const children = resolvePointerPart(resolved, keyword);
913
+ index += 1;
914
+ if (!isObject(children) || index >= parts.length) {
915
+ return undefined;
916
+ }
917
+
918
+ resolved = resolvePointerPart(children, parts[index]!);
919
+ if (keyword === 'dependencies' && !isSchemaDefinition(resolved)) {
920
+ return undefined;
921
+ }
922
+ index += 1;
923
+ continue;
924
+ }
925
+
926
+ return undefined;
927
+ }
928
+
929
+ return isSchemaDefinition(resolved) ? resolved : undefined;
930
+ }
931
+
932
+ function isObject<T>(obj: T | Array<any>): obj is Extract<T, Record<string, any>> {
933
+ return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
934
+ }
935
+
936
+ function isSchemaDefinition(value: unknown): value is JSONSchemaDefinition {
937
+ return typeof value === 'boolean' || isObject(value);
938
+ }
939
+
940
+ function isObjectOnlySchema(
941
+ schema: JSONSchemaDefinition,
942
+ root: JSONSchema,
943
+ seenRefs: Set<string> = new Set(),
944
+ ): boolean {
945
+ if (typeof schema === 'boolean' || !isObject(schema)) {
946
+ return false;
947
+ }
948
+
949
+ if (schema.$ref !== undefined) {
950
+ if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) {
951
+ return false;
952
+ }
953
+
954
+ const resolved = resolveLocalRef(root, schema.$ref);
955
+ if (resolved === undefined) {
956
+ return false;
957
+ }
958
+
959
+ return isObjectOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref]));
960
+ }
961
+
962
+ if (schema.allOf !== undefined) {
963
+ if (
964
+ !Array.isArray(schema.allOf) ||
965
+ schema.allOf.length !== 1 ||
966
+ !hasOnlyAnnotationSiblings(schema, 'allOf')
967
+ ) {
968
+ return false;
969
+ }
970
+
971
+ const branch = schema.allOf[0];
972
+ return branch !== undefined && branch !== true && branch !== false ?
973
+ isObjectOnlySchema(branch, root, seenRefs)
974
+ : false;
975
+ }
976
+
977
+ return (
978
+ schema.type === 'object' ||
979
+ (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'object')
980
+ );
981
+ }
982
+
983
+ function isArrayOnlySchema(
984
+ schema: JSONSchemaDefinition,
985
+ root: JSONSchema,
986
+ seenRefs: Set<string> = new Set(),
987
+ ): boolean {
988
+ if (typeof schema === 'boolean' || !isObject(schema)) {
989
+ return false;
990
+ }
991
+
992
+ if (schema.$ref !== undefined) {
993
+ if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) {
994
+ return false;
995
+ }
996
+
997
+ const resolved = resolveLocalRef(root, schema.$ref);
998
+ if (resolved === undefined) {
999
+ return false;
1000
+ }
1001
+
1002
+ return isArrayOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref]));
1003
+ }
1004
+
1005
+ if (schema.allOf !== undefined) {
1006
+ if (
1007
+ !Array.isArray(schema.allOf) ||
1008
+ schema.allOf.length !== 1 ||
1009
+ !hasOnlyAnnotationSiblings(schema, 'allOf')
1010
+ ) {
1011
+ return false;
1012
+ }
1013
+
1014
+ const branch = schema.allOf[0];
1015
+ return branch !== undefined && branch !== true && branch !== false ?
1016
+ isArrayOnlySchema(branch, root, seenRefs)
1017
+ : false;
1018
+ }
1019
+
1020
+ return (
1021
+ schema.type === 'array' ||
1022
+ (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'array')
1023
+ );
1024
+ }
1025
+
1026
+ export function hasOnlyRefAndAnnotations(schema: JSONSchema): boolean {
1027
+ return Object.keys(schema).every(
1028
+ // Definition maps do not add sibling validation constraints, and keeping
1029
+ // them in place preserves local pointers into a nested alias's scope.
1030
+ (keyword) =>
1031
+ keyword === '$ref' ||
1032
+ keyword === '$defs' ||
1033
+ keyword === 'definitions' ||
1034
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
1035
+ );
1036
+ }
1037
+
1038
+ function hasOnlyAnnotationSiblings(schema: JSONSchema, keyword: string): boolean {
1039
+ const schemaRecord = schema as Record<string, unknown>;
1040
+ return Object.keys(schema).every(
1041
+ // Definition maps do not add sibling validation constraints. Keep them
1042
+ // beside a flattened singleton allOf so refs into this nested scope stay
1043
+ // reachable at their original pointers.
1044
+ (schemaKeyword) =>
1045
+ schemaKeyword === keyword ||
1046
+ ((schemaKeyword === '$defs' || schemaKeyword === 'definitions') &&
1047
+ isObject(schemaRecord[schemaKeyword])) ||
1048
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(schemaKeyword),
1049
+ );
1050
+ }
1051
+
1052
+ function hasOnlyRootAllOfMetadataSiblings(schema: JSONSchema): boolean {
1053
+ return Object.keys(schema).every(
1054
+ (keyword) =>
1055
+ keyword === 'allOf' ||
1056
+ keyword === '$defs' ||
1057
+ keyword === 'definitions' ||
1058
+ JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
1059
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
1060
+ );
1061
+ }
1062
+
1063
+ function hasOnlyRootAnyOfMetadataSiblings(schema: JSONSchema): boolean {
1064
+ return Object.keys(schema).every(
1065
+ (keyword) =>
1066
+ keyword === 'anyOf' ||
1067
+ keyword === '$defs' ||
1068
+ keyword === 'definitions' ||
1069
+ (keyword === 'type' && schema.type === 'object') ||
1070
+ JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
1071
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
1072
+ );
1073
+ }
1074
+
1075
+ function hasObjectKeywords(schema: JSONSchema): boolean {
1076
+ return Object.keys(schema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword));
1077
+ }
1078
+
1079
+ function hasObjectShape(schema: JSONSchema): boolean {
1080
+ const typ = schema.type;
1081
+ return (
1082
+ typ === 'object' ||
1083
+ (Array.isArray(typ) && typ.includes('object')) ||
1084
+ (typ === undefined && hasObjectKeywords(schema))
1085
+ );
1086
+ }
1087
+
1088
+ function isRedundantUnionWrapperType(type: JSONSchema['type'], branchType: 'object' | 'array'): boolean {
1089
+ return (
1090
+ type === branchType ||
1091
+ (Array.isArray(type) && type.length === 2 && type.includes(branchType) && type.includes('null'))
1092
+ );
1093
+ }
1094
+
1095
+ function normalizeObjectUnionWrapper(jsonSchema: JSONSchema, path: string[], root: JSONSchema): void {
1096
+ if (jsonSchema.anyOf === undefined) {
1097
+ return;
1098
+ }
1099
+
1100
+ const hasEmptyProperties =
1101
+ isObject(jsonSchema.properties) && Object.keys(jsonSchema.properties).length === 0;
1102
+ const hasEmptyRequired = Array.isArray(jsonSchema.required) && jsonSchema.required.length === 0;
1103
+ if (hasEmptyProperties) {
1104
+ delete jsonSchema.properties;
1105
+ }
1106
+ if (hasEmptyRequired) {
1107
+ delete jsonSchema.required;
1108
+ }
1109
+
1110
+ if (!hasObjectShape(jsonSchema)) {
1111
+ return;
1112
+ }
1113
+
1114
+ const hasOwnObjectConstraints = Object.keys(jsonSchema).some((keyword) =>
1115
+ JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword),
1116
+ );
1117
+ if (
1118
+ isRedundantUnionWrapperType(jsonSchema.type, 'object') &&
1119
+ !hasOwnObjectConstraints &&
1120
+ Array.isArray(jsonSchema.anyOf) &&
1121
+ jsonSchema.anyOf.every((branch) => isObjectOnlySchema(branch, root))
1122
+ ) {
1123
+ // The union already excludes null and every non-object value, so both a
1124
+ // scalar object wrapper and a nullable object wrapper are redundant under
1125
+ // Draft 7's conjunctive keyword semantics.
1126
+ delete jsonSchema.type;
1127
+ return;
1128
+ }
1129
+
1130
+ throw new Error(
1131
+ 'Object anyOf schema at `' +
1132
+ (path.join('/') || '<root>') +
1133
+ '` cannot be represented in strict Structured Outputs without changing Draft 7 validation.',
1134
+ );
1135
+ }
1136
+
1137
+ function normalizeArrayUnionWrapper(jsonSchema: JSONSchema, root: JSONSchema): void {
1138
+ if (
1139
+ isRedundantUnionWrapperType(jsonSchema.type, 'array') &&
1140
+ jsonSchema.items === undefined &&
1141
+ Array.isArray(jsonSchema.anyOf) &&
1142
+ jsonSchema.anyOf.every((branch) => isArrayOnlySchema(branch, root))
1143
+ ) {
1144
+ // Every union branch already proves the value is an array and excludes
1145
+ // null. Keeping either redundant wrapper would require an outer items
1146
+ // schema that does not contribute any Draft 7 validation.
1147
+ delete jsonSchema.type;
1148
+ }
1149
+ }
1150
+
1151
+ function normalizeAnyOfFalseBranches(jsonSchema: JSONSchema): void {
1152
+ if (!Array.isArray(jsonSchema.anyOf)) {
1153
+ return;
1154
+ }
1155
+
1156
+ const realBranches = jsonSchema.anyOf.filter((branch) => branch !== false);
1157
+ if (realBranches.length > 0 && realBranches.length !== jsonSchema.anyOf.length) {
1158
+ jsonSchema.anyOf = realBranches;
1159
+ }
1160
+ }
1161
+
1162
+ export function assertNoNestedSchemaIds(schema: JSONSchema): void {
1163
+ const visit = (value: JSONSchemaDefinition, path: string[]): void => {
1164
+ if (typeof value === 'boolean' || !isObject(value)) {
1165
+ return;
1166
+ }
1167
+
1168
+ if (path.length > 0 && value.$id !== undefined) {
1169
+ throw new Error(
1170
+ 'Nested $id at ' +
1171
+ JSON.stringify(path.join('/')) +
1172
+ ' establishes a separate JSON Schema resource scope and cannot be represented in strict Structured Outputs.',
1173
+ );
1174
+ }
1175
+
1176
+ forEachJSONSchemaChild(value, path, (child, childPath) => {
1177
+ visit(child as JSONSchemaDefinition, childPath);
1178
+ });
1179
+ };
1180
+
1181
+ visit(schema, []);
1182
+ }
1183
+
1184
+ function refTargetsAllOfBranch(root: JSONSchema, ref: string): boolean {
1185
+ const parts = parseLocalRef(ref);
1186
+ if (parts === undefined) {
1187
+ return false;
1188
+ }
1189
+
1190
+ let resolved: unknown = root;
1191
+ for (const [index, part] of parts.entries()) {
1192
+ if (
1193
+ part === 'allOf' &&
1194
+ isObject(resolved) &&
1195
+ Array.isArray(resolved['allOf']) &&
1196
+ index < parts.length - 1
1197
+ ) {
1198
+ return true;
1199
+ }
1200
+
1201
+ resolved = resolvePointerPart(resolved, part);
1202
+ if (resolved === undefined) {
1203
+ return false;
1204
+ }
1205
+ }
1206
+
1207
+ return false;
1208
+ }
1209
+
1210
+ function escapeJSONPointerToken(token: string): string {
1211
+ return token.replace(/~/g, '~0').replace(/\//g, '~1');
1212
+ }
1213
+
1214
+ function encodeJSONPointerTokenForURIFragment(token: string): string {
1215
+ // `$` is a valid URI fragment sub-delimiter and keeping it readable retains
1216
+ // the conventional `#/$defs/...` spelling. Everything else that could
1217
+ // invalidate or retokenize the fragment (notably `%` and spaces) is encoded
1218
+ // after JSON Pointer escaping.
1219
+ return encodeURIComponent(escapeJSONPointerToken(token)).replace(/%24/g, '$');
1220
+ }
1221
+
1222
+ /**
1223
+ * Standard Schema normalization moves representable oneOf branches to anyOf.
1224
+ * Rewrite only pointers that traverse an actual oneOf schema array while the
1225
+ * original tree is still intact, preserving escaped tokens for every other
1226
+ * path segment.
1227
+ */
1228
+ export function rewriteLocalRefsIntoMovedOneOfBranches(root: JSONSchema): void {
1229
+ const rewriteRef = (ref: string): string => {
1230
+ const parts = parseLocalRef(ref);
1231
+ if (parts === undefined || parts.length === 0) {
1232
+ return ref;
1233
+ }
1234
+
1235
+ let resolved: unknown = root;
1236
+ let changed = false;
1237
+ for (const [index, part] of parts.entries()) {
1238
+ if (
1239
+ part === 'oneOf' &&
1240
+ index < parts.length - 1 &&
1241
+ isObject(resolved) &&
1242
+ Array.isArray(resolved['oneOf'])
1243
+ ) {
1244
+ parts[index] = 'anyOf';
1245
+ changed = true;
1246
+ }
1247
+
1248
+ resolved = resolvePointerPart(resolved, part);
1249
+ if (resolved === undefined) {
1250
+ return ref;
1251
+ }
1252
+ }
1253
+
1254
+ return changed ? '#/' + parts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref;
1255
+ };
1256
+
1257
+ const rewriteRefs = (value: JSONSchemaDefinition): void => {
1258
+ if (typeof value === 'boolean' || !isObject(value)) {
1259
+ return;
1260
+ }
1261
+
1262
+ if (typeof value.$ref === 'string') {
1263
+ value.$ref = rewriteRef(value.$ref);
1264
+ }
1265
+
1266
+ forEachJSONSchemaChild(value, [], (child) => {
1267
+ rewriteRefs(child as JSONSchemaDefinition);
1268
+ });
1269
+ };
1270
+
1271
+ rewriteRefs(root);
1272
+ }
1273
+
1274
+ /**
1275
+ * Strictification removes false anyOf alternatives. Rewrite pointers into
1276
+ * surviving alternatives before that filtering happens so each local ref
1277
+ * still names the same schema after earlier indices disappear.
1278
+ */
1279
+ function rewriteLocalRefsIntoFilteredAnyOfBranches(root: JSONSchema): void {
1280
+ const rewriteRef = (ref: string): string => {
1281
+ const originalParts = parseLocalRef(ref);
1282
+ if (originalParts === undefined || originalParts.length === 0) {
1283
+ return ref;
1284
+ }
1285
+
1286
+ const rewrittenParts = [...originalParts];
1287
+ let resolved: unknown = root;
1288
+ let changed = false;
1289
+
1290
+ for (const [index, part] of originalParts.entries()) {
1291
+ const resolvedRecord =
1292
+ typeof resolved === 'object' && resolved !== null && !Array.isArray(resolved) ?
1293
+ (resolved as Record<string, unknown>)
1294
+ : undefined;
1295
+ if (
1296
+ part === 'anyOf' &&
1297
+ index < originalParts.length - 1 &&
1298
+ resolvedRecord !== undefined &&
1299
+ Array.isArray(resolvedRecord['anyOf'])
1300
+ ) {
1301
+ const branches = resolvedRecord['anyOf'];
1302
+ const branchIndexPart = originalParts[index + 1]!;
1303
+ if (!/^(?:0|[1-9]\d*)$/.test(branchIndexPart)) {
1304
+ return ref;
1305
+ }
1306
+
1307
+ const branchIndex = Number(branchIndexPart);
1308
+ if (!Object.prototype.hasOwnProperty.call(branches, branchIndex)) {
1309
+ return ref;
1310
+ }
1311
+
1312
+ const realBranches = branches.filter((branch) => branch !== false);
1313
+ if (realBranches.length > 0 && realBranches.length !== branches.length) {
1314
+ // A ref to a removed false schema is already rejected by the first
1315
+ // ref-validation pass. Leave it untouched so it remains fail closed.
1316
+ if (branches[branchIndex] === false) {
1317
+ return ref;
1318
+ }
1319
+
1320
+ const rewrittenIndex = branches.slice(0, branchIndex).filter((branch) => branch !== false).length;
1321
+ if (rewrittenIndex !== branchIndex) {
1322
+ rewrittenParts[index + 1] = String(rewrittenIndex);
1323
+ changed = true;
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ // Resolve through the original pointer, not the rewritten one, so a
1329
+ // nested anyOf can also be remapped after its parent index shifts.
1330
+ resolved = resolvePointerPart(resolved, part);
1331
+ if (resolved === undefined) {
1332
+ return ref;
1333
+ }
1334
+ }
1335
+
1336
+ return changed ? '#/' + rewrittenParts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref;
1337
+ };
1338
+
1339
+ const rewriteRefs = (value: JSONSchemaDefinition): void => {
1340
+ if (typeof value === 'boolean' || !isObject(value)) {
1341
+ return;
1342
+ }
1343
+
1344
+ if (typeof value.$ref === 'string') {
1345
+ value.$ref = rewriteRef(value.$ref);
1346
+ }
1347
+
1348
+ forEachJSONSchemaChild(value, [], (child) => {
1349
+ rewriteRefs(child as JSONSchemaDefinition);
1350
+ });
1351
+ };
1352
+
1353
+ rewriteRefs(root);
1354
+ }
1355
+
1356
+ /**
1357
+ * Strictification removes every representable allOf. Preserve any schema
1358
+ * referenced through an allOf branch under a stable root definition first so
1359
+ * structural flattening cannot leave a dangling local pointer behind.
1360
+ */
1361
+ function preserveAllOfRefTargets(root: JSONSchema, rootOnly = false): void {
1362
+ const refsToPreserve = new Set<string>();
1363
+ const collectRefs = (value: JSONSchemaDefinition): void => {
1364
+ if (typeof value === 'boolean' || !isObject(value)) {
1365
+ return;
1366
+ }
1367
+
1368
+ if (typeof value.$ref === 'string' && refTargetsAllOfBranch(root, value.$ref)) {
1369
+ const pointerParts = parseLocalRef(value.$ref);
1370
+ if (!rootOnly || pointerParts?.[0] === 'allOf') {
1371
+ refsToPreserve.add(value.$ref);
1372
+ }
1373
+ }
1374
+
1375
+ forEachJSONSchemaChild(value, [], (child) => {
1376
+ collectRefs(child as JSONSchemaDefinition);
1377
+ });
1378
+ };
1379
+ collectRefs(root);
1380
+
1381
+ if (refsToPreserve.size === 0) {
1382
+ return;
1383
+ }
1384
+
1385
+ if (root.$defs !== undefined && !isObject(root.$defs)) {
1386
+ throw new Error('Root schema has invalid $defs and cannot preserve local allOf references.');
1387
+ }
1388
+ const definitions = (root.$defs ??= {});
1389
+ const rewrittenRefs = new Map<string, string>();
1390
+ let aliasIndex = 0;
1391
+
1392
+ for (const ref of refsToPreserve) {
1393
+ const target = resolveLocalRef(root, ref);
1394
+ if (!isSchemaDefinition(target)) {
1395
+ if (rootOnly) {
1396
+ continue;
1397
+ }
1398
+ throw new Error('Local $ref cannot be preserved before allOf flattening: ' + JSON.stringify(ref));
1399
+ }
1400
+
1401
+ let alias = '__openai_strict_allOf_ref_' + aliasIndex++;
1402
+ while (Object.prototype.hasOwnProperty.call(definitions, alias)) {
1403
+ alias = '__openai_strict_allOf_ref_' + aliasIndex++;
1404
+ }
1405
+ definitions[alias] = structuredClone(target);
1406
+ rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias));
1407
+ }
1408
+
1409
+ const rewriteRefs = (value: JSONSchemaDefinition): void => {
1410
+ if (typeof value === 'boolean' || !isObject(value)) {
1411
+ return;
1412
+ }
1413
+
1414
+ if (typeof value.$ref === 'string') {
1415
+ value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref;
1416
+ }
1417
+
1418
+ forEachJSONSchemaChild(value, [], (child) => {
1419
+ rewriteRefs(child as JSONSchemaDefinition);
1420
+ });
1421
+ };
1422
+ rewriteRefs(root);
1423
+ }
1424
+
1425
+ /**
1426
+ * Closed allOf merges can discard optional property declarations. Preserve
1427
+ * only local refs into declarations that are about to disappear, then rewrite
1428
+ * those refs to stable root definitions before the merge removes their paths.
1429
+ */
1430
+ function preserveDiscardedAllOfPropertyRefTargets(root: JSONSchema, discardedPaths: string[][]): void {
1431
+ if (discardedPaths.length === 0) {
1432
+ return;
1433
+ }
1434
+
1435
+ const refsToPreserve = new Set<string>();
1436
+ const collectRefs = (value: JSONSchemaDefinition): void => {
1437
+ if (typeof value === 'boolean' || !isObject(value)) {
1438
+ return;
1439
+ }
1440
+
1441
+ if (typeof value.$ref === 'string') {
1442
+ const parts = parseLocalRef(value.$ref);
1443
+ if (
1444
+ parts !== undefined &&
1445
+ discardedPaths.some(
1446
+ (discardedPath) =>
1447
+ parts.length >= discardedPath.length &&
1448
+ discardedPath.every((part, index) => parts[index] === part),
1449
+ )
1450
+ ) {
1451
+ refsToPreserve.add(value.$ref);
1452
+ }
1453
+ }
1454
+
1455
+ forEachJSONSchemaChild(value, [], (child) => {
1456
+ collectRefs(child as JSONSchemaDefinition);
1457
+ });
1458
+ };
1459
+ collectRefs(root);
1460
+
1461
+ if (refsToPreserve.size === 0) {
1462
+ return;
1463
+ }
1464
+
1465
+ if (root.$defs !== undefined && !isObject(root.$defs)) {
1466
+ throw new Error('Root schema has invalid $defs and cannot preserve discarded allOf properties.');
1467
+ }
1468
+ const definitions = (root.$defs ??= {});
1469
+ const rewrittenRefs = new Map<string, string>();
1470
+ let aliasIndex = 0;
1471
+
1472
+ for (const ref of refsToPreserve) {
1473
+ const target = resolveLocalRef(root, ref);
1474
+ if (!isSchemaDefinition(target)) {
1475
+ throw new Error('Local $ref cannot be preserved before allOf property removal: ' + JSON.stringify(ref));
1476
+ }
1477
+
1478
+ let alias = '__openai_strict_allOf_property_ref_' + aliasIndex++;
1479
+ while (Object.prototype.hasOwnProperty.call(definitions, alias)) {
1480
+ alias = '__openai_strict_allOf_property_ref_' + aliasIndex++;
1481
+ }
1482
+ definitions[alias] = structuredClone(target);
1483
+ rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias));
1484
+ }
1485
+
1486
+ const rewriteRefs = (value: JSONSchemaDefinition): void => {
1487
+ if (typeof value === 'boolean' || !isObject(value)) {
1488
+ return;
1489
+ }
1490
+
1491
+ if (typeof value.$ref === 'string') {
1492
+ value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref;
1493
+ }
1494
+
1495
+ forEachJSONSchemaChild(value, [], (child) => {
1496
+ rewriteRefs(child as JSONSchemaDefinition);
1497
+ });
1498
+ };
1499
+ rewriteRefs(root);
1500
+ }
1501
+
1502
+ function validateRefSchemas(schema: JSONSchemaDefinition, path: string[], root: JSONSchema): void {
1503
+ if (typeof schema === 'boolean' || !isObject(schema)) {
1504
+ return;
1505
+ }
1506
+
1507
+ const ref = schema.$ref;
1508
+ if (ref !== undefined) {
1509
+ if (typeof ref !== 'string') {
1510
+ throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
1511
+ }
1512
+ if (!ref.startsWith('#')) {
1513
+ throw new Error(
1514
+ `External $ref at \`${
1515
+ path.join('/') || '<root>'
1516
+ }\` is not supported in strict Structured Outputs: ${JSON.stringify(ref)}`,
1517
+ );
1518
+ }
1519
+ const resolved = resolveLocalRef(root, ref);
1520
+ if (resolved === undefined || !isSchemaDefinition(resolved)) {
1521
+ throw new Error(
1522
+ `Local $ref at \`${
1523
+ path.join('/') || '<root>'
1524
+ }\` does not resolve to an object or boolean schema: ${JSON.stringify(ref)}`,
1525
+ );
1526
+ }
1527
+ if (typeof resolved === 'boolean') {
1528
+ throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`);
1529
+ }
1530
+ if (!hasOnlyRefAndAnnotations(schema)) {
1531
+ throw new Error(
1532
+ `Schema $ref at \`${
1533
+ path.join('/') || '<root>'
1534
+ }\` has non-annotation siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.`,
1535
+ );
1536
+ }
1537
+ }
1538
+
1539
+ forEachJSONSchemaChild(schema, path, (child, childPath) => {
1540
+ validateRefSchemas(child as JSONSchemaDefinition, childPath, root);
1541
+ });
1542
+ }
1543
+
1544
+ type ResolvedObjectAllOfBranch = {
1545
+ schema: JSONSchema;
1546
+ refChain: JSONSchema[];
1547
+ };
1548
+
1549
+ /**
1550
+ * Resolve only local aliases whose siblings carry no validation semantics.
1551
+ * Keeping this separate from general ref validation lets allOf merging inspect
1552
+ * the effective object shape without broadening which refs or sibling
1553
+ * constraints are accepted.
1554
+ */
1555
+ function resolveObjectAllOfBranch(
1556
+ schema: JSONSchema,
1557
+ root: JSONSchema,
1558
+ normalizing: Set<JSONSchema>,
1559
+ ): ResolvedObjectAllOfBranch | undefined {
1560
+ const refChain = [schema];
1561
+ const seenRefs = new Set<string>();
1562
+ let resolved = schema;
1563
+ let resolvedPath: string[] = [];
1564
+
1565
+ while (true) {
1566
+ while (resolved.$ref !== undefined) {
1567
+ const ref = resolved.$ref;
1568
+ if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(resolved) || seenRefs.has(ref)) {
1569
+ return undefined;
1570
+ }
1571
+ seenRefs.add(ref);
1572
+
1573
+ const target = resolveLocalRef(root, ref);
1574
+ if (typeof target === 'boolean' || !isObject(target)) {
1575
+ return undefined;
1576
+ }
1577
+ const targetPath = parseLocalRef(ref);
1578
+ if (targetPath === undefined) {
1579
+ return undefined;
1580
+ }
1581
+
1582
+ resolved = target;
1583
+ resolvedPath = targetPath;
1584
+ refChain.push(resolved);
1585
+ }
1586
+
1587
+ // A ref can point forward to a target whose own mergeable allOf has not
1588
+ // been visited yet. Normalize that target before its consumer classifies
1589
+ // the resolved shape, making property order irrelevant. An active target
1590
+ // is a ref/allOf cycle; leave it wrapped so the caller still fails closed.
1591
+ if (resolved.allOf !== undefined && !normalizing.has(resolved)) {
1592
+ const previousAllOf = resolved.allOf;
1593
+ normalizeObjectAllOfBranches(resolved, resolvedPath, root, normalizing);
1594
+ // Singleton flattening can expose another local ref. Follow it through
1595
+ // the same guarded loop before returning the effective branch.
1596
+ if (resolved.$ref !== undefined || resolved.allOf !== previousAllOf) {
1597
+ continue;
1598
+ }
1599
+ }
1600
+
1601
+ return { schema: resolved, refChain };
1602
+ }
1603
+ }
1604
+
1605
+ function normalizeObjectAllOfBranches(
1606
+ schema: JSONSchemaDefinition,
1607
+ path: string[],
1608
+ root: JSONSchema,
1609
+ normalizing: Set<JSONSchema> = new Set(),
1610
+ ): void {
1611
+ if (typeof schema === 'boolean' || !isObject(schema)) {
1612
+ return;
1613
+ }
1614
+ if (normalizing.has(schema)) {
1615
+ return;
1616
+ }
1617
+
1618
+ normalizing.add(schema);
1619
+ try {
1620
+ while (true) {
1621
+ forEachJSONSchemaChild(schema, path, (child, childPath) => {
1622
+ normalizeObjectAllOfBranches(child as JSONSchemaDefinition, childPath, root, normalizing);
1623
+ });
1624
+
1625
+ // Intersections are associative, so normalize nested object
1626
+ // intersections before classifying their parents. Otherwise an inner
1627
+ // allOf wrapper has no directly visible object shape and makes an
1628
+ // exactly mergeable outer allOf fail closed.
1629
+ if (!mergeObjectAllOf(schema, path, root, normalizing)) {
1630
+ return;
1631
+ }
1632
+ }
1633
+ } finally {
1634
+ normalizing.delete(schema);
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * Standard Schema needs to prove oneOf branches exclusive before it rewrites
1640
+ * them to anyOf. Inspect a cloned branch through the same conservative object
1641
+ * allOf merger without mutating the caller's schema or broadening failures.
1642
+ */
1643
+ export function normalizeObjectAllOfForExclusivity(
1644
+ schema: JSONSchema,
1645
+ root: JSONSchema,
1646
+ ): JSONSchema | undefined {
1647
+ if (schema.allOf === undefined) {
1648
+ return undefined;
1649
+ }
1650
+
1651
+ const normalized = structuredClone(schema);
1652
+ try {
1653
+ // Removing neutral branches or flattening a singleton can expose another
1654
+ // mergeable allOf wrapper. Keep applying the recursive merger and safe
1655
+ // singleton flattening until the proof sees the branch's final shape.
1656
+ while (normalized.allOf !== undefined) {
1657
+ normalizeObjectAllOfBranches(normalized, [], root);
1658
+ if (normalized.allOf === undefined) {
1659
+ break;
1660
+ }
1661
+
1662
+ const allOf = normalized.allOf;
1663
+ if (!Array.isArray(allOf) || allOf.length !== 1 || !hasOnlyAnnotationSiblings(normalized, 'allOf')) {
1664
+ return undefined;
1665
+ }
1666
+
1667
+ const branch = allOf[0];
1668
+ if (typeof branch === 'boolean' || !isObject(branch)) {
1669
+ return undefined;
1670
+ }
1671
+
1672
+ const siblings = { ...normalized };
1673
+ delete siblings.allOf;
1674
+ const flattened = structuredClone(branch);
1675
+ for (const keyword of Object.keys(normalized)) {
1676
+ delete (normalized as Record<string, unknown>)[keyword];
1677
+ }
1678
+ Object.assign(normalized, flattened, siblings);
1679
+ }
1680
+
1681
+ return normalized;
1682
+ } catch {
1683
+ return undefined;
1684
+ }
1685
+ }
1686
+
1687
+ function mergeObjectAllOf(
1688
+ jsonSchema: JSONSchema,
1689
+ path: string[],
1690
+ root: JSONSchema,
1691
+ normalizing: Set<JSONSchema> = new Set(),
1692
+ ): boolean {
1693
+ const allOf = jsonSchema.allOf;
1694
+ if (!Array.isArray(allOf) || allOf.length === 0) {
1695
+ return false;
1696
+ }
1697
+
1698
+ // Intersections are idempotent. Collapse structurally identical branches
1699
+ // before object-shape classification so duplicate scalar and array schemas
1700
+ // can reach the existing safe singleton-flattening path.
1701
+ const uniqueBranches = allOf.filter(
1702
+ (branch, index) => !allOf.slice(0, index).some((candidate) => schemasEqual(candidate, branch)),
1703
+ );
1704
+ if (uniqueBranches.length !== allOf.length) {
1705
+ jsonSchema.allOf = uniqueBranches;
1706
+ return true;
1707
+ }
1708
+
1709
+ // `true` is the identity element for Draft 7 intersections. Remove it
1710
+ // before deciding whether the remaining branches are object-mergeable so a
1711
+ // neutral branch cannot make an otherwise exact merge fail closed.
1712
+ const nonNeutralBranches = allOf.filter((entry) => entry !== true);
1713
+ if (nonNeutralBranches.length !== allOf.length) {
1714
+ if (nonNeutralBranches.length === 0) {
1715
+ delete jsonSchema.allOf;
1716
+ } else {
1717
+ jsonSchema.allOf = nonNeutralBranches;
1718
+ }
1719
+ return true;
1720
+ }
1721
+
1722
+ const parentHasObjectShape = hasObjectShapeWithoutAllOf(jsonSchema);
1723
+ const resolvedEntries = allOf.map((entry) =>
1724
+ isObject(entry) ? resolveObjectAllOfBranch(entry, root, normalizing) : undefined,
1725
+ );
1726
+ const objectBranches = resolvedEntries
1727
+ .map((entry) => entry?.schema)
1728
+ .filter((entry): entry is JSONSchema => entry !== undefined && hasObjectShapeWithoutAllOf(entry));
1729
+ if (!parentHasObjectShape && objectBranches.length === 0) {
1730
+ return false;
1731
+ }
1732
+ // A lone object branch with no object-valued parent is handled by the
1733
+ // existing safe single-allOf flattening path below.
1734
+ if (!parentHasObjectShape && allOf.length === 1) {
1735
+ return false;
1736
+ }
1737
+
1738
+ const fail = (): never => {
1739
+ throw new Error(
1740
+ `Object allOf at \`${
1741
+ path.join('/') || '<root>'
1742
+ }\` cannot be merged without changing Draft 7 validation.`,
1743
+ );
1744
+ };
1745
+
1746
+ if (
1747
+ !parentHasObjectShape &&
1748
+ ['additionalProperties', 'properties', 'required', 'type'].some((keyword) => keyword in jsonSchema)
1749
+ ) {
1750
+ fail();
1751
+ }
1752
+
1753
+ for (const keyword of Object.keys(jsonSchema)) {
1754
+ if (
1755
+ keyword !== 'allOf' &&
1756
+ keyword !== '$defs' &&
1757
+ keyword !== 'definitions' &&
1758
+ !(path.length === 0 && JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword)) &&
1759
+ !MERGEABLE_OBJECT_ALL_OF_KEYWORDS.has(keyword)
1760
+ ) {
1761
+ fail();
1762
+ }
1763
+ }
1764
+
1765
+ const branches: Array<{ schema: JSONSchema; sourcePath: string[] | undefined }> = [];
1766
+ if (parentHasObjectShape) {
1767
+ branches.push({ schema: jsonSchema, sourcePath: path });
1768
+ }
1769
+ for (const [index, entry] of allOf.entries()) {
1770
+ if (!isObject(entry)) {
1771
+ fail();
1772
+ }
1773
+ const resolvedEntry = resolvedEntries[index];
1774
+ if (resolvedEntry === undefined) {
1775
+ return fail();
1776
+ }
1777
+ const branch = resolvedEntry.schema;
1778
+ // Definition maps do not constrain instances. Any refs into an allOf
1779
+ // branch were preserved under stable root definitions before this merge,
1780
+ // so a valid definitions-only branch can now be discarded like an
1781
+ // annotation-only branch.
1782
+ if (hasObjectShapeWithoutAllOf(branch)) {
1783
+ branches.push({
1784
+ schema: branch,
1785
+ sourcePath: branch === entry ? [...path, 'allOf', String(index)] : undefined,
1786
+ });
1787
+ } else if (!hasOnlyNeutralAllOfBranchKeywords(branch)) {
1788
+ fail();
1789
+ }
1790
+ }
1791
+
1792
+ const merged: JSONSchema = {};
1793
+ for (const keyword of ['$defs', 'definitions'] as const) {
1794
+ if (jsonSchema[keyword] !== undefined) {
1795
+ merged[keyword] = jsonSchema[keyword];
1796
+ }
1797
+ }
1798
+ if (path.length === 0) {
1799
+ for (const keyword of JSON_SCHEMA_ROOT_METADATA_KEYWORDS) {
1800
+ if (keyword in jsonSchema) {
1801
+ (merged as Record<string, unknown>)[keyword] = (jsonSchema as Record<string, unknown>)[keyword];
1802
+ }
1803
+ }
1804
+ }
1805
+
1806
+ const mergedProperties = Object.create(null) as Record<string, JSONSchemaDefinition>;
1807
+ const mergedRequired = new Set<string>();
1808
+ const closedPropertySets: Set<string>[] = [];
1809
+ const propertyEntries: Array<{
1810
+ key: string;
1811
+ propertySchema: JSONSchemaDefinition;
1812
+ sourcePath: string[] | undefined;
1813
+ }> = [];
1814
+ let sawProperties = false;
1815
+ let sawRequired = false;
1816
+ let hasExplicitObjectType = false;
1817
+ let hasExplicitNullableObjectType = false;
1818
+
1819
+ const mergeAnnotations = (schema: JSONSchema) => {
1820
+ for (const keyword of JSON_SCHEMA_ANNOTATION_KEYWORDS) {
1821
+ if (!(keyword in schema)) continue;
1822
+ // Annotation keywords do not affect Draft 7 validation. Preserve the
1823
+ // first value (the outer schema, then earlier branches) instead of
1824
+ // rejecting an otherwise exactly mergeable intersection.
1825
+ if (!(keyword in merged)) {
1826
+ (merged as any)[keyword] = (schema as any)[keyword];
1827
+ }
1828
+ }
1829
+ };
1830
+
1831
+ mergeAnnotations(jsonSchema);
1832
+ for (const resolvedEntry of resolvedEntries) {
1833
+ if (resolvedEntry === undefined) continue;
1834
+ for (const entry of resolvedEntry.refChain) {
1835
+ mergeAnnotations(entry);
1836
+ }
1837
+ }
1838
+
1839
+ for (const { schema: branch, sourcePath } of branches) {
1840
+ for (const keyword of Object.keys(branch)) {
1841
+ if (keyword === 'allOf' && branch === jsonSchema) continue;
1842
+ if (
1843
+ (keyword === '$defs' || keyword === 'definitions') &&
1844
+ isObject((branch as Record<string, unknown>)[keyword])
1845
+ ) {
1846
+ continue;
1847
+ }
1848
+ if (branch === jsonSchema && path.length === 0 && JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword)) {
1849
+ continue;
1850
+ }
1851
+ if (!MERGEABLE_OBJECT_ALL_OF_KEYWORDS.has(keyword)) {
1852
+ fail();
1853
+ }
1854
+ }
1855
+
1856
+ if (branch.type !== undefined) {
1857
+ if (!isMergeableObjectType(branch.type)) {
1858
+ fail();
1859
+ }
1860
+ if (branch.type === 'object') {
1861
+ hasExplicitObjectType = true;
1862
+ } else {
1863
+ hasExplicitNullableObjectType = true;
1864
+ }
1865
+ }
1866
+
1867
+ if (branch.properties !== undefined) {
1868
+ if (!isObject(branch.properties)) {
1869
+ fail();
1870
+ }
1871
+ sawProperties = true;
1872
+ for (const [key, propertySchema] of Object.entries(branch.properties)) {
1873
+ propertyEntries.push({
1874
+ key,
1875
+ propertySchema,
1876
+ sourcePath: sourcePath === undefined ? undefined : [...sourcePath, 'properties', key],
1877
+ });
1878
+ }
1879
+ }
1880
+
1881
+ if (branch.required !== undefined) {
1882
+ if (!Array.isArray(branch.required) || branch.required.some((key) => typeof key !== 'string')) {
1883
+ fail();
1884
+ }
1885
+ sawRequired = true;
1886
+ for (const key of branch.required) mergedRequired.add(key);
1887
+ }
1888
+
1889
+ if ('additionalProperties' in branch) {
1890
+ if (branch.additionalProperties !== false) {
1891
+ fail();
1892
+ }
1893
+ closedPropertySets.push(new Set(Object.keys(branch.properties ?? {})));
1894
+ }
1895
+ }
1896
+
1897
+ // A closed branch forbids every property it does not declare. Intersect all
1898
+ // closed property sets before merging schemas so optional declarations
1899
+ // excluded by another closed branch are discarded, while required excluded
1900
+ // properties remain unrepresentable and fail closed.
1901
+ const allowedClosedProperties =
1902
+ closedPropertySets.length === 0 ?
1903
+ undefined
1904
+ : closedPropertySets
1905
+ .slice(1)
1906
+ .reduce(
1907
+ (allowed, keys) => new Set([...allowed].filter((key) => keys.has(key))),
1908
+ new Set(closedPropertySets[0]),
1909
+ );
1910
+ const excludesRequiredProperty =
1911
+ allowedClosedProperties !== undefined &&
1912
+ [...mergedRequired].some((key) => !allowedClosedProperties.has(key));
1913
+ const collapsesToNull = excludesRequiredProperty && !hasExplicitObjectType && hasExplicitNullableObjectType;
1914
+ const discardedPropertyPaths = propertyEntries
1915
+ .filter(
1916
+ ({ key, sourcePath }) =>
1917
+ sourcePath !== undefined &&
1918
+ (collapsesToNull || (allowedClosedProperties !== undefined && !allowedClosedProperties.has(key))),
1919
+ )
1920
+ .map(({ sourcePath }) => sourcePath!);
1921
+ preserveDiscardedAllOfPropertyRefTargets(root, discardedPropertyPaths);
1922
+ if (jsonSchema === root && root.$defs !== undefined) {
1923
+ merged.$defs = root.$defs;
1924
+ }
1925
+
1926
+ if (excludesRequiredProperty) {
1927
+ // Object keywords do not constrain null. If every explicit object type
1928
+ // also admits null, the object portion is contradictory but null remains
1929
+ // an exact representation of the intersection.
1930
+ if (collapsesToNull) {
1931
+ merged.type = 'null';
1932
+ for (const keyword of Object.keys(jsonSchema)) {
1933
+ delete (jsonSchema as any)[keyword];
1934
+ }
1935
+ Object.assign(jsonSchema, merged);
1936
+ return true;
1937
+ }
1938
+ fail();
1939
+ }
1940
+
1941
+ for (const { key, propertySchema } of propertyEntries) {
1942
+ if (allowedClosedProperties !== undefined && !allowedClosedProperties.has(key)) {
1943
+ continue;
1944
+ }
1945
+ if (
1946
+ Object.prototype.hasOwnProperty.call(mergedProperties, key) &&
1947
+ !schemasEqual(mergedProperties[key], propertySchema)
1948
+ ) {
1949
+ fail();
1950
+ }
1951
+ mergedProperties[key] = propertySchema;
1952
+ }
1953
+
1954
+ if (hasExplicitObjectType || hasExplicitNullableObjectType) {
1955
+ merged.type = hasExplicitObjectType ? 'object' : ['object', 'null'];
1956
+ }
1957
+ if (sawProperties) merged.properties = Object.fromEntries(Object.entries(mergedProperties));
1958
+ if (sawRequired) merged.required = [...mergedRequired];
1959
+ if (closedPropertySets.length > 0) merged.additionalProperties = false;
1960
+
1961
+ for (const keyword of Object.keys(jsonSchema)) {
1962
+ delete (jsonSchema as any)[keyword];
1963
+ }
1964
+ Object.assign(jsonSchema, merged);
1965
+ return true;
1966
+ }
1967
+
1968
+ function hasObjectShapeWithoutAllOf(schema: JSONSchema): boolean {
1969
+ if (schema.type !== undefined) {
1970
+ return isMergeableObjectType(schema.type);
1971
+ }
1972
+ return Object.keys(schema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword));
1973
+ }
1974
+
1975
+ function hasOnlyNeutralAllOfBranchKeywords(schema: JSONSchema): boolean {
1976
+ const schemaRecord = schema as Record<string, unknown>;
1977
+ return Object.keys(schema).every(
1978
+ (keyword) =>
1979
+ JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword) ||
1980
+ ((keyword === '$defs' || keyword === 'definitions') && isObject(schemaRecord[keyword])),
1981
+ );
1982
+ }
1983
+
1984
+ function isMergeableObjectType(type: JSONSchema['type']): boolean {
1985
+ return (
1986
+ type === 'object' ||
1987
+ (Array.isArray(type) && type.length === 2 && type.includes('object') && type.includes('null'))
1988
+ );
1989
+ }
1990
+
1991
+ function schemasEqual(left: unknown, right: unknown): boolean {
1992
+ if (left === right) {
1993
+ return true;
1994
+ }
1995
+
1996
+ if (Array.isArray(left) || Array.isArray(right)) {
1997
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
1998
+ return false;
1999
+ }
2000
+
2001
+ return left.every((value, index) => schemasEqual(value, right[index]));
2002
+ }
2003
+
2004
+ if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null) {
2005
+ return false;
2006
+ }
2007
+
2008
+ const leftRecord = left as Record<string, unknown>;
2009
+ const rightRecord = right as Record<string, unknown>;
2010
+ const leftKeys = Object.keys(leftRecord).sort();
2011
+ const rightKeys = Object.keys(rightRecord).sort();
2012
+
2013
+ if (leftKeys.length !== rightKeys.length) {
2014
+ return false;
2015
+ }
2016
+
2017
+ return leftKeys.every(
2018
+ (key, index) => key === rightKeys[index] && schemasEqual(leftRecord[key], rightRecord[key]),
2019
+ );
193
2020
  }