@aeye/core 0.1.1 → 0.2.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/dist/common.d.ts +62 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +300 -0
- package/dist/common.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +26 -17
- package/dist/prompt.js.map +1 -1
- package/dist/schema.d.ts +69 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +589 -41
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
/**
|
|
3
|
-
* Transfer description and metadata from source Zod schema to target Zod schema
|
|
4
|
-
*
|
|
5
|
-
* @param target - target Zod schema
|
|
6
|
-
* @param source - source Zod schema
|
|
7
|
-
* @returns
|
|
8
|
-
*/
|
|
9
|
-
function transferMetadata(target, source) {
|
|
10
|
-
if (source.description) {
|
|
11
|
-
target = target.describe(source.description);
|
|
12
|
-
}
|
|
13
|
-
const meta = source.meta();
|
|
14
|
-
if (meta) {
|
|
15
|
-
target = target.meta(meta);
|
|
16
|
-
}
|
|
17
|
-
return target;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
3
|
* Recursively transforms a Zod schema to support strict mode.
|
|
21
4
|
*
|
|
22
5
|
* @param schema - input Zod schema
|
|
@@ -43,6 +26,44 @@ export function strictify(schema) {
|
|
|
43
26
|
return transform(schema);
|
|
44
27
|
}
|
|
45
28
|
/**
|
|
29
|
+
* Transfer description and metadata from source Zod schema to target Zod schema
|
|
30
|
+
*
|
|
31
|
+
* @param target - target Zod schema
|
|
32
|
+
* @param source - source Zod schema
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
function transferMetadata(target, source) {
|
|
36
|
+
if (source.description) {
|
|
37
|
+
target = target.describe(source.description);
|
|
38
|
+
}
|
|
39
|
+
const meta = source.meta();
|
|
40
|
+
if (meta) {
|
|
41
|
+
target = target.meta(meta);
|
|
42
|
+
}
|
|
43
|
+
return target;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extracts the input schema from a transformed schema (codec or preprocess).
|
|
47
|
+
* For preprocess with optional, we need the nullable version for strict mode input.
|
|
48
|
+
* This is needed when building input schemas that contain transformed fields.
|
|
49
|
+
*/
|
|
50
|
+
function getInputSchema(schema) {
|
|
51
|
+
if (schema instanceof z.ZodCodec) {
|
|
52
|
+
return schema.def.in;
|
|
53
|
+
}
|
|
54
|
+
// For ZodPipe (which preprocess creates), check if the output is optional
|
|
55
|
+
// If so, we need to return a nullable version for the input schema
|
|
56
|
+
if (schema instanceof z.ZodPipe) {
|
|
57
|
+
const outputSchema = schema.def.out;
|
|
58
|
+
if (outputSchema instanceof z.ZodOptional) {
|
|
59
|
+
// The input from preprocess is already set up to handle null->undefined conversion
|
|
60
|
+
// So we just need to make sure the base type accepts null
|
|
61
|
+
return z.nullable(outputSchema.unwrap());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return schema;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
46
67
|
* Recursively transforms a Zod schema to support strict mode.
|
|
47
68
|
*
|
|
48
69
|
* @param schema - input Zod schema
|
|
@@ -51,11 +72,14 @@ export function strictify(schema) {
|
|
|
51
72
|
function strictifySimple(schema, transform) {
|
|
52
73
|
// Handle ZodOptional - get the inner schema and make it optional
|
|
53
74
|
if (schema instanceof z.ZodOptional) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
const innerSchema = schema.unwrap();
|
|
76
|
+
const transformed = transform(innerSchema);
|
|
77
|
+
// Check if the inner schema is nullable
|
|
78
|
+
// If it's nullable, don't convert null to undefined
|
|
79
|
+
const isNullable = innerSchema instanceof z.ZodNullable;
|
|
80
|
+
// Use a preprocess to convert null to undefined only for non-nullable schemas
|
|
81
|
+
// This avoids issues with codec output validation in recursive schemas
|
|
82
|
+
return transferMetadata(z.preprocess((val) => (!isNullable && val === null) ? undefined : val, transformed.optional()), schema);
|
|
59
83
|
}
|
|
60
84
|
// Handle ZodObject
|
|
61
85
|
if (schema instanceof z.ZodObject) {
|
|
@@ -68,8 +92,8 @@ function strictifySimple(schema, transform) {
|
|
|
68
92
|
// Handle ZodCodec
|
|
69
93
|
if (schema instanceof z.ZodCodec) {
|
|
70
94
|
return transferMetadata(z.codec(transform(schema.def.in), transform(schema.def.out), {
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
decode: schema.def.transform,
|
|
96
|
+
encode: schema.def.reverseTransform,
|
|
73
97
|
}), schema);
|
|
74
98
|
}
|
|
75
99
|
// Handle ZodPipe
|
|
@@ -86,9 +110,12 @@ function strictifySimple(schema, transform) {
|
|
|
86
110
|
}
|
|
87
111
|
// Handle ZodRecord
|
|
88
112
|
if (schema instanceof z.ZodRecord) {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
113
|
+
const keyTransformed = schema.keyType ? transform(schema.keyType) : z.string();
|
|
114
|
+
const valueTransformed = transform(schema.valueType);
|
|
115
|
+
// For the input schema (array of {key, value}), use the input side of any codecs
|
|
116
|
+
const key = getInputSchema(keyTransformed);
|
|
117
|
+
const value = getInputSchema(valueTransformed);
|
|
118
|
+
return transferMetadata(z.codec(z.array(z.object({ key, value })), z.record(keyTransformed, valueTransformed), {
|
|
92
119
|
decode: (arr) => {
|
|
93
120
|
const record = {};
|
|
94
121
|
for (const { key, value } of arr) {
|
|
@@ -126,41 +153,146 @@ function strictifySimple(schema, transform) {
|
|
|
126
153
|
// For all other types (primitives, etc.), return as-is
|
|
127
154
|
return schema;
|
|
128
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Modify a JSON Schema property to make it optional (accept null)
|
|
158
|
+
*
|
|
159
|
+
* @param prop - input JSON Schema property
|
|
160
|
+
* @returns
|
|
161
|
+
*/
|
|
162
|
+
const makeOptional = (prop) => {
|
|
163
|
+
if (prop.type) {
|
|
164
|
+
const values = Array.isArray(prop.type) ? prop.type : [prop.type];
|
|
165
|
+
if (!values.includes('null')) {
|
|
166
|
+
prop.type = [...values, 'null'];
|
|
167
|
+
}
|
|
168
|
+
return prop;
|
|
169
|
+
}
|
|
170
|
+
else if (prop.$ref) {
|
|
171
|
+
// If it's a $ref, wrap it in anyOf with null
|
|
172
|
+
return {
|
|
173
|
+
anyOf: [
|
|
174
|
+
{ $ref: prop.$ref },
|
|
175
|
+
{ type: 'null' }
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
else if (prop.allOf) {
|
|
180
|
+
// If it's an allOf with a single entry containing a $ref, extract and wrap the $ref
|
|
181
|
+
// This handles the common case: { allOf: [{ $ref: "..." }] }
|
|
182
|
+
if (prop.allOf.length === 1 && prop.allOf[0].$ref) {
|
|
183
|
+
return {
|
|
184
|
+
anyOf: [
|
|
185
|
+
{ $ref: prop.allOf[0].$ref },
|
|
186
|
+
{ type: 'null' }
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Otherwise preserve the allOf structure
|
|
191
|
+
return {
|
|
192
|
+
anyOf: [
|
|
193
|
+
{ allOf: prop.allOf },
|
|
194
|
+
{ type: 'null' }
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const { title, description, default: defaultValue, ...rest } = prop;
|
|
200
|
+
return {
|
|
201
|
+
title,
|
|
202
|
+
description,
|
|
203
|
+
default: defaultValue,
|
|
204
|
+
anyOf: [
|
|
205
|
+
rest,
|
|
206
|
+
{ type: 'null' }
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
};
|
|
129
211
|
/**
|
|
130
212
|
* Override function to modify JSON Schema generation
|
|
131
213
|
* to make optional fields accept null values.
|
|
132
214
|
*/
|
|
133
|
-
const typeOverride = ({ jsonSchema }) => {
|
|
134
|
-
if (jsonSchema.type === 'object') {
|
|
215
|
+
const typeOverride = ({ zodSchema, jsonSchema, path }) => {
|
|
216
|
+
if (jsonSchema.type === 'object' && zodSchema instanceof z.ZodObject) {
|
|
135
217
|
const properties = jsonSchema.properties || {};
|
|
136
218
|
const propertyKeys = Object.keys(properties);
|
|
137
219
|
const required = jsonSchema.required || [];
|
|
138
220
|
const optional = propertyKeys.filter((key) => !required.includes(key));
|
|
221
|
+
// Add null to optional fields (those not in required array)
|
|
139
222
|
for (const key of optional) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
223
|
+
properties[key] = makeOptional(properties[key]);
|
|
224
|
+
}
|
|
225
|
+
// Check fields that are required - if they use preprocess with optional, add null
|
|
226
|
+
for (const key of required) {
|
|
227
|
+
const fieldSchema = zodSchema.shape[key];
|
|
228
|
+
// Check if this is a ZodPipe (from preprocess) wrapping optional
|
|
229
|
+
if (fieldSchema instanceof z.ZodPipe) {
|
|
230
|
+
const outputSchema = fieldSchema.def.out;
|
|
231
|
+
if (outputSchema instanceof z.ZodOptional) {
|
|
232
|
+
const prop = properties[key];
|
|
233
|
+
// Skip empty objects - they're placeholders for recursive schemas that will be filled in later
|
|
234
|
+
if (Object.keys(prop).length === 0) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
properties[key] = makeOptional(prop);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
150
240
|
}
|
|
151
241
|
jsonSchema.required = propertyKeys;
|
|
152
242
|
jsonSchema.additionalProperties = false;
|
|
153
243
|
}
|
|
244
|
+
// Convert allOf with single entry to that entry or anyOf.
|
|
245
|
+
// allOf is not supported by some AI schema parsers.
|
|
154
246
|
if (jsonSchema.allOf?.length) {
|
|
155
247
|
if (jsonSchema.allOf.length === 1) {
|
|
156
248
|
if (Object.keys(jsonSchema).length === 1) {
|
|
157
|
-
|
|
249
|
+
// Delete allOf first, THEN assign the content
|
|
250
|
+
const content = jsonSchema.allOf[0];
|
|
251
|
+
delete jsonSchema.allOf;
|
|
252
|
+
Object.assign(jsonSchema, content);
|
|
158
253
|
}
|
|
159
254
|
else {
|
|
160
255
|
jsonSchema.anyOf = jsonSchema.allOf;
|
|
256
|
+
delete jsonSchema.allOf;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// For anyOf with null, convert to type array
|
|
261
|
+
if (jsonSchema.anyOf?.length === 2) {
|
|
262
|
+
if (jsonSchema.anyOf[1].type === 'null'
|
|
263
|
+
&& Object.keys(jsonSchema.anyOf[1]).length === 1
|
|
264
|
+
&& jsonSchema.anyOf[0].type
|
|
265
|
+
&& !Array.isArray(jsonSchema.anyOf[0].type)) {
|
|
266
|
+
const first = jsonSchema.anyOf[0];
|
|
267
|
+
const nullableType = [first.type, 'null'];
|
|
268
|
+
delete jsonSchema.anyOf;
|
|
269
|
+
Object.assign(jsonSchema, first);
|
|
270
|
+
// Set type after assign to avoid it being overwritten
|
|
271
|
+
jsonSchema.type = nullableType;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// anyOf in an anyOf - flatten
|
|
275
|
+
// BUT: Don't flatten if it would lose $ref or other important properties
|
|
276
|
+
if (jsonSchema.anyOf?.length) {
|
|
277
|
+
const newAnyOf = [];
|
|
278
|
+
for (const subSchema of jsonSchema.anyOf) {
|
|
279
|
+
// Check if this sub-schema has a nested anyOf that should be flattened
|
|
280
|
+
// Only flatten if:
|
|
281
|
+
// 1. The sub-schema has an anyOf property
|
|
282
|
+
// 2. The sub-schema has no other properties except anyOf (it's a pure wrapper)
|
|
283
|
+
// 3. None of the elements in the nested anyOf are $refs (which should be preserved)
|
|
284
|
+
const isPureAnyOfWrapper = subSchema.anyOf?.length && Object.keys(subSchema).length === 1;
|
|
285
|
+
const hasRefInNested = subSchema.anyOf?.some((nested) => nested.$ref);
|
|
286
|
+
if (isPureAnyOfWrapper && !hasRefInNested) {
|
|
287
|
+
// Safe to flatten
|
|
288
|
+
newAnyOf.push(...subSchema.anyOf);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Keep as-is
|
|
292
|
+
newAnyOf.push(subSchema);
|
|
161
293
|
}
|
|
162
|
-
delete jsonSchema.allOf;
|
|
163
294
|
}
|
|
295
|
+
jsonSchema.anyOf = newAnyOf;
|
|
164
296
|
}
|
|
165
297
|
};
|
|
166
298
|
/**
|
|
@@ -178,4 +310,420 @@ export function toJSONSchema(schema, strict) {
|
|
|
178
310
|
override: strict ? typeOverride : undefined,
|
|
179
311
|
});
|
|
180
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Converts a Zod schema to JSON Schema V2 with support for different provider formats.
|
|
315
|
+
*
|
|
316
|
+
* This is a custom implementation that recursively inspects Zod schemas and converts them
|
|
317
|
+
* to JSON Schema following the pattern of strictify/strictifySimple.
|
|
318
|
+
*
|
|
319
|
+
* @param schema - The Zod schema to convert
|
|
320
|
+
* @param options - Configuration options for the conversion
|
|
321
|
+
* @returns JSON Schema object compatible with the specified format
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* const schema = z.object({
|
|
326
|
+
* name: z.string(),
|
|
327
|
+
* age: z.number().optional(),
|
|
328
|
+
* });
|
|
329
|
+
*
|
|
330
|
+
* // For OpenAI strict mode
|
|
331
|
+
* const strictSchema = strictify(schema);
|
|
332
|
+
* const jsonSchema = toJSONSchemaV2(strictSchema, { strict: true, format: 'openai' });
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export function toJSONSchemaV2(schema, options) {
|
|
336
|
+
const { strict, format = 'openai' } = options;
|
|
337
|
+
// Currently only OpenAI format is supported
|
|
338
|
+
if (format !== 'openai') {
|
|
339
|
+
throw new Error(`Unsupported format: ${format}. Currently only 'openai' is supported.`);
|
|
340
|
+
}
|
|
341
|
+
const context = {
|
|
342
|
+
strict,
|
|
343
|
+
format,
|
|
344
|
+
definitions: new Map(),
|
|
345
|
+
definitionSchemas: new Map(),
|
|
346
|
+
refCounter: 0,
|
|
347
|
+
path: [],
|
|
348
|
+
};
|
|
349
|
+
const result = convertSchema(schema, context);
|
|
350
|
+
// Add definitions if any were created
|
|
351
|
+
if (context.definitionSchemas.size > 0) {
|
|
352
|
+
result.$defs = Object.fromEntries(context.definitionSchemas);
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Main recursive conversion function
|
|
358
|
+
*/
|
|
359
|
+
function convertSchema(schema, context) {
|
|
360
|
+
// Check if we've seen this schema before (for recursion)
|
|
361
|
+
if (context.definitions.has(schema)) {
|
|
362
|
+
const refName = context.definitions.get(schema);
|
|
363
|
+
return { $ref: `#/$defs/${refName}` };
|
|
364
|
+
}
|
|
365
|
+
// Handle ZodOptional
|
|
366
|
+
if (schema instanceof z.ZodOptional) {
|
|
367
|
+
const innerSchema = schema.unwrap();
|
|
368
|
+
const innerJson = convertSchema(innerSchema, context);
|
|
369
|
+
if (context.strict) {
|
|
370
|
+
// In strict mode, optional fields become nullable
|
|
371
|
+
return makeNullable(innerJson);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// In non-strict mode, optional fields are just not required
|
|
375
|
+
return innerJson;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Handle ZodNullable
|
|
379
|
+
if (schema instanceof z.ZodNullable) {
|
|
380
|
+
const innerSchema = schema.unwrap();
|
|
381
|
+
const innerJson = convertSchema(innerSchema, context);
|
|
382
|
+
return makeNullable(innerJson);
|
|
383
|
+
}
|
|
384
|
+
// Handle ZodObject
|
|
385
|
+
if (schema instanceof z.ZodObject) {
|
|
386
|
+
// Check if this object is recursive (contains self-references through getters)
|
|
387
|
+
// If it has an 'aid' in meta, create a definition for it
|
|
388
|
+
const meta = schema.meta();
|
|
389
|
+
if (meta?.aid && !context.definitions.has(schema)) {
|
|
390
|
+
// Register this schema as a definition
|
|
391
|
+
const refName = meta.aid;
|
|
392
|
+
context.definitions.set(schema, refName);
|
|
393
|
+
// Convert the schema
|
|
394
|
+
const properties = {};
|
|
395
|
+
const required = [];
|
|
396
|
+
const shape = schema.shape;
|
|
397
|
+
for (const key in shape) {
|
|
398
|
+
const fieldSchema = shape[key];
|
|
399
|
+
const isOptional = fieldSchema instanceof z.ZodOptional ||
|
|
400
|
+
(fieldSchema instanceof z.ZodPipe && fieldSchema.def.out instanceof z.ZodOptional);
|
|
401
|
+
properties[key] = convertSchema(fieldSchema, context);
|
|
402
|
+
// In strict mode, ALL fields are required (optional ones are nullable)
|
|
403
|
+
// In non-strict mode, only non-optional fields are required
|
|
404
|
+
if (context.strict || !isOptional) {
|
|
405
|
+
required.push(key);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const schemaJson = {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties,
|
|
411
|
+
required,
|
|
412
|
+
};
|
|
413
|
+
if (context.strict) {
|
|
414
|
+
schemaJson.additionalProperties = false;
|
|
415
|
+
}
|
|
416
|
+
addMetadata(schemaJson, schema);
|
|
417
|
+
context.definitionSchemas.set(refName, schemaJson);
|
|
418
|
+
return { $ref: `#/$defs/${refName}` };
|
|
419
|
+
}
|
|
420
|
+
const properties = {};
|
|
421
|
+
const required = [];
|
|
422
|
+
const shape = schema.shape;
|
|
423
|
+
for (const key in shape) {
|
|
424
|
+
const fieldSchema = shape[key];
|
|
425
|
+
const isOptional = fieldSchema instanceof z.ZodOptional ||
|
|
426
|
+
(fieldSchema instanceof z.ZodPipe && fieldSchema.def.out instanceof z.ZodOptional);
|
|
427
|
+
properties[key] = convertSchema(fieldSchema, context);
|
|
428
|
+
// In strict mode, ALL fields are required (optional ones are nullable)
|
|
429
|
+
// In non-strict mode, only non-optional fields are required
|
|
430
|
+
if (context.strict || !isOptional) {
|
|
431
|
+
required.push(key);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const result = {
|
|
435
|
+
type: 'object',
|
|
436
|
+
properties,
|
|
437
|
+
required,
|
|
438
|
+
};
|
|
439
|
+
if (context.strict) {
|
|
440
|
+
result.additionalProperties = false;
|
|
441
|
+
}
|
|
442
|
+
return addMetadata(result, schema);
|
|
443
|
+
}
|
|
444
|
+
// Handle ZodArray
|
|
445
|
+
if (schema instanceof z.ZodArray) {
|
|
446
|
+
const items = convertSchema(schema.element, context);
|
|
447
|
+
const result = {
|
|
448
|
+
type: 'array',
|
|
449
|
+
items,
|
|
450
|
+
};
|
|
451
|
+
return addMetadata(result, schema);
|
|
452
|
+
}
|
|
453
|
+
// Handle ZodRecord - convert to array of {key, value} pairs in strict mode
|
|
454
|
+
if (schema instanceof z.ZodRecord) {
|
|
455
|
+
const keySchema = schema.keyType || z.string();
|
|
456
|
+
const valueSchema = schema.valueType;
|
|
457
|
+
if (context.strict) {
|
|
458
|
+
// In strict mode, records become arrays of {key, value} objects
|
|
459
|
+
const keyJson = convertSchema(keySchema, context);
|
|
460
|
+
const valueJson = convertSchema(valueSchema, context);
|
|
461
|
+
return {
|
|
462
|
+
type: 'array',
|
|
463
|
+
items: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
key: keyJson,
|
|
467
|
+
value: valueJson,
|
|
468
|
+
},
|
|
469
|
+
required: ['key', 'value'],
|
|
470
|
+
additionalProperties: false,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// In non-strict mode, use additionalProperties
|
|
476
|
+
const valueJson = convertSchema(valueSchema, context);
|
|
477
|
+
return {
|
|
478
|
+
type: 'object',
|
|
479
|
+
additionalProperties: valueJson,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Handle ZodUnion
|
|
484
|
+
if (schema instanceof z.ZodUnion) {
|
|
485
|
+
const anyOf = schema.options.map((option) => convertSchema(option, context));
|
|
486
|
+
return { anyOf };
|
|
487
|
+
}
|
|
488
|
+
// Handle ZodDiscriminatedUnion
|
|
489
|
+
if (schema instanceof z.ZodDiscriminatedUnion) {
|
|
490
|
+
const anyOf = schema.options.map((option) => convertSchema(option, context));
|
|
491
|
+
return { anyOf };
|
|
492
|
+
}
|
|
493
|
+
// Handle ZodIntersection - not supported in OpenAI strict mode, convert to anyOf
|
|
494
|
+
if (schema instanceof z.ZodIntersection) {
|
|
495
|
+
if (context.strict && context.format === 'openai') {
|
|
496
|
+
// Convert intersection to anyOf for OpenAI compatibility
|
|
497
|
+
return {
|
|
498
|
+
anyOf: [
|
|
499
|
+
convertSchema(schema.def.left, context),
|
|
500
|
+
convertSchema(schema.def.right, context),
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
// For non-strict, we could try to merge objects, but for now use anyOf
|
|
505
|
+
return {
|
|
506
|
+
anyOf: [
|
|
507
|
+
convertSchema(schema.def.left, context),
|
|
508
|
+
convertSchema(schema.def.right, context),
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// Handle ZodTuple
|
|
513
|
+
if (schema instanceof z.ZodTuple) {
|
|
514
|
+
// Tuples in JSON Schema can be represented as arrays with prefixItems
|
|
515
|
+
// For simplicity, we'll use an array type
|
|
516
|
+
const items = schema.def.items.map((item) => convertSchema(item, context));
|
|
517
|
+
// If all items are the same type, simplify to a single items schema
|
|
518
|
+
if (items.length > 0 && items.every((item) => JSON.stringify(item) === JSON.stringify(items[0]))) {
|
|
519
|
+
return {
|
|
520
|
+
type: 'array',
|
|
521
|
+
items: items[0],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Otherwise, use anyOf for the items
|
|
525
|
+
return {
|
|
526
|
+
type: 'array',
|
|
527
|
+
items: {
|
|
528
|
+
anyOf: items,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
// Handle ZodEnum
|
|
533
|
+
if (schema instanceof z.ZodEnum) {
|
|
534
|
+
const result = {
|
|
535
|
+
type: 'string',
|
|
536
|
+
enum: schema.options,
|
|
537
|
+
};
|
|
538
|
+
return addMetadata(result, schema);
|
|
539
|
+
}
|
|
540
|
+
// Handle ZodNativeEnum
|
|
541
|
+
if (schema._def?.typeName === 'ZodNativeEnum') {
|
|
542
|
+
const enumValues = Object.values(schema._def.values);
|
|
543
|
+
return {
|
|
544
|
+
type: typeof enumValues[0] === 'number' ? 'number' : 'string',
|
|
545
|
+
enum: enumValues,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// Handle ZodLiteral
|
|
549
|
+
if (schema instanceof z.ZodLiteral) {
|
|
550
|
+
const value = schema.value;
|
|
551
|
+
return {
|
|
552
|
+
type: typeof value,
|
|
553
|
+
const: value,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// Handle ZodDefault
|
|
557
|
+
if (schema instanceof z.ZodDefault) {
|
|
558
|
+
const innerJson = convertSchema(schema.def.innerType, context);
|
|
559
|
+
innerJson.default = schema.def.defaultValue;
|
|
560
|
+
return innerJson;
|
|
561
|
+
}
|
|
562
|
+
// Handle ZodLazy
|
|
563
|
+
if (schema instanceof z.ZodLazy) {
|
|
564
|
+
// Check if schema has an 'aid' in meta for custom definition name
|
|
565
|
+
const meta = schema.meta();
|
|
566
|
+
const refName = meta?.aid || `LazySchema${context.refCounter++}`;
|
|
567
|
+
context.definitions.set(schema, refName);
|
|
568
|
+
// Get the actual schema
|
|
569
|
+
const actualSchema = schema.def.getter();
|
|
570
|
+
const schemaJson = convertSchema(actualSchema, context);
|
|
571
|
+
context.definitionSchemas.set(refName, schemaJson);
|
|
572
|
+
return { $ref: `#/$defs/${refName}` };
|
|
573
|
+
}
|
|
574
|
+
// Handle ZodCodec
|
|
575
|
+
if (schema instanceof z.ZodCodec) {
|
|
576
|
+
// For codecs, use the input schema for JSON Schema generation
|
|
577
|
+
return convertSchema(schema.def.in, context);
|
|
578
|
+
}
|
|
579
|
+
// Handle ZodPipe (from preprocess)
|
|
580
|
+
if (schema instanceof z.ZodPipe) {
|
|
581
|
+
// For pipes, use the output schema if in strict mode (since strictify creates pipes for optional->nullable conversion)
|
|
582
|
+
// Otherwise use input schema
|
|
583
|
+
if (context.strict && schema.def.out instanceof z.ZodOptional) {
|
|
584
|
+
// This is from strictify - the output is optional, so we want nullable
|
|
585
|
+
const innerSchema = schema.def.out.unwrap();
|
|
586
|
+
const innerJson = convertSchema(innerSchema, context);
|
|
587
|
+
return makeNullable(innerJson);
|
|
588
|
+
}
|
|
589
|
+
return convertSchema(schema.def.in, context);
|
|
590
|
+
}
|
|
591
|
+
// Handle ZodTransform (from z.transform())
|
|
592
|
+
if (schema._def?.typeName === 'ZodTransform') {
|
|
593
|
+
// Use the input schema for JSON Schema generation
|
|
594
|
+
return convertSchema(schema._def.schema, context);
|
|
595
|
+
}
|
|
596
|
+
// Handle primitive types
|
|
597
|
+
if (schema instanceof z.ZodString) {
|
|
598
|
+
const result = { type: 'string' };
|
|
599
|
+
// Add string constraints if present
|
|
600
|
+
const checks = schema._def.checks || [];
|
|
601
|
+
for (const check of checks) {
|
|
602
|
+
if (check.kind === 'regex') {
|
|
603
|
+
result.pattern = check.regex.source;
|
|
604
|
+
}
|
|
605
|
+
else if (check.kind === 'email') {
|
|
606
|
+
result.format = 'email';
|
|
607
|
+
}
|
|
608
|
+
else if (check.kind === 'url') {
|
|
609
|
+
result.format = 'uri';
|
|
610
|
+
}
|
|
611
|
+
else if (check.kind === 'uuid') {
|
|
612
|
+
result.format = 'uuid';
|
|
613
|
+
}
|
|
614
|
+
else if (check.kind === 'datetime') {
|
|
615
|
+
result.format = 'date-time';
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return addMetadata(result, schema);
|
|
619
|
+
}
|
|
620
|
+
if (schema instanceof z.ZodNumber) {
|
|
621
|
+
const result = { type: 'number' };
|
|
622
|
+
// Add number constraints if present
|
|
623
|
+
const checks = schema._def.checks || [];
|
|
624
|
+
for (const check of checks) {
|
|
625
|
+
if (check.kind === 'min') {
|
|
626
|
+
if (check.inclusive) {
|
|
627
|
+
result.minimum = check.value;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
result.exclusiveMinimum = check.value;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (check.kind === 'max') {
|
|
634
|
+
if (check.inclusive) {
|
|
635
|
+
result.maximum = check.value;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
result.exclusiveMaximum = check.value;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else if (check.kind === 'multipleOf') {
|
|
642
|
+
result.multipleOf = check.value;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return addMetadata(result, schema);
|
|
646
|
+
}
|
|
647
|
+
if (schema instanceof z.ZodBoolean) {
|
|
648
|
+
return addMetadata({ type: 'boolean' }, schema);
|
|
649
|
+
}
|
|
650
|
+
if (schema instanceof z.ZodBigInt) {
|
|
651
|
+
return addMetadata({ type: 'integer' }, schema);
|
|
652
|
+
}
|
|
653
|
+
if (schema instanceof z.ZodDate) {
|
|
654
|
+
return addMetadata({ type: 'string', format: 'date-time' }, schema);
|
|
655
|
+
}
|
|
656
|
+
if (schema instanceof z.ZodNull) {
|
|
657
|
+
return { type: 'null' };
|
|
658
|
+
}
|
|
659
|
+
if (schema instanceof z.ZodUndefined) {
|
|
660
|
+
return { type: 'null' }; // Treat undefined as null in JSON
|
|
661
|
+
}
|
|
662
|
+
if (schema instanceof z.ZodAny) {
|
|
663
|
+
return {}; // Any type has no constraints
|
|
664
|
+
}
|
|
665
|
+
// Handle ZodEffects (transformations, refinements) - use inner schema
|
|
666
|
+
if (schema._def?.typeName === 'ZodEffects') {
|
|
667
|
+
return convertSchema(schema._def.schema, context);
|
|
668
|
+
}
|
|
669
|
+
// Fallback for unknown types
|
|
670
|
+
console.warn(`Unknown Zod schema type: ${schema.constructor.name}`);
|
|
671
|
+
return {};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Makes a JSON Schema nullable by adding null to the type
|
|
675
|
+
*/
|
|
676
|
+
function makeNullable(schema) {
|
|
677
|
+
if (schema.$ref) {
|
|
678
|
+
// If it's a reference, wrap in anyOf with null
|
|
679
|
+
return {
|
|
680
|
+
anyOf: [
|
|
681
|
+
{ $ref: schema.$ref },
|
|
682
|
+
{ type: 'null' },
|
|
683
|
+
],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (schema.type) {
|
|
687
|
+
// Add null to the type
|
|
688
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
689
|
+
if (!types.includes('null')) {
|
|
690
|
+
return {
|
|
691
|
+
...schema,
|
|
692
|
+
type: [...types, 'null'],
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return schema;
|
|
696
|
+
}
|
|
697
|
+
if (schema.anyOf) {
|
|
698
|
+
// Already has anyOf, add null option if not present
|
|
699
|
+
const hasNull = schema.anyOf.some((s) => s.type === 'null');
|
|
700
|
+
if (!hasNull) {
|
|
701
|
+
return {
|
|
702
|
+
...schema,
|
|
703
|
+
anyOf: [...schema.anyOf, { type: 'null' }],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return schema;
|
|
707
|
+
}
|
|
708
|
+
// For complex schemas without type, wrap in anyOf
|
|
709
|
+
return {
|
|
710
|
+
anyOf: [
|
|
711
|
+
schema,
|
|
712
|
+
{ type: 'null' },
|
|
713
|
+
],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Adds metadata (description, meta) from Zod schema to JSON Schema
|
|
718
|
+
*/
|
|
719
|
+
function addMetadata(jsonSchema, zodSchema) {
|
|
720
|
+
if (zodSchema.description) {
|
|
721
|
+
jsonSchema.description = zodSchema.description;
|
|
722
|
+
}
|
|
723
|
+
const meta = zodSchema.meta();
|
|
724
|
+
if (meta?.title) {
|
|
725
|
+
jsonSchema.title = meta.title;
|
|
726
|
+
}
|
|
727
|
+
return jsonSchema;
|
|
728
|
+
}
|
|
181
729
|
//# sourceMappingURL=schema.js.map
|