@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,360 +1,361 @@
1
- import {
2
- IHttpLlmApplication,
3
- IHttpLlmFunction,
4
- IHttpMigrateApplication,
5
- IHttpMigrateRoute,
6
- IJsonSchemaTransformError,
7
- ILlmSchema,
8
- IResult,
9
- OpenApi,
10
- } from "@typia/interface";
11
-
12
- import { LlmSchemaConverter } from "../../converters/LlmSchemaConverter";
13
- import { LlmJson } from "../../utils";
14
- import { OpenApiValidator } from "../../validators/OpenApiValidator";
15
-
16
- /**
17
- * Composes {@link IHttpLlmApplication} from an {@link IHttpMigrateApplication}.
18
- *
19
- * Converts OpenAPI-migrated HTTP routes into LLM function calling schemas,
20
- * filtering out unsupported methods (HEAD) and content types
21
- * (multipart/form-data), and shortening function names to fit the configured
22
- * maximum length.
23
- */
24
- export namespace HttpLlmApplicationComposer {
25
- /**
26
- * Builds an {@link IHttpLlmApplication} from migrated HTTP routes.
27
- *
28
- * Iterates all routes, converts each to an {@link IHttpLlmFunction}, and
29
- * collects conversion errors. Applies function name shortening at the end.
30
- */
31
- export const application = (props: {
32
- migrate: IHttpMigrateApplication;
33
- config?: Partial<IHttpLlmApplication.IConfig>;
34
- }): IHttpLlmApplication => {
35
- // fill in config defaults
36
- const config: IHttpLlmApplication.IConfig = {
37
- maxLength: props.config?.maxLength ?? 64,
38
- equals: props.config?.equals ?? false,
39
- reference: props.config?.reference ?? true,
40
- strict: props.config?.strict ?? false,
41
- };
42
- // seed with pre-existing migration errors, excluding human-only endpoints
43
- const errors: IHttpLlmApplication.IError[] = props.migrate.errors
44
- .filter((e) => e.operation()["x-samchon-human"] !== true)
45
- .map((e) => ({
46
- method: e.method,
47
- path: e.path,
48
- messages: e.messages,
49
- operation: () => e.operation(),
50
- route: () => undefined,
51
- }));
52
- // convert each route to an LLM function, rejecting unsupported ones
53
- const functions: IHttpLlmFunction[] = props.migrate.routes
54
- .filter((e) => e.operation()["x-samchon-human"] !== true)
55
- .map((route, i) => {
56
- // reject HEAD — LLMs cannot interpret header-only responses
57
- if (route.method === "head") {
58
- errors.push({
59
- method: route.method,
60
- path: route.path,
61
- messages: ["HEAD method is not supported in the LLM application."],
62
- operation: () => route.operation(),
63
- route: () => route as any as IHttpMigrateRoute,
64
- });
65
- return null;
66
- // reject multipart/form-data — binary uploads not expressible in JSON Schema
67
- } else if (
68
- route.body?.type === "multipart/form-data" ||
69
- route.success?.type === "multipart/form-data"
70
- ) {
71
- errors.push({
72
- method: route.method,
73
- path: route.path,
74
- messages: [
75
- `The "multipart/form-data" content type is not supported in the LLM application.`,
76
- ],
77
- operation: () => route.operation(),
78
- route: () => route as any as IHttpMigrateRoute,
79
- });
80
- return null;
81
- }
82
- const localErrors: string[] = [];
83
- const func: IHttpLlmFunction | null = composeFunction({
84
- components: props.migrate.document().components,
85
- config,
86
- route,
87
- errors: localErrors,
88
- index: i,
89
- });
90
- if (func === null)
91
- errors.push({
92
- method: route.method,
93
- path: route.path,
94
- messages: localErrors,
95
- operation: () => route.operation(),
96
- route: () => route as any as IHttpMigrateRoute,
97
- });
98
- return func;
99
- })
100
- .filter((v): v is IHttpLlmFunction => v !== null);
101
-
102
- const app: IHttpLlmApplication = {
103
- config,
104
- functions,
105
- errors,
106
- };
107
- shorten(app, props.config?.maxLength ?? 64);
108
- return app;
109
- };
110
-
111
- /**
112
- * Converts a single {@link IHttpMigrateRoute} into an {@link IHttpLlmFunction}
113
- * by composing parameter/output schemas and validating function name
114
- * constraints.
115
- */
116
- const composeFunction = (props: {
117
- components: OpenApi.IComponents;
118
- route: IHttpMigrateRoute;
119
- config: IHttpLlmApplication.IConfig;
120
- errors: string[];
121
- index: number;
122
- }): IHttpLlmFunction | null => {
123
- // accessor prefix for error messages (mirrors OpenAPI document structure)
124
- const endpoint: string = `$input.paths[${JSON.stringify(props.route.path)}][${JSON.stringify(props.route.method)}]`;
125
- const operation: OpenApi.IOperation = props.route.operation();
126
- const description: string | undefined = concatDescription({
127
- summary: operation.summary,
128
- description: operation.description,
129
- });
130
- if ((description?.length ?? 0) > 1_024) {
131
- props.errors.push(
132
- `The description of the function is too long (must be equal or less than 1,024 characters, but ${description!.length.toLocaleString()} length).`,
133
- );
134
- }
135
-
136
- // build function name from route accessor, replacing forbidden chars
137
- const name: string = emend(props.route.accessor.join("_"));
138
- const isNameVariable: boolean = /^[a-zA-Z0-9_-]+$/.test(name);
139
- const isNameStartsWithNumber: boolean = /^[0-9]/.test(name[0] ?? "");
140
- if (isNameVariable === false)
141
- props.errors.push(
142
- `Elements of path (separated by '/') must be composed with alphabets, numbers, underscores, and hyphens`,
143
- );
144
- if (isNameStartsWithNumber === true)
145
- props.errors.push(`Function name cannot start with a number.`);
146
-
147
- //----
148
- // CONSTRUCT SCHEMAS
149
- //----
150
- // merge path parameters, query, and body into a single object schema
151
- const parameters: OpenApi.IJsonSchema.IObject = {
152
- type: "object",
153
- properties: Object.fromEntries([
154
- // path parameters (e.g., /users/:id)
155
- ...props.route.parameters.map(
156
- (s) =>
157
- [
158
- s.key,
159
- {
160
- ...s.schema,
161
- description: s.parameter().description ?? s.schema.description,
162
- },
163
- ] as const,
164
- ),
165
- // query parameters
166
- ...(props.route.query
167
- ? [
168
- [
169
- props.route.query.key,
170
- {
171
- ...props.route.query.schema,
172
- title:
173
- props.route.query.title() ?? props.route.query.schema.title,
174
- description:
175
- props.route.query.description() ??
176
- props.route.query.schema.description,
177
- },
178
- ] as const,
179
- ]
180
- : []),
181
- // request body
182
- ...(props.route.body
183
- ? [
184
- [
185
- props.route.body.key,
186
- {
187
- ...props.route.body.schema,
188
- description:
189
- props.route.body.description() ??
190
- props.route.body.schema.description,
191
- },
192
- ] as const,
193
- ]
194
- : []),
195
- ]),
196
- };
197
- parameters.required = Object.keys(parameters.properties ?? {});
198
-
199
- // convert merged object schema to LLM parameters
200
- const llmParameters: IResult<
201
- ILlmSchema.IParameters,
202
- IJsonSchemaTransformError
203
- > = LlmSchemaConverter.parameters({
204
- config: props.config,
205
- components: props.components,
206
- schema: parameters,
207
- accessor: `${endpoint}.parameters`,
208
- });
209
-
210
- // convert response schema to LLM output parameters
211
- const output:
212
- | IResult<ILlmSchema.IParameters, IJsonSchemaTransformError>
213
- | undefined = props.route.success
214
- ? LlmSchemaConverter.parameters({
215
- config: props.config,
216
- components: props.components,
217
- schema: props.route.success.schema as
218
- | OpenApi.IJsonSchema.IObject
219
- | OpenApi.IJsonSchema.IReference,
220
- accessor: `${endpoint}.responses[${JSON.stringify(props.route.success.status)}][${JSON.stringify(props.route.success.type)}].schema`,
221
- })
222
- : undefined;
223
-
224
- //----
225
- // CONVERSION
226
- //----
227
- // bail out if any validation or conversion failed
228
- if (
229
- output?.success === false ||
230
- llmParameters.success === false ||
231
- isNameVariable === false ||
232
- isNameStartsWithNumber === true ||
233
- (description?.length ?? 0) > 1_024
234
- ) {
235
- if (output?.success === false)
236
- props.errors.push(
237
- ...output.error.reasons.map((r) => `${r.accessor}: ${r.message}`),
238
- );
239
- if (llmParameters.success === false)
240
- props.errors.push(
241
- // rewrite internal accessor to match OpenAPI requestBody path
242
- ...llmParameters.error.reasons.map((r) => {
243
- const accessor: string = r.accessor.replace(
244
- `parameters.properties["body"]`,
245
- `requestBody.content[${JSON.stringify(props.route.body?.type ?? "application/json")}].schema`,
246
- );
247
- return `${accessor}: ${r.message}`;
248
- }),
249
- );
250
- return null;
251
- }
252
-
253
- // assemble the LLM function
254
- return {
255
- method: props.route.method as "get",
256
- path: props.route.path,
257
- name,
258
- parameters: llmParameters.value,
259
- output: output?.value,
260
- description,
261
- deprecated: operation.deprecated,
262
- tags: operation.tags,
263
- parse: (input: string) => LlmJson.parse(input, llmParameters.value),
264
- validate: OpenApiValidator.create({
265
- components: props.components,
266
- schema: parameters,
267
- required: true,
268
- equals: props.config.equals ?? false,
269
- }),
270
- route: () => props.route as any,
271
- operation: () => props.route.operation(),
272
- };
273
- };
274
-
275
- /**
276
- * Shortens function names exceeding the character limit.
277
- *
278
- * Tries progressively shorter accessor suffixes first, then falls back to
279
- * index-prefixed names, and finally UUID as a last resort.
280
- */
281
- export const shorten = (
282
- app: IHttpLlmApplication,
283
- limit: number = 64,
284
- ): void => {
285
- // collect all names for uniqueness checks
286
- const dictionary: Set<string> = new Set();
287
- const longFunctions: IHttpLlmFunction[] = [];
288
- for (const func of app.functions) {
289
- dictionary.add(func.name);
290
- if (func.name.length > limit) {
291
- longFunctions.push(func);
292
- }
293
- }
294
- if (longFunctions.length === 0) return;
295
-
296
- let index: number = 0;
297
- for (const func of longFunctions) {
298
- let success: boolean = false;
299
- const rename = (str: string) => {
300
- dictionary.delete(func.name);
301
- dictionary.add(str);
302
- func.name = str;
303
- success = true;
304
- };
305
- // try dropping leading accessor segments to shorten the name
306
- // (e.g., "api_users_getById" "users_getById" "getById")
307
- for (let i: number = 1; i < func.route().accessor.length; ++i) {
308
- const shortName: string = func.route().accessor.slice(i).join("_");
309
- if (shortName.length > limit - 8)
310
- continue; // reserve room for "_N_" prefix
311
- else if (dictionary.has(shortName) === false) rename(shortName);
312
- else {
313
- // name collision — prefix with a counter to disambiguate
314
- const newName: string = `_${index}_${shortName}`;
315
- if (dictionary.has(newName) === true) continue;
316
- rename(newName);
317
- ++index;
318
- }
319
- break;
320
- }
321
- // last resort — all suffix attempts failed or collided
322
- if (success === false) rename(randomFormatUuid());
323
- }
324
- };
325
- }
326
-
327
- const randomFormatUuid = (): string =>
328
- "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
329
- const r = (Math.random() * 16) | 0;
330
- const v = c === "x" ? r : (r & 0x3) | 0x8;
331
- return v.toString(16);
332
- });
333
-
334
- /** Replaces forbidden characters (`$`, `%`, `.`) with underscores. */
335
- const emend = (str: string): string => {
336
- for (const ch of FORBIDDEN) str = str.split(ch).join("_");
337
- return str;
338
- };
339
-
340
- const FORBIDDEN = ["$", "%", "."];
341
-
342
- /**
343
- * Concatenates summary and description into a single string.
344
- *
345
- * If both are present, joins them with a period and double newline, avoiding
346
- * duplication when the description already starts with the summary.
347
- */
348
- const concatDescription = (p: {
349
- summary?: string | undefined;
350
- description?: string | undefined;
351
- }): string | undefined => {
352
- if (!p.summary?.length || !p.description?.length)
353
- return p.summary || p.description;
354
- const summary: string = p.summary.endsWith(".")
355
- ? p.summary.slice(0, -1)
356
- : p.summary;
357
- return p.description.startsWith(summary)
358
- ? p.description
359
- : summary + ".\n\n" + p.description;
360
- };
1
+ import {
2
+ IHttpLlmApplication,
3
+ IHttpLlmFunction,
4
+ IHttpMigrateApplication,
5
+ IHttpMigrateRoute,
6
+ IJsonSchemaTransformError,
7
+ ILlmSchema,
8
+ IResult,
9
+ OpenApi,
10
+ } from "@typia/interface";
11
+
12
+ import { LlmSchemaConverter } from "../../converters/LlmSchemaConverter";
13
+ import { LlmJson } from "../../utils";
14
+ import { OpenApiValidator } from "../../validators/OpenApiValidator";
15
+
16
+ /**
17
+ * Composes {@link IHttpLlmApplication} from an {@link IHttpMigrateApplication}.
18
+ *
19
+ * Converts OpenAPI-migrated HTTP routes into LLM function calling schemas,
20
+ * filtering out unsupported methods (HEAD) and content types
21
+ * (multipart/form-data), and shortening function names to fit the configured
22
+ * maximum length.
23
+ */
24
+ export namespace HttpLlmApplicationComposer {
25
+ /**
26
+ * Builds an {@link IHttpLlmApplication} from migrated HTTP routes.
27
+ *
28
+ * Iterates all routes, converts each to an {@link IHttpLlmFunction}, and
29
+ * collects conversion errors. Applies function name shortening at the end.
30
+ */
31
+ export const application = (props: {
32
+ migrate: IHttpMigrateApplication;
33
+ config?: Partial<IHttpLlmApplication.IConfig>;
34
+ }): IHttpLlmApplication => {
35
+ // fill in config defaults
36
+ const config: IHttpLlmApplication.IConfig = {
37
+ maxLength: props.config?.maxLength ?? 64,
38
+ equals: props.config?.equals ?? false,
39
+ reference: props.config?.reference ?? true,
40
+ strict: props.config?.strict ?? false,
41
+ };
42
+ // seed with pre-existing migration errors, excluding human-only endpoints
43
+ const errors: IHttpLlmApplication.IError[] = props.migrate.errors
44
+ .filter((e) => e.operation()["x-samchon-human"] !== true)
45
+ .map((e) => ({
46
+ method: e.method,
47
+ path: e.path,
48
+ messages: e.messages,
49
+ operation: () => e.operation(),
50
+ route: () => undefined,
51
+ }));
52
+ // convert each route to an LLM function, rejecting unsupported ones
53
+ const functions: IHttpLlmFunction[] = props.migrate.routes
54
+ .filter((e) => e.operation()["x-samchon-human"] !== true)
55
+ .map((route, i) => {
56
+ // reject HEAD — LLMs cannot interpret header-only responses
57
+ if (route.method === "head") {
58
+ errors.push({
59
+ method: route.method,
60
+ path: route.path,
61
+ messages: ["HEAD method is not supported in the LLM application."],
62
+ operation: () => route.operation(),
63
+ route: () => route as any as IHttpMigrateRoute,
64
+ });
65
+ return null;
66
+ // reject multipart/form-data — binary uploads not expressible in JSON Schema
67
+ } else if (
68
+ route.body?.type === "multipart/form-data" ||
69
+ route.success?.type === "multipart/form-data"
70
+ ) {
71
+ errors.push({
72
+ method: route.method,
73
+ path: route.path,
74
+ messages: [
75
+ `The "multipart/form-data" content type is not supported in the LLM application.`,
76
+ ],
77
+ operation: () => route.operation(),
78
+ route: () => route as any as IHttpMigrateRoute,
79
+ });
80
+ return null;
81
+ }
82
+ const localErrors: string[] = [];
83
+ const func: IHttpLlmFunction | null = composeFunction({
84
+ components: props.migrate.document().components,
85
+ config,
86
+ route,
87
+ errors: localErrors,
88
+ index: i,
89
+ });
90
+ if (func === null)
91
+ errors.push({
92
+ method: route.method,
93
+ path: route.path,
94
+ messages: localErrors,
95
+ operation: () => route.operation(),
96
+ route: () => route as any as IHttpMigrateRoute,
97
+ });
98
+ return func;
99
+ })
100
+ .filter((v): v is IHttpLlmFunction => v !== null);
101
+
102
+ const app: IHttpLlmApplication = {
103
+ config,
104
+ functions,
105
+ errors,
106
+ };
107
+ shorten(app, props.config?.maxLength ?? 64);
108
+ return app;
109
+ };
110
+
111
+ /**
112
+ * Converts a single {@link IHttpMigrateRoute} into an {@link IHttpLlmFunction}
113
+ * by composing parameter/output schemas and validating function name
114
+ * constraints.
115
+ */
116
+ const composeFunction = (props: {
117
+ components: OpenApi.IComponents;
118
+ route: IHttpMigrateRoute;
119
+ config: IHttpLlmApplication.IConfig;
120
+ errors: string[];
121
+ index: number;
122
+ }): IHttpLlmFunction | null => {
123
+ // accessor prefix for error messages (mirrors OpenAPI document structure)
124
+ const endpoint: string = `$input.paths[${JSON.stringify(props.route.path)}][${JSON.stringify(props.route.method)}]`;
125
+ const operation: OpenApi.IOperation = props.route.operation();
126
+ const description: string | undefined = concatDescription({
127
+ summary: operation.summary,
128
+ description: operation.description,
129
+ });
130
+ if ((description?.length ?? 0) > 1_024) {
131
+ props.errors.push(
132
+ `The description of the function is too long (must be equal or less than 1,024 characters, but ${description!.length.toLocaleString()} length).`,
133
+ );
134
+ }
135
+
136
+ // build function name from route accessor, replacing forbidden chars
137
+ const name: string = emend(props.route.accessor.join("_"));
138
+ const isNameVariable: boolean = /^[a-zA-Z0-9_-]+$/.test(name);
139
+ const isNameStartsWithNumber: boolean = /^[0-9]/.test(name[0] ?? "");
140
+ if (isNameVariable === false)
141
+ props.errors.push(
142
+ `Elements of path (separated by '/') must be composed with alphabets, numbers, underscores, and hyphens`,
143
+ );
144
+ if (isNameStartsWithNumber === true)
145
+ props.errors.push(`Function name cannot start with a number.`);
146
+
147
+ //----
148
+ // CONSTRUCT SCHEMAS
149
+ //----
150
+ // merge path parameters, query, and body into a single object schema
151
+ const parameters: OpenApi.IJsonSchema.IObject = {
152
+ type: "object",
153
+ properties: Object.fromEntries([
154
+ // path parameters (e.g., /users/:id)
155
+ ...props.route.parameters.map(
156
+ (s) =>
157
+ [
158
+ s.key,
159
+ {
160
+ ...s.schema,
161
+ description: s.parameter().description ?? s.schema.description,
162
+ },
163
+ ] as const,
164
+ ),
165
+ // query parameters
166
+ ...(props.route.query
167
+ ? [
168
+ [
169
+ props.route.query.key,
170
+ {
171
+ ...props.route.query.schema,
172
+ title:
173
+ props.route.query.title() ?? props.route.query.schema.title,
174
+ description:
175
+ props.route.query.description() ??
176
+ props.route.query.schema.description,
177
+ },
178
+ ] as const,
179
+ ]
180
+ : []),
181
+ // request body
182
+ ...(props.route.body
183
+ ? [
184
+ [
185
+ props.route.body.key,
186
+ {
187
+ ...props.route.body.schema,
188
+ description:
189
+ props.route.body.description() ??
190
+ props.route.body.schema.description,
191
+ },
192
+ ] as const,
193
+ ]
194
+ : []),
195
+ ]),
196
+ };
197
+ parameters.required = Object.keys(parameters.properties ?? {});
198
+
199
+ // convert merged object schema to LLM parameters
200
+ const llmParameters: IResult<
201
+ ILlmSchema.IParameters,
202
+ IJsonSchemaTransformError
203
+ > = LlmSchemaConverter.parameters({
204
+ config: props.config,
205
+ components: props.components,
206
+ schema: parameters,
207
+ accessor: `${endpoint}.parameters`,
208
+ });
209
+
210
+ // convert response schema to LLM output parameters
211
+ const output:
212
+ | IResult<ILlmSchema.IParameters, IJsonSchemaTransformError>
213
+ | undefined = props.route.success
214
+ ? LlmSchemaConverter.parameters({
215
+ config: props.config,
216
+ components: props.components,
217
+ schema: props.route.success.schema as
218
+ | OpenApi.IJsonSchema.IObject
219
+ | OpenApi.IJsonSchema.IReference,
220
+ accessor: `${endpoint}.responses[${JSON.stringify(props.route.success.status)}][${JSON.stringify(props.route.success.type)}].schema`,
221
+ })
222
+ : undefined;
223
+
224
+ //----
225
+ // CONVERSION
226
+ //----
227
+ // bail out if any validation or conversion failed
228
+ if (
229
+ output?.success === false ||
230
+ llmParameters.success === false ||
231
+ isNameVariable === false ||
232
+ isNameStartsWithNumber === true ||
233
+ (description?.length ?? 0) > 1_024
234
+ ) {
235
+ if (output?.success === false)
236
+ props.errors.push(
237
+ ...output.error.reasons.map((r) => `${r.accessor}: ${r.message}`),
238
+ );
239
+ if (llmParameters.success === false)
240
+ props.errors.push(
241
+ // rewrite internal accessor to match OpenAPI requestBody path
242
+ ...llmParameters.error.reasons.map((r) => {
243
+ const accessor: string = r.accessor.replace(
244
+ `parameters.properties["body"]`,
245
+ `requestBody.content[${JSON.stringify(props.route.body?.type ?? "application/json")}].schema`,
246
+ );
247
+ return `${accessor}: ${r.message}`;
248
+ }),
249
+ );
250
+ return null;
251
+ }
252
+
253
+ // assemble the LLM function
254
+ return {
255
+ method: props.route.method as "get",
256
+ path: props.route.path,
257
+ name,
258
+ parameters: llmParameters.value,
259
+ output: output?.value,
260
+ description,
261
+ deprecated: operation.deprecated,
262
+ tags: operation.tags,
263
+ parse: (input: string) => LlmJson.parse(input, llmParameters.value),
264
+ coerce: (input: unknown) => LlmJson.coerce(input, llmParameters.value),
265
+ validate: OpenApiValidator.create({
266
+ components: props.components,
267
+ schema: parameters,
268
+ required: true,
269
+ equals: props.config.equals ?? false,
270
+ }),
271
+ route: () => props.route as any,
272
+ operation: () => props.route.operation(),
273
+ };
274
+ };
275
+
276
+ /**
277
+ * Shortens function names exceeding the character limit.
278
+ *
279
+ * Tries progressively shorter accessor suffixes first, then falls back to
280
+ * index-prefixed names, and finally UUID as a last resort.
281
+ */
282
+ export const shorten = (
283
+ app: IHttpLlmApplication,
284
+ limit: number = 64,
285
+ ): void => {
286
+ // collect all names for uniqueness checks
287
+ const dictionary: Set<string> = new Set();
288
+ const longFunctions: IHttpLlmFunction[] = [];
289
+ for (const func of app.functions) {
290
+ dictionary.add(func.name);
291
+ if (func.name.length > limit) {
292
+ longFunctions.push(func);
293
+ }
294
+ }
295
+ if (longFunctions.length === 0) return;
296
+
297
+ let index: number = 0;
298
+ for (const func of longFunctions) {
299
+ let success: boolean = false;
300
+ const rename = (str: string) => {
301
+ dictionary.delete(func.name);
302
+ dictionary.add(str);
303
+ func.name = str;
304
+ success = true;
305
+ };
306
+ // try dropping leading accessor segments to shorten the name
307
+ // (e.g., "api_users_getById" "users_getById" "getById")
308
+ for (let i: number = 1; i < func.route().accessor.length; ++i) {
309
+ const shortName: string = func.route().accessor.slice(i).join("_");
310
+ if (shortName.length > limit - 8)
311
+ continue; // reserve room for "_N_" prefix
312
+ else if (dictionary.has(shortName) === false) rename(shortName);
313
+ else {
314
+ // name collision prefix with a counter to disambiguate
315
+ const newName: string = `_${index}_${shortName}`;
316
+ if (dictionary.has(newName) === true) continue;
317
+ rename(newName);
318
+ ++index;
319
+ }
320
+ break;
321
+ }
322
+ // last resort all suffix attempts failed or collided
323
+ if (success === false) rename(randomFormatUuid());
324
+ }
325
+ };
326
+ }
327
+
328
+ const randomFormatUuid = (): string =>
329
+ "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
330
+ const r = (Math.random() * 16) | 0;
331
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
332
+ return v.toString(16);
333
+ });
334
+
335
+ /** Replaces forbidden characters (`$`, `%`, `.`) with underscores. */
336
+ const emend = (str: string): string => {
337
+ for (const ch of FORBIDDEN) str = str.split(ch).join("_");
338
+ return str;
339
+ };
340
+
341
+ const FORBIDDEN = ["$", "%", "."];
342
+
343
+ /**
344
+ * Concatenates summary and description into a single string.
345
+ *
346
+ * If both are present, joins them with a period and double newline, avoiding
347
+ * duplication when the description already starts with the summary.
348
+ */
349
+ const concatDescription = (p: {
350
+ summary?: string | undefined;
351
+ description?: string | undefined;
352
+ }): string | undefined => {
353
+ if (!p.summary?.length || !p.description?.length)
354
+ return p.summary || p.description;
355
+ const summary: string = p.summary.endsWith(".")
356
+ ? p.summary.slice(0, -1)
357
+ : p.summary;
358
+ return p.description.startsWith(summary)
359
+ ? p.description
360
+ : summary + ".\n\n" + p.description;
361
+ };