UNPKG

openai

Version:

The official TypeScript library for the OpenAI API

1,194 lines 66.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.forEachJSONSchemaChild = forEachJSONSchemaChild; exports.toStrictJsonSchema = toStrictJsonSchema; exports.resolveLocalRef = resolveLocalRef; exports.hasOnlyRefAndAnnotations = hasOnlyRefAndAnnotations; exports.assertNoNestedSchemaIds = assertNoNestedSchemaIds; exports.rewriteLocalRefsIntoMovedOneOfBranches = rewriteLocalRefsIntoMovedOneOfBranches; exports.normalizeObjectAllOfForExclusivity = normalizeObjectAllOfForExclusivity; const JSON_SCHEMA_ANNOTATION_KEYWORDS = new Set([ '$comment', 'default', 'description', 'examples', 'readOnly', 'title', 'writeOnly', ]); const JSON_SCHEMA_ROOT_METADATA_KEYWORDS = new Set(['$id', '$schema']); const JSON_SCHEMA_OBJECT_KEYWORDS = new Set([ 'additionalProperties', 'dependencies', 'maxProperties', 'minProperties', 'patternProperties', 'properties', 'propertyNames', 'required', ]); const JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS = [ 'additionalItems', 'additionalProperties', 'contains', 'contentSchema', 'else', 'if', 'not', 'propertyNames', 'then', 'unevaluatedItems', 'unevaluatedProperties', ]; const JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS = ['allOf', 'anyOf', 'items', 'oneOf', 'prefixItems']; const JSON_SCHEMA_MAP_SCHEMA_KEYWORDS = [ '$defs', 'definitions', 'dependentSchemas', 'dependencies', 'patternProperties', 'properties', ]; const JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([ '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveAnchor', '$recursiveRef', 'allOf', 'contains', 'contentEncoding', 'contentMediaType', 'contentSchema', 'dependentRequired', 'dependentSchemas', 'dependencies', 'else', 'if', 'maxContains', 'maxProperties', 'minContains', 'minProperties', 'not', 'patternProperties', 'prefixItems', 'propertyNames', 'then', 'unevaluatedItems', 'unevaluatedProperties', 'uniqueItems', ]); const MERGEABLE_OBJECT_ALL_OF_KEYWORDS = new Set([ ...JSON_SCHEMA_ANNOTATION_KEYWORDS, 'additionalProperties', 'properties', 'required', 'type', ]); /** * Visits only values carried by JSON Schema keywords that contain schemas. * Literal payloads such as enum, const, and default deliberately do not * participate. */ function forEachJSONSchemaChild(schema, path, visit) { const record = schema; for (const keyword of JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS) { if (keyword in record) { visit(record[keyword], [...path, keyword], keyword); } } for (const keyword of JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS) { const children = record[keyword]; if (Array.isArray(children)) { for (const [index, child] of children.entries()) { visit(child, [...path, keyword, String(index)], keyword); } } else if (children !== undefined) { visit(children, [...path, keyword], keyword); } } for (const keyword of JSON_SCHEMA_MAP_SCHEMA_KEYWORDS) { const children = record[keyword]; if (!isObject(children)) continue; for (const [key, child] of Object.entries(children)) { // Draft 7 dependencies also permits property dependency arrays. They // are not schemas and must not be traversed as literal JSON payloads. if (keyword === 'dependencies' && !isSchemaDefinition(child)) continue; visit(child, [...path, keyword, key], keyword); } } } function toStrictJsonSchema(schema) { const schemaCopy = structuredClone(schema); // JSON serialization omits undefined object properties. Drop optional // placeholders before any structural checks so they cannot accidentally // look like validation-bearing schema keywords. stripUndefinedSchemaKeywords(schemaCopy); normalizeSingletonTypeArrays(schemaCopy); // Root ref/allOf normalization can promote a nested branch into the root. // Reject separate resource scopes while their original nesting is still // visible so branch-local definitions cannot be rebound at the root. assertNoNestedSchemaIds(schemaCopy); normalizeRootRefAndAllOf(schemaCopy); if (schemaCopy.type !== 'object') { throw new Error(`Root schema must have type: 'object' but got type: ${schemaCopy.type ? `'${schemaCopy.type}'` : 'undefined'}`); } if (schemaCopy.anyOf !== undefined) { throw new Error('Root schema must not use `anyOf` because strict Structured Outputs requires a root object without a union.'); } validateRefSchemas(schemaCopy, [], schemaCopy); preserveAllOfRefTargets(schemaCopy); validateRefSchemas(schemaCopy, [], schemaCopy); rewriteLocalRefsIntoFilteredAnyOfBranches(schemaCopy); // Resolve representable object intersections before recursive // strictification closes referenced definitions. Otherwise a definition // reached through an allOf ref would be closed in isolation before its // sibling object properties can be merged. normalizeObjectAllOfBranches(schemaCopy, [], schemaCopy); const strictSchema = ensureStrictJsonSchema(schemaCopy, [], schemaCopy); validateRefSchemas(strictSchema, [], strictSchema); return strictSchema; } function stripUndefinedSchemaKeywords(schema, visited = new Set()) { if (typeof schema === 'boolean' || !isObject(schema) || visited.has(schema)) { return; } visited.add(schema); const schemaRecord = schema; for (const keyword of Object.keys(schemaRecord)) { if (schemaRecord[keyword] === undefined) { delete schemaRecord[keyword]; } } forEachJSONSchemaChild(schema, [], (child) => { stripUndefinedSchemaKeywords(child, visited); }); } /** * Root ref inlining and singleton allOf flattening can expose each other. * Iterate until flattening no longer produces another root ref so every * exactly representable chain reaches its final object form before the root * type check runs. */ function normalizeRootRefAndAllOf(schema) { const seenRefs = new Set(); while (true) { if (typeof schema.$ref === 'string') { if (seenRefs.has(schema.$ref)) { throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(schema.$ref)); } seenRefs.add(schema.$ref); } inlineRootRefObject(schema); preserveAllOfRefTargets(schema, true); normalizeRootAllOf(schema); const normalizedAnyOf = normalizeRootAnyOf(schema); if (schema.$ref === undefined && !normalizedAnyOf) { return; } } } /** * Some Standard Schema converters emit the root object through a local ref, * with the referenced schema stored in a root definition map. Structured * Outputs requires the root itself to be an object, so inline that safe, * definition-only form while keeping the root maps available for every local * pointer in the schema. */ function inlineRootRefObject(schema) { let ref = schema.$ref; if (ref === undefined) { return; } assertLocalRootRef(ref); if (!hasOnlyRootRefAndDefinitions(schema)) { throw new Error('Schema $ref at `<root>` has non-metadata siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.'); } const seenRefs = new Set(); // Ref siblings are annotations in Draft 7, so keep them while following // aliases. Add outer annotations first so they win over inner aliases and // the final target when the effective root is assembled. const inheritedAnnotations = Object.fromEntries(Object.entries(schema).filter(([keyword]) => JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword))); let resolved; while (true) { if (seenRefs.has(ref)) { throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(ref)); } seenRefs.add(ref); const target = resolveLocalRef(schema, ref); if (target === undefined) { throw new Error('Local $ref at `<root>` does not resolve to an object or boolean schema: ' + JSON.stringify(ref)); } if (typeof target === 'boolean') { throw new TypeError('Expected object schema but got boolean; path=<root>'); } const nextRef = target.$ref; if (nextRef === undefined) { resolved = target; break; } assertLocalRootRef(nextRef); if (seenRefs.has(nextRef)) { throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(nextRef)); } if (!hasOnlyRefAndAnnotations(target)) { throw new Error('Schema $ref in root chain has non-annotation siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.'); } for (const keyword of JSON_SCHEMA_ANNOTATION_KEYWORDS) { if (!(keyword in inheritedAnnotations) && keyword in target) { inheritedAnnotations[keyword] = target[keyword]; } } ref = nextRef; } const rootDefinitions = schema.$defs; const legacyDefinitions = schema.definitions; const rootMetadata = Object.fromEntries(Object.entries(schema).filter(([keyword]) => JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword) || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword))); const inlined = structuredClone(resolved); // A target's definition map lives below the target's original pointer, not // at the document root. Keep the outer document map at the root so existing // absolute refs retain their meaning; the retained outer map also keeps the // target's nested map reachable at its original pointer. for (const keyword of ['$defs', 'definitions']) { if (schema[keyword] !== undefined && inlined[keyword] !== undefined) { delete inlined[keyword]; } } const schemaRecord = schema; for (const keyword of Object.keys(schema)) { delete schemaRecord[keyword]; } Object.assign(schema, inlined, inheritedAnnotations, rootMetadata); if (rootDefinitions !== undefined) { schema.$defs = rootDefinitions; } if (legacyDefinitions !== undefined) { schema.definitions = legacyDefinitions; } } /** * Root object validation runs before recursive strictification, so normalize * the same exactly representable allOf forms here that the recursive pass * handles for nested schemas. */ function normalizeRootAllOf(schema) { while (schema.allOf !== undefined) { // Root normalization runs before recursive strictification so the root // type check can see an inlined object. Normalize nested intersections in // its branches first for the same associative allOf case handled by the // later recursive pass. if (Array.isArray(schema.allOf)) { for (const [index, branch] of schema.allOf.entries()) { normalizeObjectAllOfBranches(branch, ['allOf', String(index)], schema); } } if (mergeObjectAllOf(schema, [], schema)) { // Removing neutral true branches can leave a singleton allOf behind; // keep normalizing until the root reaches its final object form. continue; } const allOf = schema.allOf; if (!Array.isArray(allOf) || allOf.length !== 1 || !hasOnlyRootAllOfMetadataSiblings(schema)) { return; } const branch = allOf[0]; if (typeof branch === 'boolean' || !isObject(branch)) { return; } const rootMetadata = { ...schema }; delete rootMetadata.allOf; const normalized = structuredClone(branch); const schemaRecord = schema; for (const keyword of Object.keys(schema)) { delete schemaRecord[keyword]; } Object.assign(schema, normalized, rootMetadata); } } /** * A singleton root anyOf with no validating siblings is equivalent to its * only branch. Flatten it before the root-union check so converters that * retain a redundant object wrapper can still produce a strict root object. */ function normalizeRootAnyOf(schema) { const anyOf = schema.anyOf; if (!Array.isArray(anyOf) || !hasOnlyRootAnyOfMetadataSiblings(schema)) { return false; } // `false` contributes no instances to a union. Keep the original array in // place until promotion so refs into the surviving branch can be rewritten // from their original index, while refs into removed false branches become // dangling and remain fail closed after the wrapper disappears. const realBranches = anyOf .map((branch, index) => ({ branch, index })) .filter(({ branch }) => branch !== false); if (realBranches.length !== 1) { return false; } const { branch, index: branchIndex } = realBranches[0]; if (typeof branch === 'boolean' || !isObject(branch) || !isObjectOnlySchema(branch, schema)) { return false; } const definitionRenames = planPromotedRootAnyOfDefinitionRenames(schema, branch); rewriteLocalRefsIntoPromotedRootAnyOfBranch(schema, branchIndex, definitionRenames); const rootMetadata = { ...schema }; delete rootMetadata.anyOf; const normalized = structuredClone(branch); for (const keyword of ['$defs', 'definitions']) { const rootDefinitions = schema[keyword]; const branchDefinitions = normalized[keyword]; if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) { continue; } const renames = definitionRenames.get(keyword); const mergedDefinitions = { ...rootDefinitions }; for (const [name, definition] of Object.entries(branchDefinitions)) { mergedDefinitions[renames?.get(name) ?? name] = definition; } normalized[keyword] = mergedDefinitions; delete rootMetadata[keyword]; } const schemaRecord = schema; for (const keyword of Object.keys(schema)) { delete schemaRecord[keyword]; } Object.assign(schema, normalized, rootMetadata); return true; } /** * Root and promoted branch definition maps occupy the same pointer after * promotion. Give conflicting branch definitions stable aliases before refs * are rewritten so neither original target is rebound. */ function planPromotedRootAnyOfDefinitionRenames(root, branch) { const renames = new Map(); for (const keyword of ['$defs', 'definitions']) { const rootDefinitions = root[keyword]; const branchDefinitions = branch[keyword]; if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) { continue; } const usedNames = new Set([...Object.keys(rootDefinitions), ...Object.keys(branchDefinitions)]); const keywordRenames = new Map(); let aliasIndex = 0; for (const [name, definition] of Object.entries(branchDefinitions)) { if (!Object.prototype.hasOwnProperty.call(rootDefinitions, name) || schemasEqual(rootDefinitions[name], definition)) { continue; } let alias = '__openai_strict_anyOf_definition_' + aliasIndex++; while (usedNames.has(alias)) { alias = '__openai_strict_anyOf_definition_' + aliasIndex++; } usedNames.add(alias); keywordRenames.set(name, alias); } if (keywordRenames.size > 0) { renames.set(keyword, keywordRenames); } } return renames; } /** * Promoting a singleton root anyOf branch removes the original anyOf/index * pointer prefix. Rewrite refs through that prefix while the old tree still * exists so the promoted schema keeps naming the same targets. */ function rewriteLocalRefsIntoPromotedRootAnyOfBranch(root, branchIndex, definitionRenames) { const rewriteRef = (ref) => { const parts = parseLocalRef(ref); if (parts === undefined || parts[0] !== 'anyOf' || parts[1] !== String(branchIndex)) { return ref; } const promotedParts = parts.slice(2); const definitionKeyword = promotedParts[0]; if (promotedParts.length > 1 && (definitionKeyword === '$defs' || definitionKeyword === 'definitions')) { const renamed = definitionRenames.get(definitionKeyword)?.get(promotedParts[1]); if (renamed !== undefined) { promotedParts[1] = renamed; } } return promotedParts.length === 0 ? '#' : '#/' + promotedParts.map(encodeJSONPointerTokenForURIFragment).join('/'); }; const rewriteRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { value.$ref = rewriteRef(value.$ref); } forEachJSONSchemaChild(value, [], (child) => { rewriteRefs(child); }); }; rewriteRefs(root); } function assertLocalRootRef(ref) { if (typeof ref !== 'string') { throw new TypeError('Received non-string $ref - ' + String(ref) + '; path=<root>'); } if (!ref.startsWith('#')) { throw new Error('External $ref at `<root>` is not supported in strict Structured Outputs: ' + JSON.stringify(ref)); } } function hasOnlyRootRefAndDefinitions(schema) { return Object.keys(schema).every((keyword) => keyword === '$ref' || keyword === '$defs' || keyword === 'definitions' || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) || JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)); } /** * Draft 7 permits `type` to be either a string or an array of strings. A * singleton array has exactly the same validation semantics as its scalar * form, so canonicalize it before root validation and recursive strictifying. * Multi-type arrays carry real union semantics and must remain unchanged. */ function normalizeSingletonTypeArrays(schema) { if (typeof schema === 'boolean' || !isObject(schema)) { return; } if (Array.isArray(schema.type) && schema.type.length === 1) { schema.type = schema.type[0]; } forEachJSONSchemaChild(schema, [], (child) => { normalizeSingletonTypeArrays(child); }); } function isNullable(schema, root, seenRefs = new Set()) { if (typeof schema === 'boolean') { return schema; } const ref = schema.$ref; if (ref !== undefined) { // Annotation keywords do not constrain validation, so they are safe beside // a local ref. Keep the proof conservative for every other sibling because // resolving those correctly would require intersecting the referenced // schema and its sibling constraints. if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(ref)) { return false; } const resolved = resolveLocalRef(root, ref); if (resolved === undefined) { return false; } return isNullable(resolved, root, new Set([...seenRefs, ref])); } if (schema.type !== undefined && schema.type !== 'null' && !(Array.isArray(schema.type) && schema.type.includes('null'))) { return false; } if ('const' in schema && schema.const !== null) { return false; } if (schema.enum !== undefined && (!Array.isArray(schema.enum) || !schema.enum.includes(null))) { return false; } if (schema.allOf !== undefined) { if (!Array.isArray(schema.allOf) || !schema.allOf.every((variant) => isNullable(variant, root))) { return false; } } if (schema.anyOf !== undefined) { if (!Array.isArray(schema.anyOf) || !schema.anyOf.some((variant) => isNullable(variant, root))) { return false; } } if (schema.oneOf !== undefined) { if (!Array.isArray(schema.oneOf) || schema.oneOf.filter((variant) => isNullable(variant, root)).length !== 1) { return false; } } // Conditional and negated schemas need a full JSON Schema evaluator to prove // that null is allowed. Treat them conservatively instead of accepting an // optional field that may not actually accept null. if (schema.not !== undefined || schema.if !== undefined || schema.then !== undefined || schema.else !== undefined) { return false; } return true; } /** * Mutates the given JSON schema to ensure it conforms to the `strict` standard * that the API expects. */ function ensureStrictJsonSchema(jsonSchema, path, root) { if (typeof jsonSchema === 'boolean') { throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`); } if (!isObject(jsonSchema)) { throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be an object; path=${path.join('/')}`); } // Closing each object branch in an allOf independently changes the // intersection: sibling branches' properties become forbidden extras. Merge // the small object-intersection subset that can be represented exactly before // applying strict object closure, and fail closed for the rest. if (mergeObjectAllOf(jsonSchema, path, root)) { return ensureStrictJsonSchema(jsonSchema, path, root); } // false is the identity element for a union. Remove it before proving // whether an outer object/array wrapper is redundant so an impossible // alternative cannot hide the shape of every real branch. Keep all-false // and boolean-only unions intact so the existing boolean-schema rejection // remains fail closed. normalizeAnyOfFalseBranches(jsonSchema); // Closing a type: object wrapper around object union branches without any // own properties would turn it into an empty object and forbid every branch // property. A bare object type is redundant when every branch already proves // the value is an object, so remove only that exact constraint; fail closed // for wrappers whose object constraints cannot be preserved mechanically. normalizeObjectUnionWrapper(jsonSchema, path, root); // Add additionalProperties: false to object schemas. Draft 7 permits object // keywords without an explicit type, so those implicit object shapes need // the same strict handling as type: 'object'. Explicitly open object schemas // cannot be represented in Structured Outputs strict mode. if (hasObjectShape(jsonSchema)) { if (!('additionalProperties' in jsonSchema)) { jsonSchema.additionalProperties = false; } else if (jsonSchema.additionalProperties !== false) { throw new Error(`Object schema at \`${path.join('/') || '<root>'}\` must set \`additionalProperties: false\` to be compatible with strict Structured Outputs.`); } } const required = jsonSchema.required ?? []; if (!Array.isArray(required) || required.some((key) => typeof key !== 'string')) { throw new TypeError(`Expected \`required\` to be an array of strings; path=${path.join('/') || '<root>'}`); } // Handle object properties const properties = jsonSchema.properties; if (hasObjectShape(jsonSchema)) { for (const key of required) { if (!isObject(properties) || !Object.prototype.hasOwnProperty.call(properties, key)) { throw new Error(`Object schema at \`${path.join('/') || '<root>'}\` requires property \`${key}\` but does not declare it in \`properties\`.`); } } } if (isObject(properties)) { for (const [key, value] of Object.entries(properties)) { if (!isNullable(value, root) && !required.includes(key)) { 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`); } } jsonSchema.required = Object.keys(properties); } // Structured Outputs accepts one schema for every array item and does not // support Draft 7 tuples or additionalItems. Reject both forms rather than // advertising a schema whose array validation the API cannot preserve. const items = jsonSchema.items; const additionalItems = jsonSchema.additionalItems; if (Array.isArray(items)) { throw new Error(`Schema at \`${path.join('/') || '<root>'}\` uses tuple-form \`items\`, which cannot be represented in strict Structured Outputs.`); } if (additionalItems !== undefined) { throw new Error(`Schema at \`${path.join('/') || '<root>'}\` uses unsupported keyword \`additionalItems\` and cannot be represented in strict Structured Outputs.`); } // Handle intersections (allOf) const allOf = jsonSchema.allOf; if (Array.isArray(allOf)) { if (allOf.length === 1 && hasOnlyAnnotationSiblings(jsonSchema, 'allOf')) { const branch = allOf[0]; if (branch === false) { throw new Error(`Schema at \`${path.join('/') || '<root>'}\` uses \`allOf: [false]\`, which cannot be represented in strict Structured Outputs.`); } if (branch === true) { // true is the neutral schema for an intersection, so removing this // branch preserves validation while retaining the parent annotations. delete jsonSchema.allOf; } else { const resolved = ensureStrictJsonSchema(branch, [...path, 'allOf', '0'], root); const annotations = { ...jsonSchema }; delete annotations.allOf; Object.assign(jsonSchema, resolved, annotations); delete jsonSchema.allOf; } } } normalizeArrayUnionWrapper(jsonSchema, root); const schemaRecord = jsonSchema; for (const keyword of JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS) { // Optional converter output often keeps undefined placeholders on the // JavaScript object even though JSON serialization omits them. They carry // no validation semantics, so treat only undefined as absent; every // defined value remains unsupported. if (schemaRecord[keyword] !== undefined) { throw new Error(`Schema at \`${path.join('/') || '<root>'}\` uses unsupported keyword \`${keyword}\` and cannot be represented in strict Structured Outputs.`); } delete schemaRecord[keyword]; } const type = jsonSchema.type; const currentItems = jsonSchema.items; if ((type === 'array' || (Array.isArray(type) && type.includes('array'))) && currentItems === undefined) { throw new Error(`Schema at \`${path.join('/') || '<root>'}\` declares an array without \`items\`, which cannot be represented in strict Structured Outputs.`); } forEachJSONSchemaChild(jsonSchema, path, (child, childPath, keyword) => { // These boolean forms are already handled as parent-keyword semantics: // additionalProperties: false closes objects, while boolean // additionalItems does not contain a nested schema to strictify. if (typeof child === 'boolean' && (keyword === 'additionalProperties' || keyword === 'additionalItems')) { return; } ensureStrictJsonSchema(child, childPath, root); }); // Strip `null` defaults as there's no meaningful distinction if (jsonSchema.default === null) { delete jsonSchema.default; } return jsonSchema; } function parseLocalRef(ref) { if (!ref.startsWith('#')) { return undefined; } let pointer; try { // A local $ref is a URI fragment containing a JSON Pointer. RFC 6901 // decodes the complete fragment before tokenizing the pointer, so an // encoded slash is a separator rather than part of a literal key. pointer = decodeURIComponent(ref.slice(1)); } catch { return undefined; } if (pointer === '') { return []; } if (!pointer.startsWith('/')) { return undefined; } const parts = []; for (const encodedPart of pointer.slice(1).split('/')) { // JSON Pointer only defines ~0 and ~1 escapes. Reject malformed escape // sequences instead of looking up a different literal key. if (/~(?:[^01]|$)/.test(encodedPart)) { return undefined; } parts.push(encodedPart.replace(/~1/g, '/').replace(/~0/g, '~')); } return parts; } function resolvePointerPart(resolved, part) { if (Array.isArray(resolved)) { if (!/^(?:0|[1-9]\d*)$/.test(part)) { return undefined; } const index = Number(part); if (!Object.prototype.hasOwnProperty.call(resolved, index)) { return undefined; } return resolved[index]; } if (!isObject(resolved) || !Object.prototype.hasOwnProperty.call(resolved, part)) { return undefined; } return resolved[part]; } function resolveLocalRef(root, ref) { const parts = parseLocalRef(ref); if (parts === undefined) { return undefined; } // Literal payloads such as `default`, `enum`, and `const` can contain // object-shaped values, but they are not schemas and are never traversed by // strictification. Resolve only the schema-bearing locations visited by // forEachJSONSchemaChild so every accepted target is normalized before we // advertise the result as strict. let resolved = root; for (let index = 0; index < parts.length;) { if (!isObject(resolved)) { return undefined; } const keyword = parts[index]; if (JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS.includes(keyword)) { resolved = resolvePointerPart(resolved, keyword); index += 1; continue; } if (JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS.includes(keyword)) { resolved = resolvePointerPart(resolved, keyword); index += 1; if (Array.isArray(resolved)) { if (index >= parts.length) { return undefined; } resolved = resolvePointerPart(resolved, parts[index]); index += 1; } continue; } if (JSON_SCHEMA_MAP_SCHEMA_KEYWORDS.includes(keyword)) { const children = resolvePointerPart(resolved, keyword); index += 1; if (!isObject(children) || index >= parts.length) { return undefined; } resolved = resolvePointerPart(children, parts[index]); if (keyword === 'dependencies' && !isSchemaDefinition(resolved)) { return undefined; } index += 1; continue; } return undefined; } return isSchemaDefinition(resolved) ? resolved : undefined; } function isObject(obj) { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); } function isSchemaDefinition(value) { return typeof value === 'boolean' || isObject(value); } function isObjectOnlySchema(schema, root, seenRefs = new Set()) { if (typeof schema === 'boolean' || !isObject(schema)) { return false; } if (schema.$ref !== undefined) { if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) { return false; } const resolved = resolveLocalRef(root, schema.$ref); if (resolved === undefined) { return false; } return isObjectOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref])); } if (schema.allOf !== undefined) { if (!Array.isArray(schema.allOf) || schema.allOf.length !== 1 || !hasOnlyAnnotationSiblings(schema, 'allOf')) { return false; } const branch = schema.allOf[0]; return branch !== undefined && branch !== true && branch !== false ? isObjectOnlySchema(branch, root, seenRefs) : false; } return (schema.type === 'object' || (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'object')); } function isArrayOnlySchema(schema, root, seenRefs = new Set()) { if (typeof schema === 'boolean' || !isObject(schema)) { return false; } if (schema.$ref !== undefined) { if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) { return false; } const resolved = resolveLocalRef(root, schema.$ref); if (resolved === undefined) { return false; } return isArrayOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref])); } if (schema.allOf !== undefined) { if (!Array.isArray(schema.allOf) || schema.allOf.length !== 1 || !hasOnlyAnnotationSiblings(schema, 'allOf')) { return false; } const branch = schema.allOf[0]; return branch !== undefined && branch !== true && branch !== false ? isArrayOnlySchema(branch, root, seenRefs) : false; } return (schema.type === 'array' || (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'array')); } function hasOnlyRefAndAnnotations(schema) { return Object.keys(schema).every( // Definition maps do not add sibling validation constraints, and keeping // them in place preserves local pointers into a nested alias's scope. (keyword) => keyword === '$ref' || keyword === '$defs' || keyword === 'definitions' || JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)); } function hasOnlyAnnotationSiblings(schema, keyword) { const schemaRecord = schema; return Object.keys(schema).every( // Definition maps do not add sibling validation constraints. Keep them // beside a flattened singleton allOf so refs into this nested scope stay // reachable at their original pointers. (schemaKeyword) => schemaKeyword === keyword || ((schemaKeyword === '$defs' || schemaKeyword === 'definitions') && isObject(schemaRecord[schemaKeyword])) || JSON_SCHEMA_ANNOTATION_KEYWORDS.has(schemaKeyword)); } function hasOnlyRootAllOfMetadataSiblings(schema) { return Object.keys(schema).every((keyword) => keyword === 'allOf' || keyword === '$defs' || keyword === 'definitions' || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) || JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)); } function hasOnlyRootAnyOfMetadataSiblings(schema) { return Object.keys(schema).every((keyword) => keyword === 'anyOf' || keyword === '$defs' || keyword === 'definitions' || (keyword === 'type' && schema.type === 'object') || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) || JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)); } function hasObjectKeywords(schema) { return Object.keys(schema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword)); } function hasObjectShape(schema) { const typ = schema.type; return (typ === 'object' || (Array.isArray(typ) && typ.includes('object')) || (typ === undefined && hasObjectKeywords(schema))); } function isRedundantUnionWrapperType(type, branchType) { return (type === branchType || (Array.isArray(type) && type.length === 2 && type.includes(branchType) && type.includes('null'))); } function normalizeObjectUnionWrapper(jsonSchema, path, root) { if (jsonSchema.anyOf === undefined) { return; } const hasEmptyProperties = isObject(jsonSchema.properties) && Object.keys(jsonSchema.properties).length === 0; const hasEmptyRequired = Array.isArray(jsonSchema.required) && jsonSchema.required.length === 0; if (hasEmptyProperties) { delete jsonSchema.properties; } if (hasEmptyRequired) { delete jsonSchema.required; } if (!hasObjectShape(jsonSchema)) { return; } const hasOwnObjectConstraints = Object.keys(jsonSchema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword)); if (isRedundantUnionWrapperType(jsonSchema.type, 'object') && !hasOwnObjectConstraints && Array.isArray(jsonSchema.anyOf) && jsonSchema.anyOf.every((branch) => isObjectOnlySchema(branch, root))) { // The union already excludes null and every non-object value, so both a // scalar object wrapper and a nullable object wrapper are redundant under // Draft 7's conjunctive keyword semantics. delete jsonSchema.type; return; } throw new Error('Object anyOf schema at `' + (path.join('/') || '<root>') + '` cannot be represented in strict Structured Outputs without changing Draft 7 validation.'); } function normalizeArrayUnionWrapper(jsonSchema, root) { if (isRedundantUnionWrapperType(jsonSchema.type, 'array') && jsonSchema.items === undefined && Array.isArray(jsonSchema.anyOf) && jsonSchema.anyOf.every((branch) => isArrayOnlySchema(branch, root))) { // Every union branch already proves the value is an array and excludes // null. Keeping either redundant wrapper would require an outer items // schema that does not contribute any Draft 7 validation. delete jsonSchema.type; } } function normalizeAnyOfFalseBranches(jsonSchema) { if (!Array.isArray(jsonSchema.anyOf)) { return; } const realBranches = jsonSchema.anyOf.filter((branch) => branch !== false); if (realBranches.length > 0 && realBranches.length !== jsonSchema.anyOf.length) { jsonSchema.anyOf = realBranches; } } function assertNoNestedSchemaIds(schema) { const visit = (value, path) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (path.length > 0 && value.$id !== undefined) { throw new Error('Nested $id at ' + JSON.stringify(path.join('/')) + ' establishes a separate JSON Schema resource scope and cannot be represented in strict Structured Outputs.'); } forEachJSONSchemaChild(value, path, (child, childPath) => { visit(child, childPath); }); }; visit(schema, []); } function refTargetsAllOfBranch(root, ref) { const parts = parseLocalRef(ref); if (parts === undefined) { return false; } let resolved = root; for (const [index, part] of parts.entries()) { if (part === 'allOf' && isObject(resolved) && Array.isArray(resolved['allOf']) && index < parts.length - 1) { return true; } resolved = resolvePointerPart(resolved, part); if (resolved === undefined) { return false; } } return false; } function escapeJSONPointerToken(token) { return token.replace(/~/g, '~0').replace(/\//g, '~1'); } function encodeJSONPointerTokenForURIFragment(token) { // `$` is a valid URI fragment sub-delimiter and keeping it readable retains // the conventional `#/$defs/...` spelling. Everything else that could // invalidate or retokenize the fragment (notably `%` and spaces) is encoded // after JSON Pointer escaping. return encodeURIComponent(escapeJSONPointerToken(token)).replace(/%24/g, '$'); } /** * Standard Schema normalization moves representable oneOf branches to anyOf. * Rewrite only pointers that traverse an actual oneOf schema array while the * original tree is still intact, preserving escaped tokens for every other * path segment. */ function rewriteLocalRefsIntoMovedOneOfBranches(root) { const rewriteRef = (ref) => { const parts = parseLocalRef(ref); if (parts === undefined || parts.length === 0) { return ref; } let resolved = root; let changed = false; for (const [index, part] of parts.entries()) { if (part === 'oneOf' && index < parts.length - 1 && isObject(resolved) && Array.isArray(resolved['oneOf'])) { parts[index] = 'anyOf'; changed = true; } resolved = resolvePointerPart(resolved, part); if (resolved === undefined) { return ref; } } return changed ? '#/' + parts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref; }; const rewriteRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { value.$ref = rewriteRef(value.$ref); } forEachJSONSchemaChild(value, [], (child) => { rewriteRefs(child); }); }; rewriteRefs(root); } /** * Strictification removes false anyOf alternatives. Rewrite pointers into * surviving alternatives before that filtering happens so each local ref * still names the same schema after earlier indices disappear. */ function rewriteLocalRefsIntoFilteredAnyOfBranches(root) { const rewriteRef = (ref) => { const originalParts = parseLocalRef(ref); if (originalParts === undefined || originalParts.length === 0) { return ref; } const rewrittenParts = [...originalParts]; let resolved = root; let changed = false; for (const [index, part] of originalParts.entries()) { const resolvedRecord = typeof resolved === 'object' && resolved !== null && !Array.isArray(resolved) ? resolved : undefined; if (part === 'anyOf' && index < originalParts.length - 1 && resolvedRecord !== undefined && Array.isArray(resolvedRecord['anyOf'])) { const branches = resolvedRecord['anyOf']; const branchIndexPart = originalParts[index + 1]; if (!/^(?:0|[1-9]\d*)$/.test(branchIndexPart)) { return ref; } const branchIndex = Number(branchIndexPart); if (!Object.prototype.hasOwnProperty.call(branches, branchIndex)) { return ref; } const realBranches = branches.filter((branch) => branch !== false); if (realBranches.length > 0 && realBranches.length !== branches.length) { // A ref to a removed false schema is already rejected by the first // ref-validation pass. Leave it untouched so it remains fail closed. if (branches[branchIndex] === false) { return ref; } const rewrittenIndex = branches.slice(0, branchIndex).filter((branch) => branch !== false).length; if (rewrittenIndex !== branchIndex) { rewrittenParts[index + 1] = String(rewrittenIndex); changed = true; } } } // Resolve through the original pointer, not the rewritten one, so a // nested anyOf can also be remapped after its parent index shifts. resolved = resolvePointerPart(resolved, part); if (resolved === undefined) { return ref; } } return changed ? '#/' + rewrittenParts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref; }; const rewriteRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { value.$ref = rewriteRef(value.$ref); } forEachJSONSchemaChild(value, [], (child) => { rewriteRefs(child); }); }; rewriteRefs(root); } /** * Strictification removes every representable allOf. Preserve any schema * referenced through an allOf branch under a stable root definition first so * structural flattening cannot leave a dangling local pointer behind. */ function preserveAllOfRefTargets(root, rootOnly = false) { const refsToPreserve = new Set(); const collectRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string' && refTargetsAllOfBranch(root, value.$ref)) { const pointerParts = parseLocalRef(value.$ref); if (!rootOnly || pointerParts?.[0] === 'allOf') { refsToPreserve.add(value.$ref); } } forEachJSONSchemaChild(value, [], (child) => { collectRefs(child); }); }; collectRefs(root); if (refsToPreserve.size === 0) { return; } if (root.$defs !== undefined && !isObject(root.$defs)) { throw new Error('Root schema has invalid $defs and cannot preserve local allOf references.'); } const definitions = (root.$defs ?? (root.$defs = {})); const rewrittenRefs = new Map(); let aliasIndex = 0; for (const ref of refsToPreserve) { const target = resolveLocalRef(root, ref); if (!isSchemaDefinition(target)) { if (rootOnly) { continue; } throw new Error('Local $ref cannot be preserved before allOf flattening: ' + JSON.stringify(ref)); } let alias = '__openai_strict_allOf_ref_' + aliasIndex++; while (Object.prototype.hasOwnProperty.call(definitions, alias)) { alias = '__openai_strict_allOf_ref_' + aliasIndex++; } definitions[alias] = structuredClone(target); rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias)); } const rewriteRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref; } forEachJSONSchemaChild(value, [], (child) => { rewriteRefs(child); }); }; rewriteRefs(root); } /** * Closed allOf merges can discard optional property declarations. Preserve * only local refs into declarations that are about to disappear, then rewrite * those refs to stable root definitions before the merge removes their paths. */ function preserveDiscardedAllOfPropertyRefTargets(root, discardedPaths) { if (discardedPaths.length === 0) { return; } const refsToPreserve = new Set(); const collectRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { const parts = parseLocalRef(value.$ref); if (parts !== undefined && discardedPaths.some((discardedPath) => parts.length >= discardedPath.length && discardedPath.every((part, index) => parts[index] === part))) { refsToPreserve.add(value.$ref); } } forEachJSONSchemaChild(value, [], (child) => { collectRefs(child); }); }; collectRefs(root); if (refsToPreserve.size === 0) { return; } if (root.$defs !== undefined && !isObject(root.$defs)) { throw new Error('Root schema has invalid $defs and cannot preserve discarded allOf properties.'); } const definitions = (root.$defs ?? (root.$defs = {})); const rewrittenRefs = new Map(); let aliasIndex = 0; for (const ref of refsToPreserve) { const target = resolveLocalRef(root, ref); if (!isSchemaDefinition(target)) { throw new Error('Local $ref cannot be preserved before allOf property removal: ' + JSON.stringify(ref)); } let alias = '__openai_strict_allOf_property_ref_' + aliasIndex++; while (Object.prototype.hasOwnProperty.call(definitions, alias)) { alias = '__openai_strict_allOf_property_ref_' + aliasIndex++; } definitions[alias] = structuredClone(target); rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias)); } const rewriteRefs = (value) => { if (typeof value === 'boolean' || !isObject(value)) { return; } if (typeof value.$ref === 'string') { value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref; } forEachJSONSchemaChild(value, [], (child) => { rewriteRefs(child); }); }; rewriteRefs(root); } function validateRefSchemas(schema, path, root) { if (typeof schema === 'boolean' || !isObject(schema)) { return; } const ref = schema.$ref; if (ref !== undefined) { if (typeof ref !== 'string') { throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`); } if (!ref.startsWith('#')) { throw new Error(`External $ref at \`${path.join('/') || '<root>'}\` is not supported in strict Structured Outputs: ${JSON.stringify(ref)}`); } const resolved = resolveLocalRef(root, ref); if (resolved === undefined || !isSchemaDefinition(resolved)) { throw new Error(`Local $ref at \`${path.join('/') || '<root>'}\` does not resolve to an object or boolean schema: ${JSON.stringify(ref)}