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