@typia/utils 12.0.0-dev.20260303 → 12.0.0-dev.20260305

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