@typia/utils 12.0.0-dev.20260309 → 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 (92) hide show
  1. package/lib/http/internal/HttpLlmApplicationComposer.mjs +5 -1
  2. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  3. package/lib/index.mjs +9 -9
  4. package/lib/utils/LlmJson.mjs +9 -2
  5. package/lib/utils/LlmJson.mjs.map +1 -1
  6. package/lib/utils/internal/stringifyValidationFailure.js +17 -15
  7. package/lib/utils/internal/stringifyValidationFailure.js.map +1 -1
  8. package/lib/utils/internal/stringifyValidationFailure.mjs +17 -15
  9. package/lib/utils/internal/stringifyValidationFailure.mjs.map +1 -1
  10. package/lib/validators/internal/OpenApiOneOfValidator.mjs +5 -1
  11. package/lib/validators/internal/OpenApiOneOfValidator.mjs.map +1 -1
  12. package/package.json +2 -2
  13. package/src/converters/LlmSchemaConverter.ts +647 -647
  14. package/src/converters/OpenApiConverter.ts +285 -285
  15. package/src/converters/index.ts +5 -5
  16. package/src/converters/internal/LlmDescriptionInverter.ts +178 -178
  17. package/src/converters/internal/LlmParametersComposer.ts +52 -52
  18. package/src/converters/internal/OpenApiConstraintShifter.ts +154 -154
  19. package/src/converters/internal/OpenApiExclusiveEmender.ts +46 -46
  20. package/src/converters/internal/OpenApiV3Downgrader.ts +355 -355
  21. package/src/converters/internal/OpenApiV3Upgrader.ts +470 -470
  22. package/src/converters/internal/OpenApiV3_1Upgrader.ts +685 -685
  23. package/src/converters/internal/SwaggerV2Downgrader.ts +424 -424
  24. package/src/converters/internal/SwaggerV2Upgrader.ts +523 -523
  25. package/src/http/HttpError.ts +107 -107
  26. package/src/http/HttpLlm.ts +167 -167
  27. package/src/http/HttpMigration.ts +92 -92
  28. package/src/http/index.ts +3 -3
  29. package/src/http/internal/HttpLlmApplicationComposer.ts +361 -361
  30. package/src/http/internal/HttpLlmFunctionFetcher.ts +37 -37
  31. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  32. package/src/http/internal/HttpMigrateRouteAccessor.ts +135 -135
  33. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  34. package/src/http/internal/HttpMigrateRouteFetcher.ts +203 -203
  35. package/src/index.ts +4 -4
  36. package/src/utils/ArrayUtil.ts +42 -42
  37. package/src/utils/LlmJson.ts +141 -141
  38. package/src/utils/MapUtil.ts +15 -15
  39. package/src/utils/NamingConvention.ts +205 -205
  40. package/src/utils/Singleton.ts +17 -17
  41. package/src/utils/StringUtil.ts +14 -14
  42. package/src/utils/dedent.ts +57 -57
  43. package/src/utils/index.ts +8 -8
  44. package/src/utils/internal/EndpointUtil.ts +44 -44
  45. package/src/utils/internal/JsonDescriptor.ts +70 -70
  46. package/src/utils/internal/OpenApiTypeCheckerBase.ts +822 -822
  47. package/src/utils/internal/coerceLlmArguments.ts +314 -314
  48. package/src/utils/internal/parseLenientJson.ts +894 -894
  49. package/src/utils/internal/stringifyValidationFailure.ts +415 -411
  50. package/src/validators/LlmTypeChecker.ts +402 -402
  51. package/src/validators/OpenApiTypeChecker.ts +297 -297
  52. package/src/validators/OpenApiV3TypeChecker.ts +70 -70
  53. package/src/validators/OpenApiV3_1TypeChecker.ts +86 -86
  54. package/src/validators/OpenApiValidator.ts +94 -94
  55. package/src/validators/SwaggerV2TypeChecker.ts +71 -71
  56. package/src/validators/functional/_isBigintString.ts +8 -8
  57. package/src/validators/functional/_isFormatByte.ts +7 -7
  58. package/src/validators/functional/_isFormatDate.ts +3 -3
  59. package/src/validators/functional/_isFormatDateTime.ts +4 -4
  60. package/src/validators/functional/_isFormatDuration.ts +4 -4
  61. package/src/validators/functional/_isFormatEmail.ts +4 -4
  62. package/src/validators/functional/_isFormatHostname.ts +4 -4
  63. package/src/validators/functional/_isFormatIdnEmail.ts +4 -4
  64. package/src/validators/functional/_isFormatIdnHostname.ts +4 -4
  65. package/src/validators/functional/_isFormatIpv4.ts +4 -4
  66. package/src/validators/functional/_isFormatIpv6.ts +4 -4
  67. package/src/validators/functional/_isFormatIri.ts +3 -3
  68. package/src/validators/functional/_isFormatIriReference.ts +4 -4
  69. package/src/validators/functional/_isFormatJsonPointer.ts +3 -3
  70. package/src/validators/functional/_isFormatPassword.ts +1 -1
  71. package/src/validators/functional/_isFormatRegex.ts +8 -8
  72. package/src/validators/functional/_isFormatRelativeJsonPointer.ts +4 -4
  73. package/src/validators/functional/_isFormatTime.ts +4 -4
  74. package/src/validators/functional/_isFormatUri.ts +6 -6
  75. package/src/validators/functional/_isFormatUriReference.ts +5 -5
  76. package/src/validators/functional/_isFormatUriTemplate.ts +4 -4
  77. package/src/validators/functional/_isFormatUrl.ts +4 -4
  78. package/src/validators/functional/_isFormatUuid.ts +3 -3
  79. package/src/validators/functional/_isUniqueItems.ts +159 -159
  80. package/src/validators/index.ts +14 -14
  81. package/src/validators/internal/IOpenApiValidatorContext.ts +17 -17
  82. package/src/validators/internal/OpenApiArrayValidator.ts +49 -49
  83. package/src/validators/internal/OpenApiBooleanValidator.ts +11 -11
  84. package/src/validators/internal/OpenApiConstantValidator.ts +11 -11
  85. package/src/validators/internal/OpenApiIntegerValidator.ts +49 -49
  86. package/src/validators/internal/OpenApiNumberValidator.ts +48 -48
  87. package/src/validators/internal/OpenApiObjectValidator.ts +83 -83
  88. package/src/validators/internal/OpenApiOneOfValidator.ts +309 -309
  89. package/src/validators/internal/OpenApiSchemaNamingRule.ts +124 -124
  90. package/src/validators/internal/OpenApiStationValidator.ts +115 -115
  91. package/src/validators/internal/OpenApiStringValidator.ts +88 -88
  92. package/src/validators/internal/OpenApiTupleValidator.ts +55 -55
@@ -1,361 +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
- 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
- };
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
+ };