@aeye/core 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +51 -4
  2. package/aeye-core-schema.md +11 -7
  3. package/dist/common.d.ts +30 -5
  4. package/dist/common.d.ts.map +1 -1
  5. package/dist/common.js +134 -129
  6. package/dist/common.js.map +1 -1
  7. package/dist/delivery.d.ts +39 -0
  8. package/dist/delivery.d.ts.map +1 -0
  9. package/dist/delivery.js +127 -0
  10. package/dist/delivery.js.map +1 -0
  11. package/dist/errors.d.ts +60 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +84 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/prompt.d.ts +101 -9
  20. package/dist/prompt.d.ts.map +1 -1
  21. package/dist/prompt.js +163 -141
  22. package/dist/prompt.js.map +1 -1
  23. package/dist/schema/analyze.d.ts +94 -0
  24. package/dist/schema/analyze.d.ts.map +1 -0
  25. package/dist/schema/analyze.js +221 -0
  26. package/dist/schema/analyze.js.map +1 -0
  27. package/dist/schema/any-value.d.ts +32 -0
  28. package/dist/schema/any-value.d.ts.map +1 -0
  29. package/dist/schema/any-value.js +77 -0
  30. package/dist/schema/any-value.js.map +1 -0
  31. package/dist/schema/budget.d.ts +114 -0
  32. package/dist/schema/budget.d.ts.map +1 -0
  33. package/dist/schema/budget.js +223 -0
  34. package/dist/schema/budget.js.map +1 -0
  35. package/dist/schema/consistency.d.ts +27 -0
  36. package/dist/schema/consistency.d.ts.map +1 -0
  37. package/dist/schema/consistency.js +110 -0
  38. package/dist/schema/consistency.js.map +1 -0
  39. package/dist/schema/cycle-breaker.d.ts +14 -0
  40. package/dist/schema/cycle-breaker.d.ts.map +1 -0
  41. package/dist/schema/cycle-breaker.js +157 -0
  42. package/dist/schema/cycle-breaker.js.map +1 -0
  43. package/dist/schema/decode.d.ts +31 -0
  44. package/dist/schema/decode.d.ts.map +1 -0
  45. package/dist/schema/decode.js +207 -0
  46. package/dist/schema/decode.js.map +1 -0
  47. package/dist/schema/descriptor.d.ts +300 -0
  48. package/dist/schema/descriptor.d.ts.map +1 -0
  49. package/dist/schema/descriptor.js +15 -0
  50. package/dist/schema/descriptor.js.map +1 -0
  51. package/dist/schema/dialects/anthropic.d.ts +34 -0
  52. package/dist/schema/dialects/anthropic.d.ts.map +1 -0
  53. package/dist/schema/dialects/anthropic.js +77 -0
  54. package/dist/schema/dialects/anthropic.js.map +1 -0
  55. package/dist/schema/dialects/google.d.ts +47 -0
  56. package/dist/schema/dialects/google.d.ts.map +1 -0
  57. package/dist/schema/dialects/google.js +91 -0
  58. package/dist/schema/dialects/google.js.map +1 -0
  59. package/dist/schema/dialects/lenient.d.ts +4 -0
  60. package/dist/schema/dialects/lenient.d.ts.map +1 -0
  61. package/dist/schema/dialects/lenient.js +26 -0
  62. package/dist/schema/dialects/lenient.js.map +1 -0
  63. package/dist/schema/dialects/openai.d.ts +40 -0
  64. package/dist/schema/dialects/openai.d.ts.map +1 -0
  65. package/dist/schema/dialects/openai.js +81 -0
  66. package/dist/schema/dialects/openai.js.map +1 -0
  67. package/dist/schema/dialects/xai.d.ts +23 -0
  68. package/dist/schema/dialects/xai.d.ts.map +1 -0
  69. package/dist/schema/dialects/xai.js +53 -0
  70. package/dist/schema/dialects/xai.js.map +1 -0
  71. package/dist/schema/enum-values.d.ts +44 -0
  72. package/dist/schema/enum-values.d.ts.map +1 -0
  73. package/dist/schema/enum-values.js +56 -0
  74. package/dist/schema/enum-values.js.map +1 -0
  75. package/dist/schema/expressible.d.ts +59 -0
  76. package/dist/schema/expressible.d.ts.map +1 -0
  77. package/dist/schema/expressible.js +161 -0
  78. package/dist/schema/expressible.js.map +1 -0
  79. package/dist/schema/index.d.ts +20 -0
  80. package/dist/schema/index.d.ts.map +1 -0
  81. package/dist/schema/index.js +20 -0
  82. package/dist/schema/index.js.map +1 -0
  83. package/dist/schema/json-schema.d.ts +64 -0
  84. package/dist/schema/json-schema.d.ts.map +1 -0
  85. package/dist/schema/json-schema.js +2 -0
  86. package/dist/schema/json-schema.js.map +1 -0
  87. package/dist/schema/registry.d.ts +73 -0
  88. package/dist/schema/registry.d.ts.map +1 -0
  89. package/dist/schema/registry.js +135 -0
  90. package/dist/schema/registry.js.map +1 -0
  91. package/dist/schema/strictify.d.ts +19 -0
  92. package/dist/schema/strictify.d.ts.map +1 -0
  93. package/dist/schema/strictify.js +237 -0
  94. package/dist/schema/strictify.js.map +1 -0
  95. package/dist/schema/to-json-schema.d.ts +29 -0
  96. package/dist/schema/to-json-schema.d.ts.map +1 -0
  97. package/dist/schema/to-json-schema.js +691 -0
  98. package/dist/schema/to-json-schema.js.map +1 -0
  99. package/dist/schema/walk-key.d.ts +24 -0
  100. package/dist/schema/walk-key.d.ts.map +1 -0
  101. package/dist/schema/walk-key.js +33 -0
  102. package/dist/schema/walk-key.js.map +1 -0
  103. package/dist/to.js +1 -1
  104. package/dist/to.js.map +1 -1
  105. package/dist/tool.d.ts +32 -3
  106. package/dist/tool.d.ts.map +1 -1
  107. package/dist/tool.js +41 -0
  108. package/dist/tool.js.map +1 -1
  109. package/dist/types.d.ts +265 -8
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +4 -1
  112. package/dist/types.js.map +1 -1
  113. package/package.json +72 -72
  114. package/dist/schema.d.ts +0 -840
  115. package/dist/schema.d.ts.map +0 -1
  116. package/dist/schema.js +0 -2478
  117. package/dist/schema.js.map +0 -1
@@ -0,0 +1,691 @@
1
+ import z from 'zod';
2
+ import { resolveDescriptor } from './registry';
3
+ import { buildAnyValueSchema, usesSharedAnyDefinition } from './any-value';
4
+ import { buildCycleBreakerSchema } from './cycle-breaker';
5
+ import { describeOversizeEnum, enumEmittedValues, literalEmittedValues } from './enum-values';
6
+ /**
7
+ * Converts a Zod schema to JSON Schema with support for different provider dialects.
8
+ *
9
+ * Accepts either a boolean (legacy: true = OPENAI_STRICT, false = LENIENT),
10
+ * an options object `{ strict, format? }`, or a `FormatDescriptor` directly.
11
+ *
12
+ * @param schema - The Zod schema to convert
13
+ * @param options - Configuration options for the conversion
14
+ * @returns JSON Schema object compatible with the resolved dialect
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const schema = z.object({
19
+ * name: z.string(),
20
+ * age: z.number().optional(),
21
+ * });
22
+ *
23
+ * // For OpenAI strict mode
24
+ * const jsonSchema = toJSONSchema(schema, { strict: true, format: 'openai' });
25
+ *
26
+ * // For Anthropic strict mode
27
+ * const anthropic = toJSONSchema(schema, ANTHROPIC_STRICT);
28
+ * ```
29
+ */
30
+ export function toJSONSchema(schema, options) {
31
+ const descriptor = resolveDescriptor(options);
32
+ const context = {
33
+ root: schema,
34
+ descriptor,
35
+ definitions: new Map(),
36
+ definitionSchemas: new Map(),
37
+ refCounter: 0,
38
+ path: [],
39
+ inProgress: new Set(),
40
+ };
41
+ const result = convert(schema, context);
42
+ // Add definitions if any were created
43
+ if (context.definitionSchemas.size > 0) {
44
+ result.$defs = Object.fromEntries(context.definitionSchemas);
45
+ }
46
+ return result;
47
+ }
48
+ /**
49
+ * Main recursive conversion function
50
+ */
51
+ function convert(schema, context) {
52
+ // For lazy schemas, we need to check by aid first to handle .describe()/.optional() wrappers
53
+ // that create new objects but share the same lazy getter
54
+ let cacheKey = schema;
55
+ let unwrappedSchema = schema;
56
+ // `z.any()` / `z.unknown()` under an INLINE any-encoding (`flat`,
57
+ // `unconstrained`) must never travel through the definition cache: a second
58
+ // use site of the SAME schema instance — common when a codegen layer hands
59
+ // out one shared "any" node — would otherwise be emitted as
60
+ // `$ref: '#/$defs/__schemaN'`, resurrecting the `$defs` entry the inline
61
+ // encoding exists to avoid. Bypassing the cache re-inlines the (tiny,
62
+ // keyword-free) shape at each site instead.
63
+ const bypassCache = !usesSharedAnyDefinition(context.descriptor)
64
+ && (schema instanceof z.ZodAny || schema instanceof z.ZodUnknown);
65
+ // Check if this is a lazy schema and extract metadata early
66
+ if (bypassCache) {
67
+ // fall through to conversion with no cache read
68
+ }
69
+ else if (schema instanceof z.ZodLazy) {
70
+ // Check cache FIRST before evaluating getter to prevent infinite recursion
71
+ const [cachedJs, cachedId] = context.definitions.get(schema) || [];
72
+ if (cachedJs && cachedId) {
73
+ return emitCachedRef(schema, cachedJs, cachedId, context);
74
+ }
75
+ const metadata = (schema instanceof z.ZodType) ? schema.meta() : null;
76
+ const aid = metadata?.aid;
77
+ // If there's an aid, check if we've already seen this aid
78
+ if (aid) {
79
+ for (const [cachedSchema, [js, jsId]] of context.definitions.entries()) {
80
+ if (cachedSchema instanceof z.ZodType) {
81
+ const cachedMeta = cachedSchema.meta();
82
+ if (cachedMeta?.aid === aid && jsId && js) {
83
+ // Found a cached version with the same aid - use it
84
+ return emitCachedRef(schema, js, jsId, context);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ unwrappedSchema = schema.def.getter();
90
+ }
91
+ else {
92
+ // Check cache by object identity for non-lazy schemas
93
+ const [js, jsId] = context.definitions.get(schema) || [];
94
+ if (jsId && js) {
95
+ return emitCachedRef(schema, js, jsId, context);
96
+ }
97
+ }
98
+ // Capture metadata from the original schema
99
+ const metadata = {};
100
+ if (schema instanceof z.ZodType) {
101
+ Object.assign(metadata, schema.meta() || {});
102
+ if (!metadata.description && schema.description) {
103
+ metadata.description = schema.description;
104
+ }
105
+ }
106
+ // If the schema has an 'aid' or 'id' in meta, promote it to a definition.
107
+ // A cache-bypassed "any" node is never promoted — naming a schema that
108
+ // asserts nothing buys nothing, and the `$defs` entry is precisely what the
109
+ // inline encoding is avoiding. A descriptor that forbids `$defs` outright
110
+ // (`allowDefsRef: false` — Google strict) is never promoted to either: the id
111
+ // is still computed, because the cycle-breaker's description names it, but
112
+ // the definition is inlined at each use site, which is the only spelling that
113
+ // dialect has for a shared node.
114
+ const id = (metadata.aid ? String(metadata.aid) : 0) || metadata.id || `__schema${context.refCounter++}`;
115
+ const save = !bypassCache
116
+ && !!(metadata.aid || metadata.id)
117
+ && context.root !== schema
118
+ && context.descriptor.allowDefsRef;
119
+ // A schema target - will hold either the converted schema or a $ref
120
+ const target = {};
121
+ // Before converting, register this schema to handle recursion
122
+ if (!bypassCache) {
123
+ context.definitions.set(cacheKey, [target, id]);
124
+ }
125
+ context.inProgress.add(id);
126
+ // Convert the unwrapped schema
127
+ let result;
128
+ try {
129
+ result = convertSchema(unwrappedSchema, context);
130
+ }
131
+ finally {
132
+ context.inProgress.delete(id);
133
+ }
134
+ // A description the CONVERTER produced describes the EMITTED node, not the
135
+ // source schema — today that is only the `maxEnumValues` widening, whose
136
+ // description IS the value list that replaced the `enum` keyword. The source's
137
+ // own `.describe()` must not silently replace it, or a model handed a widened
138
+ // `{type:'string'}` is told nothing about which strings are legal. Keep both,
139
+ // converter note last.
140
+ const emittedNote = result.description;
141
+ Object.assign(result, metadata);
142
+ if (emittedNote !== undefined && metadata.description !== undefined && metadata.description !== emittedNote) {
143
+ result.description = `${metadata.description} ${emittedNote}`;
144
+ }
145
+ delete result.aid;
146
+ // Promote it because user requested it or it's recursive
147
+ if (save || context.definitionSchemas.has(id)) {
148
+ context.definitionSchemas.set(id, result);
149
+ target.$ref = `#/$defs/${id}`;
150
+ }
151
+ else {
152
+ // Inline schema
153
+ Object.assign(target, result);
154
+ }
155
+ return target;
156
+ }
157
+ /**
158
+ * Emit a `$ref` (or, for descriptors that don't support recursion, an inline
159
+ * shape-aware placeholder) when the converter re-encounters a schema it's
160
+ * already started or finished converting.
161
+ *
162
+ * Three cases:
163
+ * 1. Re-encounter while still on the conversion stack (`inProgress.has(id)`)
164
+ * AND the descriptor cannot express the `$ref` this back-edge would need —
165
+ * emit a shape-aware placeholder via `buildCycleBreakerSchema()` (number
166
+ * stays number, array stays array, etc.) with a description naming the
167
+ * `$defs` entry it would have referenced. This breaks the cycle inline so
168
+ * providers like Anthropic don't reject the schema.
169
+ *
170
+ * "Cannot express" is TWO flags, not one, because a back-edge is only ever
171
+ * spelled one of two ways: `{$ref: '#'}` at the root, or `{$ref:
172
+ * '#/$defs/X'}` anywhere else. `!supportsRecursion` rules out both.
173
+ * `!allowDefsRef` rules out the second — so a NON-ROOT cycle under a
174
+ * descriptor that forbids `$defs` (Google strict is exactly that:
175
+ * `supportsRecursion: true`, `allowRootRef: true`, `allowDefsRef: false`)
176
+ * must take the same bounded placeholder. Checking only `supportsRecursion`
177
+ * silently emitted the `$defs`/`$ref` pair the descriptor declares it cannot
178
+ * send — a schema the provider answers with HTTP 400.
179
+ * 2. Re-encounter at root with `allowRootRef` — emit `{$ref: '#'}`.
180
+ * 3. Otherwise — emit `{$ref: '#/$defs/<id>'}`, promoting the original
181
+ * target to a `$defs` entry on first share.
182
+ */
183
+ function emitCachedRef(schema, cachedJs, cachedId, context) {
184
+ const descriptor = context.descriptor;
185
+ const isCycle = context.inProgress.has(cachedId);
186
+ const isRoot = schema === context.root;
187
+ // The spelling this back-edge would need, and whether the descriptor has it.
188
+ const backEdgeExpressible = isRoot ? descriptor.allowRootRef : descriptor.allowDefsRef;
189
+ if (isCycle && (!descriptor.supportsRecursion || !backEdgeExpressible)) {
190
+ // Replace the back-edge with a placeholder whose top-level shape matches
191
+ // the recursive zod schema. The original `cachedJs` target keeps filling
192
+ // in normally — once conversion completes the `$defs` entry exists; we
193
+ // just don't reference it from here, since doing so would form the cycle
194
+ // the provider rejects.
195
+ return buildCycleBreakerSchema(schema, cachedId, descriptor);
196
+ }
197
+ if (isRoot && descriptor.allowRootRef) {
198
+ return { $ref: `#` };
199
+ }
200
+ // A non-cyclic re-encounter under a descriptor with no `$defs`: INLINE the
201
+ // already-converted shape instead of naming it. There is no third spelling in
202
+ // that dialect — a shared node is either repeated or it is a `$ref` the
203
+ // provider rejects. Safe and finite: `!isCycle` means this node's conversion
204
+ // already completed, and any cycle BELOW it was replaced by the bounded
205
+ // placeholder above, so the inlined value is a finite tree. The cost is
206
+ // duplication in the emitted schema, which is what that dialect charges for
207
+ // shared structure.
208
+ //
209
+ // On a schema the dialect can actually SEND that cost is negligible (measured
210
+ // on the cases in `schema-google-defs.test.ts`: 497→435, 627→565, 280→322
211
+ // bytes — inlining a leaf is often smaller than naming it). It is only large
212
+ // on a heavily-shared recursive DAG — a gin `buildSchemas().Expr` renders
213
+ // 44 KB with `$defs` and 1.36 MB without — and such a schema carries 1646
214
+ // `anyOf`s, so `canExpress` already answers `false` for it and no provider
215
+ // path reaches here. Paying size on a schema that must not be sent anyway is
216
+ // the right side of the trade against emitting `$defs` the descriptor
217
+ // declares it cannot send.
218
+ if (!descriptor.allowDefsRef) {
219
+ return { ...cachedJs };
220
+ }
221
+ if (!context.definitionSchemas.has(cachedId)) {
222
+ context.definitionSchemas.set(cachedId, { ...cachedJs });
223
+ for (const prop in cachedJs) {
224
+ delete cachedJs[prop];
225
+ }
226
+ cachedJs.$ref = `#/$defs/${cachedId}`;
227
+ }
228
+ return { $ref: `#/$defs/${cachedId}` };
229
+ }
230
+ /**
231
+ * Main conversion function
232
+ */
233
+ function convertSchema(schema, context) {
234
+ const descriptor = context.descriptor;
235
+ // TODO: Map, Set, File, ReadOnly, Nan, Catch, Prefault, NonOptional, Transform, Function, Promise, Custom
236
+ // Handle ZodOptional
237
+ if (schema instanceof z.ZodOptional) {
238
+ const innerSchema = schema.unwrap();
239
+ const innerJson = convert(innerSchema, context);
240
+ if (descriptor.optionalAsNullable) {
241
+ // OpenAI-strict-style: optional fields become nullable on the wire.
242
+ return makeNullable(innerJson, descriptor);
243
+ }
244
+ else {
245
+ // Standard JSON Schema: optional fields are simply not in `required[]`.
246
+ return innerJson;
247
+ }
248
+ }
249
+ // Handle ZodNullable
250
+ if (schema instanceof z.ZodNullable) {
251
+ const innerSchema = schema.unwrap();
252
+ const innerJson = convert(innerSchema, context);
253
+ return makeNullable(innerJson, descriptor);
254
+ }
255
+ // Handle ZodObject
256
+ if (schema instanceof z.ZodObject) {
257
+ const properties = {};
258
+ const required = [];
259
+ const shape = schema.shape;
260
+ for (const key in shape) {
261
+ const fieldSchema = shape[key];
262
+ const isFieldRequired = descriptor.objectAllFieldsRequired || !isOptional(fieldSchema);
263
+ properties[key] = convert(fieldSchema, context);
264
+ if (isFieldRequired) {
265
+ required.push(key);
266
+ }
267
+ }
268
+ const result = {
269
+ type: 'object',
270
+ properties,
271
+ required,
272
+ };
273
+ if (descriptor.objectClosedByDefault || schema.def.catchall?._zod.def.type === "never") {
274
+ result.additionalProperties = false;
275
+ }
276
+ else if (schema.def.catchall) {
277
+ result.additionalProperties = convert(schema.def.catchall, context);
278
+ }
279
+ // Gemini 2.0 strict requires `propertyOrdering` so the model emits keys
280
+ // in a deterministic order. Other dialects ignore this hint, so emitting
281
+ // it under non-Google descriptors is harmless — but we gate it on the
282
+ // descriptor flag to keep the JSON Schema minimal everywhere else.
283
+ if (descriptor.emitPropertyOrdering) {
284
+ result.propertyOrdering = Object.keys(properties);
285
+ }
286
+ return result;
287
+ }
288
+ // Handle ZodArray
289
+ if (schema instanceof z.ZodArray) {
290
+ const result = {
291
+ type: 'array',
292
+ items: convert(schema.element, context),
293
+ };
294
+ if (descriptor.allowMinMaxItems) {
295
+ const { minimum, maximum } = schema._zod.bag;
296
+ if (typeof minimum === 'number') {
297
+ result.minItems = minimum;
298
+ }
299
+ if (typeof maximum === 'number') {
300
+ result.maxItems = maximum;
301
+ }
302
+ }
303
+ return result;
304
+ }
305
+ // Handle ZodRecord
306
+ if (schema instanceof z.ZodRecord) {
307
+ if (descriptor.recordEncoding === 'array-of-pairs') {
308
+ // Strict-mode workaround: records become arrays of {key, value} objects.
309
+ return {
310
+ type: 'array',
311
+ items: {
312
+ type: 'object',
313
+ properties: {
314
+ key: convert(schema.keyType, context),
315
+ value: convert(schema.valueType, context),
316
+ },
317
+ required: ['key', 'value'],
318
+ additionalProperties: false,
319
+ },
320
+ };
321
+ }
322
+ // Standard: open record. `propertyNames` only when the key says more than
323
+ // "a string": several grammar-compiling hosts reject the keyword outright
324
+ // (`Unimplemented keys: ["propertyNames"]`, DeepInfra and Cloudflare via
325
+ // OpenRouter, 2026-11-04), and for a plain string key it constrains nothing.
326
+ const propertyNames = convert(schema.keyType, context);
327
+ const additionalProperties = convert(schema.valueType, context);
328
+ return Object.keys(propertyNames).some((key) => key !== 'type' && key !== 'description')
329
+ ? { type: 'object', propertyNames, additionalProperties }
330
+ : { type: 'object', additionalProperties };
331
+ }
332
+ // Handle ZodUnion
333
+ if (schema instanceof z.ZodUnion) {
334
+ const anyOf = schema.options.map(option => convert(option, context));
335
+ return { anyOf };
336
+ }
337
+ // Handle ZodDiscriminatedUnion
338
+ if (schema instanceof z.ZodDiscriminatedUnion) {
339
+ const anyOf = schema.options.map(option => convert(option, context));
340
+ return { anyOf };
341
+ }
342
+ // Handle ZodIntersection
343
+ if (schema instanceof z.ZodIntersection) {
344
+ const left = convert(schema.def.left, context);
345
+ const right = convert(schema.def.right, context);
346
+ const allOf = [
347
+ ...(left.allOf && Object.keys(left).length === 1 ? left.allOf : [left]),
348
+ ...(right.allOf && Object.keys(right).length === 1 ? right.allOf : [right]),
349
+ ];
350
+ // OpenAI strict bans allOf in some shapes; collapse to anyOf when the
351
+ // descriptor disallows it.
352
+ return descriptor.allowAllOf ? { allOf } : { anyOf: allOf };
353
+ }
354
+ // Handle ZodTuple
355
+ if (schema instanceof z.ZodTuple) {
356
+ const items = schema.def.items.map(item => convert(item, context));
357
+ const rest = schema.def.rest ? convert(schema.def.rest, context) : undefined;
358
+ // Object-numeric-keys encoding: per-position type preserved as object
359
+ // properties. Variadic rests can't be represented this way — fall back to
360
+ // a homogeneous array of (items ∪ rest) for that rare case.
361
+ if (descriptor.tupleEncoding === 'object-numeric-keys' && !rest) {
362
+ const properties = {};
363
+ const required = [];
364
+ for (let i = 0; i < items.length; i++) {
365
+ const k = String(i);
366
+ properties[k] = isOptional(schema.def.items[i]) ? makeNullable(items[i], descriptor) : items[i];
367
+ required.push(k);
368
+ }
369
+ return {
370
+ type: 'object',
371
+ properties,
372
+ required,
373
+ additionalProperties: false,
374
+ };
375
+ }
376
+ const result = { type: 'array' };
377
+ if (descriptor.tupleEncoding === 'object-numeric-keys' && rest) {
378
+ // Strict + rest fallback: every position fits one of the declared
379
+ // types (positional info is lost — there's no way to express a
380
+ // mixed-prefix-plus-rest array in strict-object form).
381
+ items.push(rest);
382
+ }
383
+ // If all items are the same type, simplify to a single items schema
384
+ if (items.length > 0 && items.every((item) => JSON.stringify(item) === JSON.stringify(items[0]))) {
385
+ result.items = items[0];
386
+ }
387
+ else if (descriptor.tupleEncoding === 'prefix-items') {
388
+ result.prefixItems = items;
389
+ }
390
+ else {
391
+ // items-union or strict-mode-with-rest fallback: collapse to anyOf
392
+ result.items = { anyOf: items };
393
+ }
394
+ if (descriptor.tupleEncoding === 'prefix-items' && rest) {
395
+ result.additionalItems = rest;
396
+ }
397
+ if (!rest) {
398
+ let minItems = items.length;
399
+ while (minItems > 0 && isOptional(schema.def.items[minItems - 1])) {
400
+ minItems--;
401
+ }
402
+ result.minItems = minItems;
403
+ result.maxItems = items.length;
404
+ }
405
+ if (descriptor.allowMinMaxItems) {
406
+ const { minimum, maximum } = schema._zod.bag;
407
+ if (typeof minimum === 'number') {
408
+ result.minItems = minimum;
409
+ }
410
+ if (typeof maximum === 'number') {
411
+ result.maxItems = maximum;
412
+ }
413
+ }
414
+ return result;
415
+ }
416
+ // Handle ZodEnum
417
+ if (schema instanceof z.ZodEnum) {
418
+ const values = enumEmittedValues(schema);
419
+ // Over the dialect's cap the `enum` keyword is DROPPED and the node widens
420
+ // to its plain scalar type, with every value listed in the description —
421
+ // see `describeOversizeEnum` for why widening beats truncating.
422
+ const oversize = describeOversizeEnum(values, descriptor);
423
+ return {
424
+ type: values.every((v) => typeof v === 'number')
425
+ ? 'number'
426
+ : values.every((v) => typeof v === 'string')
427
+ ? 'string'
428
+ : undefined,
429
+ ...(oversize === undefined ? { enum: values } : { description: oversize }),
430
+ };
431
+ }
432
+ // Handle ZodLiteral
433
+ if (schema instanceof z.ZodLiteral) {
434
+ const values = literalEmittedValues(schema);
435
+ const types = Array.from(new Set(values.map(v => v === null ? 'null' : typeof v)));
436
+ // A multi-value literal emits the SAME `enum` keyword as ZodEnum, so the
437
+ // dialect cap applies identically — the provider sees no difference. The
438
+ // single-value form emits `const`, which no cap can be exceeded by.
439
+ const oversize = values.length === 1 ? undefined : describeOversizeEnum(values, descriptor);
440
+ return {
441
+ ...(types.length === 1 ? { type: types[0] } : {}),
442
+ ...(values.length === 1
443
+ ? { const: values[0] }
444
+ : oversize === undefined ? { enum: values } : { description: oversize }),
445
+ };
446
+ }
447
+ // Handle ZodDefault
448
+ if (schema instanceof z.ZodDefault) {
449
+ const innerJson = convert(schema.def.innerType, context);
450
+ innerJson.default = JSON.parse(JSON.stringify(schema.def.defaultValue));
451
+ return innerJson;
452
+ }
453
+ // Handle ZodLazy - don't unwrap here, let convert() handle caching
454
+ if (schema instanceof z.ZodLazy) {
455
+ throw new Error('ZodLazy should be handled in convert(), not convertSchema()');
456
+ }
457
+ // Handle ZodCodec
458
+ if (schema instanceof z.ZodCodec) {
459
+ return convert(schema.def.in, context);
460
+ }
461
+ // Handle ZodPipe (from preprocess)
462
+ if (schema instanceof z.ZodPipe) {
463
+ const innerType = schema.def.in._zod.def.type === "transform" ? schema.def.out : schema.def.in;
464
+ return convert(innerType, context);
465
+ }
466
+ // Handle primitive types
467
+ if (schema instanceof z.ZodString) {
468
+ const result = { type: 'string' };
469
+ const { minimum, maximum, format, patterns, contentEncoding } = schema._zod.bag;
470
+ // Format: emit only when allowed by the descriptor.
471
+ if (typeof format === 'string' && emitsFormat(format, descriptor)) {
472
+ result.format = format;
473
+ }
474
+ if (descriptor.allowMinMaxLength) {
475
+ if (typeof minimum === 'number') {
476
+ result.minLength = minimum;
477
+ }
478
+ if (typeof maximum === 'number') {
479
+ result.maxLength = maximum;
480
+ }
481
+ }
482
+ if (typeof contentEncoding === 'string' && descriptor.allowMinMaxLength) {
483
+ // contentEncoding piggybacks on the lenient gate (it's not a strict-mode-safe field)
484
+ result.contentEncoding = contentEncoding;
485
+ }
486
+ if (patterns && descriptor.allowPattern) {
487
+ if (patterns.size === 1 || !descriptor.allowMultiplePatterns) {
488
+ result.pattern = Array.from(patterns)[0].source;
489
+ }
490
+ else {
491
+ result.allOf = Array.from(patterns).map((regex) => ({
492
+ type: 'string',
493
+ pattern: regex.source,
494
+ }));
495
+ }
496
+ }
497
+ return result;
498
+ }
499
+ if (schema instanceof z.ZodNumber) {
500
+ const result = { type: 'number' };
501
+ const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
502
+ if (typeof format === 'string' && format.includes("int")) {
503
+ result.type = 'integer';
504
+ }
505
+ if (descriptor.allowMinimumMaximum) {
506
+ if (typeof exclusiveMinimum === 'number') {
507
+ result.exclusiveMinimum = exclusiveMinimum;
508
+ }
509
+ else if (typeof minimum === 'number') {
510
+ result.minimum = minimum;
511
+ }
512
+ if (typeof exclusiveMaximum === 'number') {
513
+ result.exclusiveMaximum = exclusiveMaximum;
514
+ }
515
+ else if (typeof maximum === 'number') {
516
+ result.maximum = maximum;
517
+ }
518
+ if (typeof multipleOf === 'number') {
519
+ result.multipleOf = multipleOf;
520
+ }
521
+ }
522
+ return result;
523
+ }
524
+ if (schema instanceof z.ZodBoolean || schema instanceof z.ZodSuccess) {
525
+ return { type: 'boolean' };
526
+ }
527
+ if (schema instanceof z.ZodBigInt) {
528
+ return { type: 'integer' };
529
+ }
530
+ if (schema instanceof z.ZodDate || schema instanceof z.ZodISODateTime) {
531
+ return stringWithFormat('date-time', descriptor);
532
+ }
533
+ if (schema instanceof z.ZodISODate) {
534
+ return stringWithFormat('date', descriptor);
535
+ }
536
+ if (schema instanceof z.ZodISOTime) {
537
+ return stringWithFormat('time', descriptor);
538
+ }
539
+ if (schema instanceof z.ZodISODuration) {
540
+ return stringWithFormat('duration', descriptor);
541
+ }
542
+ if (schema instanceof z.ZodEmail) {
543
+ return stringWithFormat('email', descriptor);
544
+ }
545
+ if (schema instanceof z.ZodIPv4) {
546
+ return stringWithFormat('ipv4', descriptor);
547
+ }
548
+ if (schema instanceof z.ZodIPv6) {
549
+ return stringWithFormat('ipv6', descriptor);
550
+ }
551
+ if (schema instanceof z.ZodUUID) {
552
+ return stringWithFormat('uuid', descriptor);
553
+ }
554
+ if (schema instanceof z.ZodNull) {
555
+ return { type: 'null' };
556
+ }
557
+ if (schema instanceof z.ZodTemplateLiteral) {
558
+ return { type: 'string', pattern: schema._zod.pattern?.source };
559
+ }
560
+ if (schema instanceof z.ZodUndefined) {
561
+ return { type: 'null' }; // Treat undefined as null in JSON
562
+ }
563
+ if (schema instanceof z.ZodAny || schema instanceof z.ZodUnknown) {
564
+ // Bare `{}` would satisfy the meaning of "any" but OpenAI's strict
565
+ // structured-output mode rejects schemas without a `type` key. For
566
+ // descriptors with a recursive Any encoding, promote to a single shared
567
+ // `$defs/Any` definition that covers every JSON value and return a
568
+ // `$ref` — sharing keeps the output compact when `z.any()` appears many
569
+ // times inside a big union. For descriptors with `flat` Any encoding
570
+ // (e.g. Anthropic, which rejects recursive `$defs` graphs) or `unconstrained`
571
+ // (Google, which rejects both `anyOf` and named `$defs`), inline the
572
+ // permissive shape directly so the schema stays acyclic and `$defs`-free.
573
+ if (!usesSharedAnyDefinition(descriptor)) {
574
+ return buildAnyValueSchema(descriptor);
575
+ }
576
+ const id = 'Any';
577
+ if (!context.definitionSchemas.has(id)) {
578
+ // Seed a placeholder BEFORE building the body so recursive
579
+ // `$ref: '#/$defs/Any'` references don't infinite-loop during
580
+ // construction.
581
+ context.definitionSchemas.set(id, {});
582
+ context.definitionSchemas.set(id, buildAnyValueSchema(descriptor));
583
+ }
584
+ return { $ref: `#/$defs/${id}` };
585
+ }
586
+ if (schema instanceof z.ZodTransform) {
587
+ return {}; // Transforms are not represented in JSON Schema
588
+ }
589
+ // Fallback for unknown types
590
+ console.warn(`Unknown Zod schema type: ${schema.constructor.name}`);
591
+ return {};
592
+ }
593
+ /**
594
+ * Makes a JSON Schema nullable: `z.nullable()` under every dialect, and
595
+ * `z.optional()` under one that spells optional as nullable (OpenAI strict).
596
+ *
597
+ * The null joins as an `anyOf` branch wherever the dialect has `anyOf`. A
598
+ * `["T", "null"]` type array is the spelling of last resort: o3's strict
599
+ * function validator rejects it ("schema must have a 'type' key", measured
600
+ * 2026-11-05) where the `anyOf` form passes.
601
+ */
602
+ function makeNullable(schema, descriptor) {
603
+ // The empty schema already admits null
604
+ if (Object.keys(schema).length === 0) {
605
+ return schema;
606
+ }
607
+ if (schema.$ref) {
608
+ return {
609
+ anyOf: [
610
+ { $ref: schema.$ref },
611
+ { type: 'null' },
612
+ ],
613
+ };
614
+ }
615
+ if (schema.type) {
616
+ const hasTypeSpecificProps = schema.items !== undefined ||
617
+ schema.properties !== undefined ||
618
+ schema.additionalProperties !== undefined ||
619
+ schema.patternProperties !== undefined ||
620
+ schema.minItems !== undefined ||
621
+ schema.maxItems !== undefined ||
622
+ schema.minProperties !== undefined ||
623
+ schema.maxProperties !== undefined;
624
+ if (hasTypeSpecificProps || (descriptor.allowAnyOf && !Array.isArray(schema.type) && schema.type !== 'null')) {
625
+ const { type, description, ...rest } = schema;
626
+ const baseType = Array.isArray(type) ? type[0] : type;
627
+ return {
628
+ anyOf: [
629
+ { type: baseType, ...rest },
630
+ { type: 'null' },
631
+ ],
632
+ ...(description ? { description } : {}),
633
+ };
634
+ }
635
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
636
+ if (!types.includes('null')) {
637
+ return {
638
+ ...schema,
639
+ type: [...types, 'null'],
640
+ };
641
+ }
642
+ return schema;
643
+ }
644
+ if (schema.anyOf) {
645
+ const hasNull = schema.anyOf.some((s) => s.type === 'null');
646
+ if (!hasNull) {
647
+ return {
648
+ ...schema,
649
+ anyOf: [...schema.anyOf, { type: 'null' }],
650
+ };
651
+ }
652
+ return schema;
653
+ }
654
+ return {
655
+ anyOf: [
656
+ schema,
657
+ { type: 'null' },
658
+ ],
659
+ };
660
+ }
661
+ function isOptional(schema) {
662
+ return schema._zod.optin !== undefined;
663
+ }
664
+ /**
665
+ * The `format` values JSON Schema (2020-12) defines, which is what `'all'`
666
+ * passes through. Zod's own check names (`regex` for `.regex()`, `cuid`,
667
+ * `starts_with`, …) share the field but are not formats: a grammar-compiling
668
+ * host rejects them (`Unknown format: regex`, DeepInfra via OpenRouter,
669
+ * 2026-11-04), so they are never emitted.
670
+ */
671
+ const JSON_SCHEMA_FORMATS = new Set([
672
+ 'date-time', 'date', 'time', 'duration', 'email', 'idn-email', 'hostname', 'idn-hostname',
673
+ 'ipv4', 'ipv6', 'uri', 'uri-reference', 'iri', 'iri-reference', 'uuid', 'uri-template',
674
+ 'json-pointer', 'relative-json-pointer',
675
+ ]);
676
+ /**
677
+ * Whether `format` may be emitted under the descriptor's whitelist.
678
+ */
679
+ function emitsFormat(format, descriptor) {
680
+ const formats = descriptor.supportedStringFormats;
681
+ return formats === 'all' ? JSON_SCHEMA_FORMATS.has(format) : formats.has(format);
682
+ }
683
+ /**
684
+ * Emit a `{ type: 'string', format: ... }` JSON Schema, dropping the format
685
+ * when the descriptor's whitelist excludes it. The string type is always
686
+ * preserved; only the format hint is gated.
687
+ */
688
+ function stringWithFormat(format, descriptor) {
689
+ return emitsFormat(format, descriptor) ? { type: 'string', format } : { type: 'string' };
690
+ }
691
+ //# sourceMappingURL=to-json-schema.js.map