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