@typia/utils 12.0.0-dev.20260312-4 → 12.0.0-dev.20260313

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