@typia/utils 12.0.0-dev.20260307-2 → 12.0.0-dev.20260310

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 (103) hide show
  1. package/README.md +2 -2
  2. package/lib/http/internal/HttpLlmApplicationComposer.js +1 -0
  3. package/lib/http/internal/HttpLlmApplicationComposer.js.map +1 -1
  4. package/lib/http/internal/HttpLlmApplicationComposer.mjs +1 -0
  5. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  6. package/lib/utils/LlmJson.d.ts +3 -3
  7. package/lib/utils/LlmJson.js +2 -2
  8. package/lib/utils/LlmJson.js.map +1 -1
  9. package/lib/utils/LlmJson.mjs +2 -2
  10. package/lib/utils/LlmJson.mjs.map +1 -1
  11. package/lib/utils/internal/coerceLlmArguments.js +17 -1
  12. package/lib/utils/internal/coerceLlmArguments.js.map +1 -1
  13. package/lib/utils/internal/coerceLlmArguments.mjs +17 -1
  14. package/lib/utils/internal/coerceLlmArguments.mjs.map +1 -1
  15. package/lib/utils/internal/parseLenientJson.js +236 -96
  16. package/lib/utils/internal/parseLenientJson.js.map +1 -1
  17. package/lib/utils/internal/parseLenientJson.mjs +236 -96
  18. package/lib/utils/internal/parseLenientJson.mjs.map +1 -1
  19. package/lib/utils/internal/stringifyValidationFailure.js +17 -15
  20. package/lib/utils/internal/stringifyValidationFailure.js.map +1 -1
  21. package/lib/utils/internal/stringifyValidationFailure.mjs +17 -15
  22. package/lib/utils/internal/stringifyValidationFailure.mjs.map +1 -1
  23. package/package.json +2 -2
  24. package/src/converters/LlmSchemaConverter.ts +647 -647
  25. package/src/converters/OpenApiConverter.ts +285 -285
  26. package/src/converters/index.ts +5 -5
  27. package/src/converters/internal/LlmDescriptionInverter.ts +178 -178
  28. package/src/converters/internal/LlmParametersComposer.ts +52 -52
  29. package/src/converters/internal/OpenApiConstraintShifter.ts +154 -154
  30. package/src/converters/internal/OpenApiExclusiveEmender.ts +46 -46
  31. package/src/converters/internal/OpenApiV3Downgrader.ts +355 -355
  32. package/src/converters/internal/OpenApiV3Upgrader.ts +470 -470
  33. package/src/converters/internal/OpenApiV3_1Upgrader.ts +685 -685
  34. package/src/converters/internal/SwaggerV2Downgrader.ts +424 -424
  35. package/src/converters/internal/SwaggerV2Upgrader.ts +523 -523
  36. package/src/http/HttpError.ts +107 -107
  37. package/src/http/HttpLlm.ts +167 -167
  38. package/src/http/HttpMigration.ts +92 -92
  39. package/src/http/index.ts +3 -3
  40. package/src/http/internal/HttpLlmApplicationComposer.ts +361 -360
  41. package/src/http/internal/HttpLlmFunctionFetcher.ts +37 -37
  42. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  43. package/src/http/internal/HttpMigrateRouteAccessor.ts +135 -135
  44. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  45. package/src/http/internal/HttpMigrateRouteFetcher.ts +203 -203
  46. package/src/index.ts +4 -4
  47. package/src/utils/ArrayUtil.ts +42 -42
  48. package/src/utils/LlmJson.ts +141 -141
  49. package/src/utils/MapUtil.ts +15 -15
  50. package/src/utils/NamingConvention.ts +205 -205
  51. package/src/utils/Singleton.ts +17 -17
  52. package/src/utils/StringUtil.ts +14 -14
  53. package/src/utils/dedent.ts +57 -57
  54. package/src/utils/index.ts +8 -8
  55. package/src/utils/internal/EndpointUtil.ts +44 -44
  56. package/src/utils/internal/JsonDescriptor.ts +70 -70
  57. package/src/utils/internal/OpenApiTypeCheckerBase.ts +822 -822
  58. package/src/utils/internal/coerceLlmArguments.ts +314 -297
  59. package/src/utils/internal/parseLenientJson.ts +894 -731
  60. package/src/utils/internal/stringifyValidationFailure.ts +415 -411
  61. package/src/validators/LlmTypeChecker.ts +402 -402
  62. package/src/validators/OpenApiTypeChecker.ts +297 -297
  63. package/src/validators/OpenApiV3TypeChecker.ts +70 -70
  64. package/src/validators/OpenApiV3_1TypeChecker.ts +86 -86
  65. package/src/validators/OpenApiValidator.ts +94 -94
  66. package/src/validators/SwaggerV2TypeChecker.ts +71 -71
  67. package/src/validators/functional/_isBigintString.ts +8 -8
  68. package/src/validators/functional/_isFormatByte.ts +7 -7
  69. package/src/validators/functional/_isFormatDate.ts +3 -3
  70. package/src/validators/functional/_isFormatDateTime.ts +4 -4
  71. package/src/validators/functional/_isFormatDuration.ts +4 -4
  72. package/src/validators/functional/_isFormatEmail.ts +4 -4
  73. package/src/validators/functional/_isFormatHostname.ts +4 -4
  74. package/src/validators/functional/_isFormatIdnEmail.ts +4 -4
  75. package/src/validators/functional/_isFormatIdnHostname.ts +4 -4
  76. package/src/validators/functional/_isFormatIpv4.ts +4 -4
  77. package/src/validators/functional/_isFormatIpv6.ts +4 -4
  78. package/src/validators/functional/_isFormatIri.ts +3 -3
  79. package/src/validators/functional/_isFormatIriReference.ts +4 -4
  80. package/src/validators/functional/_isFormatJsonPointer.ts +3 -3
  81. package/src/validators/functional/_isFormatPassword.ts +1 -1
  82. package/src/validators/functional/_isFormatRegex.ts +8 -8
  83. package/src/validators/functional/_isFormatRelativeJsonPointer.ts +4 -4
  84. package/src/validators/functional/_isFormatTime.ts +4 -4
  85. package/src/validators/functional/_isFormatUri.ts +6 -6
  86. package/src/validators/functional/_isFormatUriReference.ts +5 -5
  87. package/src/validators/functional/_isFormatUriTemplate.ts +4 -4
  88. package/src/validators/functional/_isFormatUrl.ts +4 -4
  89. package/src/validators/functional/_isFormatUuid.ts +3 -3
  90. package/src/validators/functional/_isUniqueItems.ts +159 -159
  91. package/src/validators/index.ts +14 -14
  92. package/src/validators/internal/IOpenApiValidatorContext.ts +17 -17
  93. package/src/validators/internal/OpenApiArrayValidator.ts +49 -49
  94. package/src/validators/internal/OpenApiBooleanValidator.ts +11 -11
  95. package/src/validators/internal/OpenApiConstantValidator.ts +11 -11
  96. package/src/validators/internal/OpenApiIntegerValidator.ts +49 -49
  97. package/src/validators/internal/OpenApiNumberValidator.ts +48 -48
  98. package/src/validators/internal/OpenApiObjectValidator.ts +83 -83
  99. package/src/validators/internal/OpenApiOneOfValidator.ts +309 -309
  100. package/src/validators/internal/OpenApiSchemaNamingRule.ts +124 -124
  101. package/src/validators/internal/OpenApiStationValidator.ts +115 -115
  102. package/src/validators/internal/OpenApiStringValidator.ts +88 -88
  103. package/src/validators/internal/OpenApiTupleValidator.ts +55 -55
@@ -1,647 +1,647 @@
1
- import {
2
- IJsonSchemaAttribute,
3
- IJsonSchemaTransformError,
4
- ILlmSchema,
5
- IResult,
6
- OpenApi,
7
- } from "@typia/interface";
8
-
9
- import { JsonDescriptor } from "../utils/internal/JsonDescriptor";
10
- import { LlmTypeChecker } from "../validators/LlmTypeChecker";
11
- import { OpenApiTypeChecker } from "../validators/OpenApiTypeChecker";
12
- import { LlmDescriptionInverter } from "./internal/LlmDescriptionInverter";
13
- import { LlmParametersFinder } from "./internal/LlmParametersComposer";
14
- import { OpenApiConstraintShifter } from "./internal/OpenApiConstraintShifter";
15
-
16
- /**
17
- * OpenAPI to LLM schema converter.
18
- *
19
- * `LlmSchemaConverter` converts OpenAPI JSON schemas to LLM-compatible
20
- * {@link ILlmSchema} format. LLMs don't fully support JSON Schema, so this
21
- * simplifies schemas by removing unsupported features (tuples, `const`, mixed
22
- * unions).
23
- *
24
- * Main functions:
25
- *
26
- * - {@link parameters}: Convert object schema to {@link ILlmSchema.IParameters}
27
- * - {@link schema}: Convert any schema to {@link ILlmSchema}
28
- * - {@link invert}: Extract constraints from description back to schema
29
- *
30
- * Configuration options ({@link ILlmSchema.IConfig}):
31
- *
32
- * - `reference`: Allow `$ref` references (reduces tokens but may confuse LLM)
33
- * - `strict`: OpenAI structured output mode (all properties required)
34
- *
35
- * @author Jeongho Nam - https://github.com/samchon
36
- */
37
- export namespace LlmSchemaConverter {
38
- /**
39
- * Get configuration with defaults applied.
40
- *
41
- * @param config Partial configuration
42
- * @returns Full configuration with defaults
43
- */
44
- export const getConfig = (
45
- config?: Partial<ILlmSchema.IConfig> | undefined,
46
- ): ILlmSchema.IConfig => ({
47
- reference: config?.reference ?? true,
48
- strict: config?.strict ?? false,
49
- });
50
-
51
- /* -----------------------------------------------------------
52
- CONVERTERS
53
- ----------------------------------------------------------- */
54
- /**
55
- * Convert OpenAPI object schema to LLM parameters schema.
56
- *
57
- * @param props.config Conversion configuration
58
- * @param props.components OpenAPI components for reference resolution
59
- * @param props.schema Object or reference schema to convert
60
- * @param props.accessor Error path accessor
61
- * @param props.refAccessor Reference path accessor
62
- * @returns Converted parameters or error
63
- */
64
- export const parameters = (props: {
65
- config?: Partial<ILlmSchema.IConfig>;
66
- components: OpenApi.IComponents;
67
- schema: OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference;
68
- accessor?: string;
69
- refAccessor?: string;
70
- }): IResult<ILlmSchema.IParameters, IJsonSchemaTransformError> => {
71
- const config: ILlmSchema.IConfig = getConfig(props.config);
72
- const entity: IResult<
73
- OpenApi.IJsonSchema.IObject,
74
- IJsonSchemaTransformError
75
- > = LlmParametersFinder.parameters({
76
- ...props,
77
- method: "LlmSchemaConverter.parameters",
78
- });
79
- if (entity.success === false) return entity;
80
-
81
- const $defs: Record<string, ILlmSchema> = {};
82
- const result: IResult<ILlmSchema, IJsonSchemaTransformError> = transform({
83
- ...props,
84
- config,
85
- $defs,
86
- schema: entity.value,
87
- });
88
- if (result.success === false) return result;
89
- return {
90
- success: true,
91
- value: {
92
- ...(result.value as ILlmSchema.IObject),
93
- additionalProperties: false,
94
- $defs,
95
- description: OpenApiTypeChecker.isReference(props.schema)
96
- ? JsonDescriptor.cascade({
97
- prefix: "#/components/schemas/",
98
- components: props.components,
99
- schema: {
100
- ...props.schema,
101
- description: result.value.description,
102
- },
103
- escape: true,
104
- })
105
- : result.value.description,
106
- } satisfies ILlmSchema.IParameters,
107
- };
108
- };
109
-
110
- /**
111
- * Convert OpenAPI schema to LLM schema.
112
- *
113
- * @param props.config Conversion configuration
114
- * @param props.components OpenAPI components for reference resolution
115
- * @param props.$defs Definition store (mutated with referenced types)
116
- * @param props.schema Schema to convert
117
- * @param props.accessor Error path accessor
118
- * @param props.refAccessor Reference path accessor
119
- * @returns Converted schema or error
120
- */
121
- export const schema = (props: {
122
- config?: Partial<ILlmSchema.IConfig>;
123
- components: OpenApi.IComponents;
124
- $defs: Record<string, ILlmSchema>;
125
- schema: OpenApi.IJsonSchema;
126
- accessor?: string;
127
- refAccessor?: string;
128
- }): IResult<ILlmSchema, IJsonSchemaTransformError> =>
129
- transform({
130
- config: getConfig(props.config),
131
- components: props.components,
132
- $defs: props.$defs,
133
- schema: props.schema,
134
- accessor: props.accessor,
135
- refAccessor: props.refAccessor,
136
- });
137
-
138
- const transform = (props: {
139
- config: ILlmSchema.IConfig;
140
- components: OpenApi.IComponents;
141
- $defs: Record<string, ILlmSchema>;
142
- schema: OpenApi.IJsonSchema;
143
- accessor?: string;
144
- refAccessor?: string;
145
- }): IResult<ILlmSchema, IJsonSchemaTransformError> => {
146
- // PREPARE ASSETS
147
- const union: Array<ILlmSchema> = [];
148
- const attribute: IJsonSchemaAttribute = {
149
- title: props.schema.title,
150
- description: props.schema.description,
151
- deprecated: props.schema.deprecated,
152
- readOnly: props.schema.readOnly,
153
- writeOnly: props.schema.writeOnly,
154
- example: props.schema.example,
155
- examples: props.schema.examples,
156
- ...Object.fromEntries(
157
- Object.entries(props.schema).filter(
158
- ([key, value]) => key.startsWith("x-") && value !== undefined,
159
- ),
160
- ),
161
- };
162
-
163
- // VALIDADTE SCHEMA
164
- const reasons: IJsonSchemaTransformError.IReason[] = [];
165
- OpenApiTypeChecker.visit({
166
- closure: (next, accessor) => {
167
- if (props.config.strict === true) {
168
- // STRICT MODE VALIDATION
169
- reasons.push(...validateStrict(next, accessor));
170
- }
171
- if (OpenApiTypeChecker.isTuple(next))
172
- reasons.push({
173
- accessor,
174
- schema: next,
175
- message: `LLM does not allow tuple type.`,
176
- });
177
- else if (OpenApiTypeChecker.isReference(next)) {
178
- // UNABLE TO FIND MATCHED REFERENCE
179
- const key: string =
180
- next.$ref.split("#/components/schemas/")[1] ??
181
- next.$ref.split("/").at(-1)!;
182
- if (props.components.schemas?.[key] === undefined)
183
- reasons.push({
184
- schema: next,
185
- accessor: accessor,
186
- message: `unable to find reference type ${JSON.stringify(key)}.`,
187
- });
188
- }
189
- },
190
- components: props.components,
191
- schema: props.schema,
192
- accessor: props.accessor,
193
- refAccessor: props.refAccessor,
194
- });
195
- if (reasons.length > 0)
196
- return {
197
- success: false,
198
- error: {
199
- method: "LlmSchemaConverter.schema",
200
- message: "Failed to compose LLM schema",
201
- reasons,
202
- },
203
- };
204
-
205
- const visitConstant = (input: OpenApi.IJsonSchema): void => {
206
- const insert = (value: any): void => {
207
- const matched:
208
- | ILlmSchema.IString
209
- | ILlmSchema.INumber
210
- | ILlmSchema.IBoolean
211
- | undefined = union.find(
212
- (u) =>
213
- (u as (IJsonSchemaAttribute & { type: string }) | undefined)
214
- ?.type === typeof value,
215
- ) as ILlmSchema.IString | undefined;
216
- if (matched !== undefined) {
217
- matched.enum ??= [];
218
- matched.enum.push(value);
219
- } else
220
- union.push({
221
- type: typeof value as "number",
222
- enum: [value],
223
- });
224
- };
225
- if (OpenApiTypeChecker.isConstant(input)) insert(input.const);
226
- else if (OpenApiTypeChecker.isOneOf(input))
227
- input.oneOf.forEach(visitConstant);
228
- };
229
- const visit = (input: OpenApi.IJsonSchema, accessor: string): void => {
230
- if (OpenApiTypeChecker.isOneOf(input)) {
231
- // UNION TYPE
232
- input.oneOf.forEach((s, i) => visit(s, `${accessor}.oneOf[${i}]`));
233
- } else if (OpenApiTypeChecker.isReference(input)) {
234
- // REFERENCE TYPE
235
- const key: string =
236
- input.$ref.split("#/components/schemas/")[1] ??
237
- input.$ref.split("/").at(-1)!;
238
- const target: OpenApi.IJsonSchema | undefined =
239
- props.components.schemas?.[key];
240
- if (target === undefined) return;
241
- else if (
242
- // KEEP THE REFERENCE TYPE
243
- props.config.reference === true ||
244
- OpenApiTypeChecker.isRecursiveReference({
245
- components: props.components,
246
- schema: input,
247
- })
248
- ) {
249
- const out = () => {
250
- union.push({
251
- ...input,
252
- $ref: `#/$defs/${key}`,
253
- });
254
- };
255
- if (props.$defs[key] !== undefined) return out();
256
-
257
- props.$defs[key] = {};
258
- const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
259
- transform({
260
- config: props.config,
261
- components: props.components,
262
- $defs: props.$defs,
263
- schema: target,
264
- refAccessor: props.refAccessor,
265
- accessor: `${props.refAccessor ?? "$def"}[${JSON.stringify(key)}]`,
266
- });
267
- if (converted.success === false) return; // UNREACHABLE
268
- props.$defs[key] = converted.value;
269
- return out();
270
- } else {
271
- // DISCARD THE REFERENCE TYPE
272
- const length: number = union.length;
273
- visit(target, accessor);
274
- visitConstant(target);
275
- if (length === union.length - 1)
276
- union[union.length - 1] = {
277
- ...union[union.length - 1]!,
278
- description: JsonDescriptor.cascade({
279
- prefix: "#/components/schemas/",
280
- components: props.components,
281
- schema: input,
282
- escape: true,
283
- }),
284
- };
285
- else
286
- attribute.description = JsonDescriptor.cascade({
287
- prefix: "#/components/schemas/",
288
- components: props.components,
289
- schema: input,
290
- escape: true,
291
- });
292
- }
293
- } else if (OpenApiTypeChecker.isObject(input)) {
294
- // OBJECT TYPE
295
- const properties: Record<string, ILlmSchema> = Object.fromEntries(
296
- Object.entries(input.properties ?? {})
297
- .map(([key, value]) => {
298
- const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
299
- transform({
300
- config: props.config,
301
- components: props.components,
302
- $defs: props.$defs,
303
- schema: value,
304
- refAccessor: props.refAccessor,
305
- accessor: `${props.accessor ?? "$input.schema"}.properties[${JSON.stringify(key)}]`,
306
- });
307
- if (converted.success === false) {
308
- reasons.push(...converted.error.reasons);
309
- return [key, null];
310
- }
311
- return [key, converted.value];
312
- })
313
- .filter(([, value]) => value !== null),
314
- );
315
- if (Object.values(properties).some((v) => v === null)) return;
316
-
317
- const additionalProperties: ILlmSchema | boolean | undefined | null =
318
- (() => {
319
- if (
320
- typeof input.additionalProperties === "object" &&
321
- input.additionalProperties !== null
322
- ) {
323
- const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
324
- transform({
325
- config: props.config,
326
- components: props.components,
327
- $defs: props.$defs,
328
- schema: input.additionalProperties,
329
- refAccessor: props.refAccessor,
330
- accessor: `${accessor}.additionalProperties`,
331
- });
332
- if (converted.success === false) {
333
- reasons.push(...converted.error.reasons);
334
- return null;
335
- }
336
- return converted.value;
337
- }
338
- return props.config.strict === true
339
- ? false
340
- : input.additionalProperties;
341
- })();
342
- if (additionalProperties === null) return;
343
- union.push({
344
- ...input,
345
- properties,
346
- additionalProperties,
347
- required: input.required ?? [],
348
- description:
349
- props.config.strict === true
350
- ? JsonDescriptor.take(input)
351
- : input.description,
352
- });
353
- } else if (OpenApiTypeChecker.isArray(input)) {
354
- // ARRAY TYPE
355
- const items: IResult<ILlmSchema, IJsonSchemaTransformError> = transform(
356
- {
357
- config: props.config,
358
- components: props.components,
359
- $defs: props.$defs,
360
- schema: input.items,
361
- refAccessor: props.refAccessor,
362
- accessor: `${accessor}.items`,
363
- },
364
- );
365
- if (items.success === false) {
366
- reasons.push(...items.error.reasons);
367
- return;
368
- }
369
- union.push(
370
- props.config.strict === true
371
- ? OpenApiConstraintShifter.shiftArray({
372
- ...input,
373
- items: items.value,
374
- })
375
- : {
376
- ...input,
377
- items: items.value,
378
- },
379
- );
380
- } else if (OpenApiTypeChecker.isString(input))
381
- union.push(
382
- props.config.strict === true
383
- ? OpenApiConstraintShifter.shiftString({ ...input })
384
- : input,
385
- );
386
- else if (
387
- OpenApiTypeChecker.isNumber(input) ||
388
- OpenApiTypeChecker.isInteger(input)
389
- )
390
- union.push(
391
- props.config.strict === true
392
- ? OpenApiConstraintShifter.shiftNumeric({ ...input })
393
- : input,
394
- );
395
- else if (OpenApiTypeChecker.isTuple(input))
396
- return; // UNREACHABLE
397
- else if (OpenApiTypeChecker.isConstant(input) === false)
398
- union.push({ ...input });
399
- };
400
-
401
- visitConstant(props.schema);
402
- visit(props.schema, props.accessor ?? "$input.schema");
403
-
404
- if (reasons.length > 0)
405
- return {
406
- success: false,
407
- error: {
408
- method: "LlmSchemaConverter.schema",
409
- message: "Failed to compose LLM schema",
410
- reasons,
411
- },
412
- };
413
- else if (union.length === 0)
414
- return {
415
- // unknown type
416
- success: true,
417
- value: {
418
- ...attribute,
419
- type: undefined,
420
- },
421
- };
422
- else if (union.length === 1)
423
- return {
424
- // single type
425
- success: true,
426
- value: {
427
- ...attribute,
428
- ...union[0],
429
- description:
430
- props.config.strict === true &&
431
- LlmTypeChecker.isReference(union[0]!)
432
- ? undefined
433
- : (union[0]!.description ?? attribute.description),
434
- },
435
- };
436
- return {
437
- success: true,
438
- value: {
439
- ...attribute,
440
- anyOf: union.map((u) => ({
441
- ...u,
442
- description:
443
- props.config.strict === true && LlmTypeChecker.isReference(u)
444
- ? undefined
445
- : u.description,
446
- })),
447
- "x-discriminator":
448
- OpenApiTypeChecker.isOneOf(props.schema) &&
449
- props.schema.discriminator !== undefined &&
450
- props.schema.oneOf.length === union.length &&
451
- union.every(
452
- (e) => LlmTypeChecker.isReference(e) || LlmTypeChecker.isNull(e),
453
- )
454
- ? {
455
- propertyName: props.schema.discriminator.propertyName,
456
- mapping:
457
- props.schema.discriminator.mapping !== undefined
458
- ? Object.fromEntries(
459
- Object.entries(props.schema.discriminator.mapping).map(
460
- ([key, value]) => [
461
- key,
462
- `#/$defs/${value.split("/").at(-1)}`,
463
- ],
464
- ),
465
- )
466
- : undefined,
467
- }
468
- : undefined,
469
- },
470
- };
471
- };
472
-
473
- /* -----------------------------------------------------------
474
- INVERTERS
475
- ----------------------------------------------------------- */
476
- /**
477
- * Convert LLM schema back to OpenAPI schema.
478
- *
479
- * Restores constraint information from description tags and converts `$defs`
480
- * references to `#/components/schemas`.
481
- *
482
- * @param props.components Target components (mutated with definitions)
483
- * @param props.schema LLM schema to invert
484
- * @param props.$defs LLM schema definitions
485
- * @returns OpenAPI JSON schema
486
- */
487
- export const invert = (props: {
488
- components: OpenApi.IComponents;
489
- schema: ILlmSchema;
490
- $defs: Record<string, ILlmSchema>;
491
- }): OpenApi.IJsonSchema => {
492
- const union: OpenApi.IJsonSchema[] = [];
493
- const attribute: IJsonSchemaAttribute = {
494
- title: props.schema.title,
495
- description: props.schema.description,
496
- deprecated: props.schema.deprecated,
497
- readOnly: props.schema.readOnly,
498
- writeOnly: props.schema.writeOnly,
499
- example: props.schema.example,
500
- examples: props.schema.examples,
501
- ...Object.fromEntries(
502
- Object.entries(props.schema).filter(
503
- ([key, value]) => key.startsWith("x-") && value !== undefined,
504
- ),
505
- ),
506
- };
507
-
508
- const next = (schema: ILlmSchema): OpenApi.IJsonSchema =>
509
- invert({
510
- components: props.components,
511
- $defs: props.$defs,
512
- schema,
513
- });
514
- const visit = (schema: ILlmSchema): void => {
515
- if (LlmTypeChecker.isArray(schema))
516
- union.push({
517
- ...schema,
518
- ...LlmDescriptionInverter.array(schema.description),
519
- items: next(schema.items),
520
- });
521
- else if (LlmTypeChecker.isObject(schema))
522
- union.push({
523
- ...schema,
524
- properties: Object.fromEntries(
525
- Object.entries(schema.properties).map(([key, value]) => [
526
- key,
527
- next(value),
528
- ]),
529
- ),
530
- additionalProperties:
531
- typeof schema.additionalProperties === "object" &&
532
- schema.additionalProperties !== null
533
- ? next(schema.additionalProperties)
534
- : schema.additionalProperties,
535
- });
536
- else if (LlmTypeChecker.isAnyOf(schema)) schema.anyOf.forEach(visit);
537
- else if (LlmTypeChecker.isReference(schema)) {
538
- const key: string =
539
- schema.$ref.split("#/$defs/")[1] ?? schema.$ref.split("/").at(-1)!;
540
- if (props.components.schemas?.[key] === undefined) {
541
- props.components.schemas ??= {};
542
- props.components.schemas[key] = {};
543
- props.components.schemas[key] = next(props.$defs[key] ?? {});
544
- }
545
- union.push({
546
- ...schema,
547
- $ref: `#/components/schemas/${key}`,
548
- });
549
- } else if (LlmTypeChecker.isBoolean(schema))
550
- if (!!schema.enum?.length)
551
- schema.enum.forEach((v) =>
552
- union.push({
553
- const: v,
554
- }),
555
- );
556
- else union.push(schema);
557
- else if (
558
- LlmTypeChecker.isInteger(schema) ||
559
- LlmTypeChecker.isNumber(schema)
560
- )
561
- if (!!schema.enum?.length)
562
- schema.enum.forEach((v) =>
563
- union.push({
564
- const: v,
565
- }),
566
- );
567
- else
568
- union.push({
569
- ...schema,
570
- ...LlmDescriptionInverter.numeric(schema.description),
571
- ...{ enum: undefined },
572
- });
573
- else if (LlmTypeChecker.isString(schema))
574
- if (!!schema.enum?.length)
575
- schema.enum.forEach((v) =>
576
- union.push({
577
- const: v,
578
- }),
579
- );
580
- else
581
- union.push({
582
- ...schema,
583
- ...LlmDescriptionInverter.string(schema.description),
584
- ...{ enum: undefined },
585
- });
586
- else
587
- union.push({
588
- ...schema,
589
- });
590
- };
591
- visit(props.schema);
592
-
593
- return {
594
- ...attribute,
595
- ...(union.length === 0
596
- ? { type: undefined }
597
- : union.length === 1
598
- ? { ...union[0] }
599
- : {
600
- oneOf: union.map((u) => ({ ...u, nullable: undefined })),
601
- discriminator:
602
- LlmTypeChecker.isAnyOf(props.schema) &&
603
- props.schema["x-discriminator"] !== undefined
604
- ? {
605
- propertyName:
606
- props.schema["x-discriminator"].propertyName,
607
- mapping:
608
- props.schema["x-discriminator"].mapping !== undefined
609
- ? Object.fromEntries(
610
- Object.entries(
611
- props.schema["x-discriminator"].mapping,
612
- ).map(([key, value]) => [
613
- key,
614
- `#/components/schemas/${value.split("/").at(-1)}`,
615
- ]),
616
- )
617
- : undefined,
618
- }
619
- : undefined,
620
- }),
621
- } satisfies OpenApi.IJsonSchema;
622
- };
623
- }
624
-
625
- const validateStrict = (
626
- schema: OpenApi.IJsonSchema,
627
- accessor: string,
628
- ): IJsonSchemaTransformError.IReason[] => {
629
- const reasons: IJsonSchemaTransformError.IReason[] = [];
630
- if (OpenApiTypeChecker.isObject(schema)) {
631
- if (!!schema.additionalProperties)
632
- reasons.push({
633
- schema: schema,
634
- accessor: `${accessor}.additionalProperties`,
635
- message:
636
- "LLM does not allow additionalProperties in strict mode, the dynamic key typed object.",
637
- });
638
- for (const key of Object.keys(schema.properties ?? {}))
639
- if (schema.required?.includes(key) === false)
640
- reasons.push({
641
- schema: schema,
642
- accessor: `${accessor}.properties.${key}`,
643
- message: "LLM does not allow optional properties in strict mode.",
644
- });
645
- }
646
- return reasons;
647
- };
1
+ import {
2
+ IJsonSchemaAttribute,
3
+ IJsonSchemaTransformError,
4
+ ILlmSchema,
5
+ IResult,
6
+ OpenApi,
7
+ } from "@typia/interface";
8
+
9
+ import { JsonDescriptor } from "../utils/internal/JsonDescriptor";
10
+ import { LlmTypeChecker } from "../validators/LlmTypeChecker";
11
+ import { OpenApiTypeChecker } from "../validators/OpenApiTypeChecker";
12
+ import { LlmDescriptionInverter } from "./internal/LlmDescriptionInverter";
13
+ import { LlmParametersFinder } from "./internal/LlmParametersComposer";
14
+ import { OpenApiConstraintShifter } from "./internal/OpenApiConstraintShifter";
15
+
16
+ /**
17
+ * OpenAPI to LLM schema converter.
18
+ *
19
+ * `LlmSchemaConverter` converts OpenAPI JSON schemas to LLM-compatible
20
+ * {@link ILlmSchema} format. LLMs don't fully support JSON Schema, so this
21
+ * simplifies schemas by removing unsupported features (tuples, `const`, mixed
22
+ * unions).
23
+ *
24
+ * Main functions:
25
+ *
26
+ * - {@link parameters}: Convert object schema to {@link ILlmSchema.IParameters}
27
+ * - {@link schema}: Convert any schema to {@link ILlmSchema}
28
+ * - {@link invert}: Extract constraints from description back to schema
29
+ *
30
+ * Configuration options ({@link ILlmSchema.IConfig}):
31
+ *
32
+ * - `reference`: Allow `$ref` references (reduces tokens but may confuse LLM)
33
+ * - `strict`: OpenAI structured output mode (all properties required)
34
+ *
35
+ * @author Jeongho Nam - https://github.com/samchon
36
+ */
37
+ export namespace LlmSchemaConverter {
38
+ /**
39
+ * Get configuration with defaults applied.
40
+ *
41
+ * @param config Partial configuration
42
+ * @returns Full configuration with defaults
43
+ */
44
+ export const getConfig = (
45
+ config?: Partial<ILlmSchema.IConfig> | undefined,
46
+ ): ILlmSchema.IConfig => ({
47
+ reference: config?.reference ?? true,
48
+ strict: config?.strict ?? false,
49
+ });
50
+
51
+ /* -----------------------------------------------------------
52
+ CONVERTERS
53
+ ----------------------------------------------------------- */
54
+ /**
55
+ * Convert OpenAPI object schema to LLM parameters schema.
56
+ *
57
+ * @param props.config Conversion configuration
58
+ * @param props.components OpenAPI components for reference resolution
59
+ * @param props.schema Object or reference schema to convert
60
+ * @param props.accessor Error path accessor
61
+ * @param props.refAccessor Reference path accessor
62
+ * @returns Converted parameters or error
63
+ */
64
+ export const parameters = (props: {
65
+ config?: Partial<ILlmSchema.IConfig>;
66
+ components: OpenApi.IComponents;
67
+ schema: OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference;
68
+ accessor?: string;
69
+ refAccessor?: string;
70
+ }): IResult<ILlmSchema.IParameters, IJsonSchemaTransformError> => {
71
+ const config: ILlmSchema.IConfig = getConfig(props.config);
72
+ const entity: IResult<
73
+ OpenApi.IJsonSchema.IObject,
74
+ IJsonSchemaTransformError
75
+ > = LlmParametersFinder.parameters({
76
+ ...props,
77
+ method: "LlmSchemaConverter.parameters",
78
+ });
79
+ if (entity.success === false) return entity;
80
+
81
+ const $defs: Record<string, ILlmSchema> = {};
82
+ const result: IResult<ILlmSchema, IJsonSchemaTransformError> = transform({
83
+ ...props,
84
+ config,
85
+ $defs,
86
+ schema: entity.value,
87
+ });
88
+ if (result.success === false) return result;
89
+ return {
90
+ success: true,
91
+ value: {
92
+ ...(result.value as ILlmSchema.IObject),
93
+ additionalProperties: false,
94
+ $defs,
95
+ description: OpenApiTypeChecker.isReference(props.schema)
96
+ ? JsonDescriptor.cascade({
97
+ prefix: "#/components/schemas/",
98
+ components: props.components,
99
+ schema: {
100
+ ...props.schema,
101
+ description: result.value.description,
102
+ },
103
+ escape: true,
104
+ })
105
+ : result.value.description,
106
+ } satisfies ILlmSchema.IParameters,
107
+ };
108
+ };
109
+
110
+ /**
111
+ * Convert OpenAPI schema to LLM schema.
112
+ *
113
+ * @param props.config Conversion configuration
114
+ * @param props.components OpenAPI components for reference resolution
115
+ * @param props.$defs Definition store (mutated with referenced types)
116
+ * @param props.schema Schema to convert
117
+ * @param props.accessor Error path accessor
118
+ * @param props.refAccessor Reference path accessor
119
+ * @returns Converted schema or error
120
+ */
121
+ export const schema = (props: {
122
+ config?: Partial<ILlmSchema.IConfig>;
123
+ components: OpenApi.IComponents;
124
+ $defs: Record<string, ILlmSchema>;
125
+ schema: OpenApi.IJsonSchema;
126
+ accessor?: string;
127
+ refAccessor?: string;
128
+ }): IResult<ILlmSchema, IJsonSchemaTransformError> =>
129
+ transform({
130
+ config: getConfig(props.config),
131
+ components: props.components,
132
+ $defs: props.$defs,
133
+ schema: props.schema,
134
+ accessor: props.accessor,
135
+ refAccessor: props.refAccessor,
136
+ });
137
+
138
+ const transform = (props: {
139
+ config: ILlmSchema.IConfig;
140
+ components: OpenApi.IComponents;
141
+ $defs: Record<string, ILlmSchema>;
142
+ schema: OpenApi.IJsonSchema;
143
+ accessor?: string;
144
+ refAccessor?: string;
145
+ }): IResult<ILlmSchema, IJsonSchemaTransformError> => {
146
+ // PREPARE ASSETS
147
+ const union: Array<ILlmSchema> = [];
148
+ const attribute: IJsonSchemaAttribute = {
149
+ title: props.schema.title,
150
+ description: props.schema.description,
151
+ deprecated: props.schema.deprecated,
152
+ readOnly: props.schema.readOnly,
153
+ writeOnly: props.schema.writeOnly,
154
+ example: props.schema.example,
155
+ examples: props.schema.examples,
156
+ ...Object.fromEntries(
157
+ Object.entries(props.schema).filter(
158
+ ([key, value]) => key.startsWith("x-") && value !== undefined,
159
+ ),
160
+ ),
161
+ };
162
+
163
+ // VALIDADTE SCHEMA
164
+ const reasons: IJsonSchemaTransformError.IReason[] = [];
165
+ OpenApiTypeChecker.visit({
166
+ closure: (next, accessor) => {
167
+ if (props.config.strict === true) {
168
+ // STRICT MODE VALIDATION
169
+ reasons.push(...validateStrict(next, accessor));
170
+ }
171
+ if (OpenApiTypeChecker.isTuple(next))
172
+ reasons.push({
173
+ accessor,
174
+ schema: next,
175
+ message: `LLM does not allow tuple type.`,
176
+ });
177
+ else if (OpenApiTypeChecker.isReference(next)) {
178
+ // UNABLE TO FIND MATCHED REFERENCE
179
+ const key: string =
180
+ next.$ref.split("#/components/schemas/")[1] ??
181
+ next.$ref.split("/").at(-1)!;
182
+ if (props.components.schemas?.[key] === undefined)
183
+ reasons.push({
184
+ schema: next,
185
+ accessor: accessor,
186
+ message: `unable to find reference type ${JSON.stringify(key)}.`,
187
+ });
188
+ }
189
+ },
190
+ components: props.components,
191
+ schema: props.schema,
192
+ accessor: props.accessor,
193
+ refAccessor: props.refAccessor,
194
+ });
195
+ if (reasons.length > 0)
196
+ return {
197
+ success: false,
198
+ error: {
199
+ method: "LlmSchemaConverter.schema",
200
+ message: "Failed to compose LLM schema",
201
+ reasons,
202
+ },
203
+ };
204
+
205
+ const visitConstant = (input: OpenApi.IJsonSchema): void => {
206
+ const insert = (value: any): void => {
207
+ const matched:
208
+ | ILlmSchema.IString
209
+ | ILlmSchema.INumber
210
+ | ILlmSchema.IBoolean
211
+ | undefined = union.find(
212
+ (u) =>
213
+ (u as (IJsonSchemaAttribute & { type: string }) | undefined)
214
+ ?.type === typeof value,
215
+ ) as ILlmSchema.IString | undefined;
216
+ if (matched !== undefined) {
217
+ matched.enum ??= [];
218
+ matched.enum.push(value);
219
+ } else
220
+ union.push({
221
+ type: typeof value as "number",
222
+ enum: [value],
223
+ });
224
+ };
225
+ if (OpenApiTypeChecker.isConstant(input)) insert(input.const);
226
+ else if (OpenApiTypeChecker.isOneOf(input))
227
+ input.oneOf.forEach(visitConstant);
228
+ };
229
+ const visit = (input: OpenApi.IJsonSchema, accessor: string): void => {
230
+ if (OpenApiTypeChecker.isOneOf(input)) {
231
+ // UNION TYPE
232
+ input.oneOf.forEach((s, i) => visit(s, `${accessor}.oneOf[${i}]`));
233
+ } else if (OpenApiTypeChecker.isReference(input)) {
234
+ // REFERENCE TYPE
235
+ const key: string =
236
+ input.$ref.split("#/components/schemas/")[1] ??
237
+ input.$ref.split("/").at(-1)!;
238
+ const target: OpenApi.IJsonSchema | undefined =
239
+ props.components.schemas?.[key];
240
+ if (target === undefined) return;
241
+ else if (
242
+ // KEEP THE REFERENCE TYPE
243
+ props.config.reference === true ||
244
+ OpenApiTypeChecker.isRecursiveReference({
245
+ components: props.components,
246
+ schema: input,
247
+ })
248
+ ) {
249
+ const out = () => {
250
+ union.push({
251
+ ...input,
252
+ $ref: `#/$defs/${key}`,
253
+ });
254
+ };
255
+ if (props.$defs[key] !== undefined) return out();
256
+
257
+ props.$defs[key] = {};
258
+ const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
259
+ transform({
260
+ config: props.config,
261
+ components: props.components,
262
+ $defs: props.$defs,
263
+ schema: target,
264
+ refAccessor: props.refAccessor,
265
+ accessor: `${props.refAccessor ?? "$def"}[${JSON.stringify(key)}]`,
266
+ });
267
+ if (converted.success === false) return; // UNREACHABLE
268
+ props.$defs[key] = converted.value;
269
+ return out();
270
+ } else {
271
+ // DISCARD THE REFERENCE TYPE
272
+ const length: number = union.length;
273
+ visit(target, accessor);
274
+ visitConstant(target);
275
+ if (length === union.length - 1)
276
+ union[union.length - 1] = {
277
+ ...union[union.length - 1]!,
278
+ description: JsonDescriptor.cascade({
279
+ prefix: "#/components/schemas/",
280
+ components: props.components,
281
+ schema: input,
282
+ escape: true,
283
+ }),
284
+ };
285
+ else
286
+ attribute.description = JsonDescriptor.cascade({
287
+ prefix: "#/components/schemas/",
288
+ components: props.components,
289
+ schema: input,
290
+ escape: true,
291
+ });
292
+ }
293
+ } else if (OpenApiTypeChecker.isObject(input)) {
294
+ // OBJECT TYPE
295
+ const properties: Record<string, ILlmSchema> = Object.fromEntries(
296
+ Object.entries(input.properties ?? {})
297
+ .map(([key, value]) => {
298
+ const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
299
+ transform({
300
+ config: props.config,
301
+ components: props.components,
302
+ $defs: props.$defs,
303
+ schema: value,
304
+ refAccessor: props.refAccessor,
305
+ accessor: `${props.accessor ?? "$input.schema"}.properties[${JSON.stringify(key)}]`,
306
+ });
307
+ if (converted.success === false) {
308
+ reasons.push(...converted.error.reasons);
309
+ return [key, null];
310
+ }
311
+ return [key, converted.value];
312
+ })
313
+ .filter(([, value]) => value !== null),
314
+ );
315
+ if (Object.values(properties).some((v) => v === null)) return;
316
+
317
+ const additionalProperties: ILlmSchema | boolean | undefined | null =
318
+ (() => {
319
+ if (
320
+ typeof input.additionalProperties === "object" &&
321
+ input.additionalProperties !== null
322
+ ) {
323
+ const converted: IResult<ILlmSchema, IJsonSchemaTransformError> =
324
+ transform({
325
+ config: props.config,
326
+ components: props.components,
327
+ $defs: props.$defs,
328
+ schema: input.additionalProperties,
329
+ refAccessor: props.refAccessor,
330
+ accessor: `${accessor}.additionalProperties`,
331
+ });
332
+ if (converted.success === false) {
333
+ reasons.push(...converted.error.reasons);
334
+ return null;
335
+ }
336
+ return converted.value;
337
+ }
338
+ return props.config.strict === true
339
+ ? false
340
+ : input.additionalProperties;
341
+ })();
342
+ if (additionalProperties === null) return;
343
+ union.push({
344
+ ...input,
345
+ properties,
346
+ additionalProperties,
347
+ required: input.required ?? [],
348
+ description:
349
+ props.config.strict === true
350
+ ? JsonDescriptor.take(input)
351
+ : input.description,
352
+ });
353
+ } else if (OpenApiTypeChecker.isArray(input)) {
354
+ // ARRAY TYPE
355
+ const items: IResult<ILlmSchema, IJsonSchemaTransformError> = transform(
356
+ {
357
+ config: props.config,
358
+ components: props.components,
359
+ $defs: props.$defs,
360
+ schema: input.items,
361
+ refAccessor: props.refAccessor,
362
+ accessor: `${accessor}.items`,
363
+ },
364
+ );
365
+ if (items.success === false) {
366
+ reasons.push(...items.error.reasons);
367
+ return;
368
+ }
369
+ union.push(
370
+ props.config.strict === true
371
+ ? OpenApiConstraintShifter.shiftArray({
372
+ ...input,
373
+ items: items.value,
374
+ })
375
+ : {
376
+ ...input,
377
+ items: items.value,
378
+ },
379
+ );
380
+ } else if (OpenApiTypeChecker.isString(input))
381
+ union.push(
382
+ props.config.strict === true
383
+ ? OpenApiConstraintShifter.shiftString({ ...input })
384
+ : input,
385
+ );
386
+ else if (
387
+ OpenApiTypeChecker.isNumber(input) ||
388
+ OpenApiTypeChecker.isInteger(input)
389
+ )
390
+ union.push(
391
+ props.config.strict === true
392
+ ? OpenApiConstraintShifter.shiftNumeric({ ...input })
393
+ : input,
394
+ );
395
+ else if (OpenApiTypeChecker.isTuple(input))
396
+ return; // UNREACHABLE
397
+ else if (OpenApiTypeChecker.isConstant(input) === false)
398
+ union.push({ ...input });
399
+ };
400
+
401
+ visitConstant(props.schema);
402
+ visit(props.schema, props.accessor ?? "$input.schema");
403
+
404
+ if (reasons.length > 0)
405
+ return {
406
+ success: false,
407
+ error: {
408
+ method: "LlmSchemaConverter.schema",
409
+ message: "Failed to compose LLM schema",
410
+ reasons,
411
+ },
412
+ };
413
+ else if (union.length === 0)
414
+ return {
415
+ // unknown type
416
+ success: true,
417
+ value: {
418
+ ...attribute,
419
+ type: undefined,
420
+ },
421
+ };
422
+ else if (union.length === 1)
423
+ return {
424
+ // single type
425
+ success: true,
426
+ value: {
427
+ ...attribute,
428
+ ...union[0],
429
+ description:
430
+ props.config.strict === true &&
431
+ LlmTypeChecker.isReference(union[0]!)
432
+ ? undefined
433
+ : (union[0]!.description ?? attribute.description),
434
+ },
435
+ };
436
+ return {
437
+ success: true,
438
+ value: {
439
+ ...attribute,
440
+ anyOf: union.map((u) => ({
441
+ ...u,
442
+ description:
443
+ props.config.strict === true && LlmTypeChecker.isReference(u)
444
+ ? undefined
445
+ : u.description,
446
+ })),
447
+ "x-discriminator":
448
+ OpenApiTypeChecker.isOneOf(props.schema) &&
449
+ props.schema.discriminator !== undefined &&
450
+ props.schema.oneOf.length === union.length &&
451
+ union.every(
452
+ (e) => LlmTypeChecker.isReference(e) || LlmTypeChecker.isNull(e),
453
+ )
454
+ ? {
455
+ propertyName: props.schema.discriminator.propertyName,
456
+ mapping:
457
+ props.schema.discriminator.mapping !== undefined
458
+ ? Object.fromEntries(
459
+ Object.entries(props.schema.discriminator.mapping).map(
460
+ ([key, value]) => [
461
+ key,
462
+ `#/$defs/${value.split("/").at(-1)}`,
463
+ ],
464
+ ),
465
+ )
466
+ : undefined,
467
+ }
468
+ : undefined,
469
+ },
470
+ };
471
+ };
472
+
473
+ /* -----------------------------------------------------------
474
+ INVERTERS
475
+ ----------------------------------------------------------- */
476
+ /**
477
+ * Convert LLM schema back to OpenAPI schema.
478
+ *
479
+ * Restores constraint information from description tags and converts `$defs`
480
+ * references to `#/components/schemas`.
481
+ *
482
+ * @param props.components Target components (mutated with definitions)
483
+ * @param props.schema LLM schema to invert
484
+ * @param props.$defs LLM schema definitions
485
+ * @returns OpenAPI JSON schema
486
+ */
487
+ export const invert = (props: {
488
+ components: OpenApi.IComponents;
489
+ schema: ILlmSchema;
490
+ $defs: Record<string, ILlmSchema>;
491
+ }): OpenApi.IJsonSchema => {
492
+ const union: OpenApi.IJsonSchema[] = [];
493
+ const attribute: IJsonSchemaAttribute = {
494
+ title: props.schema.title,
495
+ description: props.schema.description,
496
+ deprecated: props.schema.deprecated,
497
+ readOnly: props.schema.readOnly,
498
+ writeOnly: props.schema.writeOnly,
499
+ example: props.schema.example,
500
+ examples: props.schema.examples,
501
+ ...Object.fromEntries(
502
+ Object.entries(props.schema).filter(
503
+ ([key, value]) => key.startsWith("x-") && value !== undefined,
504
+ ),
505
+ ),
506
+ };
507
+
508
+ const next = (schema: ILlmSchema): OpenApi.IJsonSchema =>
509
+ invert({
510
+ components: props.components,
511
+ $defs: props.$defs,
512
+ schema,
513
+ });
514
+ const visit = (schema: ILlmSchema): void => {
515
+ if (LlmTypeChecker.isArray(schema))
516
+ union.push({
517
+ ...schema,
518
+ ...LlmDescriptionInverter.array(schema.description),
519
+ items: next(schema.items),
520
+ });
521
+ else if (LlmTypeChecker.isObject(schema))
522
+ union.push({
523
+ ...schema,
524
+ properties: Object.fromEntries(
525
+ Object.entries(schema.properties).map(([key, value]) => [
526
+ key,
527
+ next(value),
528
+ ]),
529
+ ),
530
+ additionalProperties:
531
+ typeof schema.additionalProperties === "object" &&
532
+ schema.additionalProperties !== null
533
+ ? next(schema.additionalProperties)
534
+ : schema.additionalProperties,
535
+ });
536
+ else if (LlmTypeChecker.isAnyOf(schema)) schema.anyOf.forEach(visit);
537
+ else if (LlmTypeChecker.isReference(schema)) {
538
+ const key: string =
539
+ schema.$ref.split("#/$defs/")[1] ?? schema.$ref.split("/").at(-1)!;
540
+ if (props.components.schemas?.[key] === undefined) {
541
+ props.components.schemas ??= {};
542
+ props.components.schemas[key] = {};
543
+ props.components.schemas[key] = next(props.$defs[key] ?? {});
544
+ }
545
+ union.push({
546
+ ...schema,
547
+ $ref: `#/components/schemas/${key}`,
548
+ });
549
+ } else if (LlmTypeChecker.isBoolean(schema))
550
+ if (!!schema.enum?.length)
551
+ schema.enum.forEach((v) =>
552
+ union.push({
553
+ const: v,
554
+ }),
555
+ );
556
+ else union.push(schema);
557
+ else if (
558
+ LlmTypeChecker.isInteger(schema) ||
559
+ LlmTypeChecker.isNumber(schema)
560
+ )
561
+ if (!!schema.enum?.length)
562
+ schema.enum.forEach((v) =>
563
+ union.push({
564
+ const: v,
565
+ }),
566
+ );
567
+ else
568
+ union.push({
569
+ ...schema,
570
+ ...LlmDescriptionInverter.numeric(schema.description),
571
+ ...{ enum: undefined },
572
+ });
573
+ else if (LlmTypeChecker.isString(schema))
574
+ if (!!schema.enum?.length)
575
+ schema.enum.forEach((v) =>
576
+ union.push({
577
+ const: v,
578
+ }),
579
+ );
580
+ else
581
+ union.push({
582
+ ...schema,
583
+ ...LlmDescriptionInverter.string(schema.description),
584
+ ...{ enum: undefined },
585
+ });
586
+ else
587
+ union.push({
588
+ ...schema,
589
+ });
590
+ };
591
+ visit(props.schema);
592
+
593
+ return {
594
+ ...attribute,
595
+ ...(union.length === 0
596
+ ? { type: undefined }
597
+ : union.length === 1
598
+ ? { ...union[0] }
599
+ : {
600
+ oneOf: union.map((u) => ({ ...u, nullable: undefined })),
601
+ discriminator:
602
+ LlmTypeChecker.isAnyOf(props.schema) &&
603
+ props.schema["x-discriminator"] !== undefined
604
+ ? {
605
+ propertyName:
606
+ props.schema["x-discriminator"].propertyName,
607
+ mapping:
608
+ props.schema["x-discriminator"].mapping !== undefined
609
+ ? Object.fromEntries(
610
+ Object.entries(
611
+ props.schema["x-discriminator"].mapping,
612
+ ).map(([key, value]) => [
613
+ key,
614
+ `#/components/schemas/${value.split("/").at(-1)}`,
615
+ ]),
616
+ )
617
+ : undefined,
618
+ }
619
+ : undefined,
620
+ }),
621
+ } satisfies OpenApi.IJsonSchema;
622
+ };
623
+ }
624
+
625
+ const validateStrict = (
626
+ schema: OpenApi.IJsonSchema,
627
+ accessor: string,
628
+ ): IJsonSchemaTransformError.IReason[] => {
629
+ const reasons: IJsonSchemaTransformError.IReason[] = [];
630
+ if (OpenApiTypeChecker.isObject(schema)) {
631
+ if (!!schema.additionalProperties)
632
+ reasons.push({
633
+ schema: schema,
634
+ accessor: `${accessor}.additionalProperties`,
635
+ message:
636
+ "LLM does not allow additionalProperties in strict mode, the dynamic key typed object.",
637
+ });
638
+ for (const key of Object.keys(schema.properties ?? {}))
639
+ if (schema.required?.includes(key) === false)
640
+ reasons.push({
641
+ schema: schema,
642
+ accessor: `${accessor}.properties.${key}`,
643
+ message: "LLM does not allow optional properties in strict mode.",
644
+ });
645
+ }
646
+ return reasons;
647
+ };