@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.
- package/lib/http/internal/HttpLlmApplicationComposer.d.ts +20 -0
- package/lib/http/internal/HttpLlmApplicationComposer.js +86 -36
- package/lib/http/internal/HttpLlmApplicationComposer.js.map +1 -1
- package/lib/http/internal/HttpLlmApplicationComposer.mjs +79 -31
- package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
- package/lib/http/internal/HttpMigrateRouteComposer.js +4 -12
- package/lib/http/internal/HttpMigrateRouteComposer.js.map +1 -1
- package/lib/http/internal/HttpMigrateRouteComposer.mjs +4 -12
- package/lib/http/internal/HttpMigrateRouteComposer.mjs.map +1 -1
- package/lib/index.mjs +10 -10
- package/lib/validators/internal/OpenApiOneOfValidator.mjs +1 -4
- package/lib/validators/internal/OpenApiOneOfValidator.mjs.map +1 -1
- package/package.json +2 -2
- package/src/http/internal/HttpLlmApplicationComposer.ts +93 -35
- package/src/http/internal/HttpMigrateRouteComposer.ts +4 -12
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
192
|
-
const output:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
}
|