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