-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: add json schema transformation logic #1715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
0a3b077
7e22f24
3de111b
4025547
dd4012e
e75e41f
84d211d
1874dcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,28 +14,33 @@ import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/Res | |
| import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses'; | ||
| import { toStrictJsonSchema } from '../lib/transform'; | ||
| import { JSONSchema } from '../lib/jsonschema'; | ||
| import { transformJSONSchema } from '../lib/transform-json-schema'; | ||
|
|
||
| type InferZodType<T> = | ||
| T extends z4.ZodType ? z4.infer<T> | ||
| : T extends z3.ZodType ? z3.infer<T> | ||
| : never; | ||
|
|
||
| function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Record<string, unknown> { | ||
| return _zodToJsonSchema(schema, { | ||
| openaiStrictMode: true, | ||
| name: options.name, | ||
| nameStrategy: 'duplicate-ref', | ||
| $refStrategy: 'extract-to-root', | ||
| nullableStrategy: 'property', | ||
| }); | ||
| return transformJSONSchema( | ||
| _zodToJsonSchema(schema, { | ||
| openaiStrictMode: true, | ||
| name: options.name, | ||
| nameStrategy: 'duplicate-ref', | ||
| $refStrategy: 'extract-to-root', | ||
| nullableStrategy: 'property', | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| function zodV4ToJsonSchema(schema: z4.ZodType): Record<string, unknown> { | ||
| return toStrictJsonSchema( | ||
| z4.toJSONSchema(schema, { | ||
| target: 'draft-7', | ||
| }) as JSONSchema, | ||
| ) as Record<string, unknown>; | ||
| return transformJSONSchema( | ||
| toStrictJsonSchema( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh wait we already have
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh yeah you're right. It looks like it also checks that the root is an object, which is slightly different behavior between v3 and v4, and if we change it users will now get vanilla Errors rather than 400s OpenAIErrors. I think that that's probably fine though |
||
| z4.toJSONSchema(schema, { | ||
| target: 'draft-7', | ||
| }) as JSONSchema, | ||
| ) as Record<string, unknown>, | ||
| ); | ||
| } | ||
|
|
||
| function isZodV4(zodObject: z3.ZodType | z4.ZodType): zodObject is z4.ZodType { | ||
|
|
||
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { pop } from '../internal/utils'; | ||
|
|
||
| const SUPPORTED_STRING_FORMATS = new Set([ | ||
| 'date-time', | ||
| 'time', | ||
| 'date', | ||
| 'duration', | ||
| 'email', | ||
| 'hostname', | ||
| 'ipv4', | ||
| 'ipv6', | ||
| 'uuid', | ||
| ]); | ||
|
|
||
| export type JSONSchema = Record<string, any>; | ||
|
|
||
| function deepClone<T>(obj: T): T { | ||
| return JSON.parse(JSON.stringify(obj)); | ||
| } | ||
|
|
||
| export function transformJSONSchema(jsonSchema: JSONSchema): JSONSchema { | ||
| const workingCopy = deepClone(jsonSchema); | ||
| return _transformJSONSchema(workingCopy); | ||
| } | ||
|
|
||
| function _transformJSONSchema(jsonSchema: JSONSchema): JSONSchema { | ||
RobertCraigie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const strictSchema: JSONSchema = {}; | ||
|
|
||
| const ref = pop(jsonSchema, '$ref'); | ||
| if (ref !== undefined) { | ||
| strictSchema['$ref'] = ref; | ||
| return strictSchema; | ||
| } | ||
|
|
||
| const defs = pop(jsonSchema, '$defs'); | ||
| if (defs !== undefined) { | ||
| const strictDefs: Record<string, any> = {}; | ||
| strictSchema['$defs'] = strictDefs; | ||
| for (const [name, defSchema] of Object.entries(defs)) { | ||
| strictDefs[name] = _transformJSONSchema(defSchema as JSONSchema); | ||
| } | ||
| } | ||
|
|
||
| const type = pop(jsonSchema, 'type'); | ||
| const anyOf = pop(jsonSchema, 'anyOf'); | ||
| const oneOf = pop(jsonSchema, 'oneOf'); | ||
| const allOf = pop(jsonSchema, 'allOf'); | ||
|
|
||
| if (Array.isArray(anyOf)) { | ||
| strictSchema['anyOf'] = anyOf.map((variant) => _transformJSONSchema(variant as JSONSchema)); | ||
| } else if (Array.isArray(oneOf)) { | ||
| strictSchema['anyOf'] = oneOf.map((variant) => _transformJSONSchema(variant as JSONSchema)); | ||
| } else if (Array.isArray(allOf)) { | ||
| strictSchema['allOf'] = allOf.map((entry) => _transformJSONSchema(entry as JSONSchema)); | ||
| } else { | ||
| if (type === undefined) { | ||
| throw new Error('JSON schema must have a type defined if anyOf/oneOf/allOf are not used'); | ||
| } | ||
| strictSchema['type'] = type; | ||
| } | ||
|
|
||
| const description = pop(jsonSchema, 'description'); | ||
| if (description !== undefined) { | ||
| strictSchema['description'] = description; | ||
| } | ||
|
|
||
| const title = pop(jsonSchema, 'title'); | ||
| if (title !== undefined) { | ||
| strictSchema['title'] = title; | ||
| } | ||
|
|
||
| if (type === 'object') { | ||
| const properties = pop(jsonSchema, 'properties') || {}; | ||
|
|
||
| strictSchema['properties'] = Object.fromEntries( | ||
| Object.entries(properties).map(([key, propSchema]) => [ | ||
| key, | ||
| _transformJSONSchema(propSchema as JSONSchema), | ||
| ]), | ||
| ); | ||
|
|
||
| pop(jsonSchema, 'additionalProperties'); | ||
| strictSchema['additionalProperties'] = false; | ||
|
|
||
| const required = pop(jsonSchema, 'required'); | ||
| if (required !== undefined) { | ||
| strictSchema['required'] = required; | ||
| } | ||
| } else if (type === 'string') { | ||
| const format = pop(jsonSchema, 'format'); | ||
| if (format !== undefined && SUPPORTED_STRING_FORMATS.has(format)) { | ||
| strictSchema['format'] = format; | ||
| } else if (format !== undefined) { | ||
| jsonSchema['format'] = format; | ||
| } | ||
| } else if (type === 'array') { | ||
| const items = pop(jsonSchema, 'items'); | ||
| if (items !== undefined) { | ||
| strictSchema['items'] = _transformJSONSchema(items as JSONSchema); | ||
| } | ||
|
|
||
| const minItems = pop(jsonSchema, 'minItems'); | ||
| if (minItems !== undefined && (minItems === 0 || minItems === 1)) { | ||
| strictSchema['minItems'] = minItems; | ||
| } else if (minItems !== undefined) { | ||
| jsonSchema['minItems'] = minItems; | ||
| } | ||
| } | ||
|
|
||
|
||
| if (Object.keys(jsonSchema).length > 0) { | ||
| const existingDescription = strictSchema['description']; | ||
| strictSchema['description'] = | ||
| (existingDescription ? existingDescription + '\n\n' : '') + | ||
| '{' + | ||
| Object.entries(jsonSchema) | ||
| .map(([key, value]) => `${key}: ${JSON.stringify(value)}`) | ||
| .join(', ') + | ||
| '}'; | ||
| } | ||
|
|
||
| return strictSchema; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: is this necessary for v3? maybe it would make sense for consistency but I think we'll already do the
oneOf->anyOftransform in the vendored to-json-schema library we use (I could be wrong)