@khanglvm/llm-router 1.3.1 → 2.0.0-beta.0
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/CHANGELOG.md +39 -0
- package/README.md +337 -41
- package/package.json +19 -3
- package/src/cli/router-module.js +7331 -3805
- package/src/cli/wrangler-toml.js +1 -1
- package/src/cli-entry.js +162 -24
- package/src/node/amp-client-config.js +426 -0
- package/src/node/coding-tool-config.js +763 -0
- package/src/node/config-store.js +49 -18
- package/src/node/instance-state.js +213 -12
- package/src/node/listen-port.js +5 -37
- package/src/node/local-server-settings.js +122 -0
- package/src/node/local-server.js +3 -2
- package/src/node/provider-probe.js +13 -0
- package/src/node/start-command.js +282 -40
- package/src/node/startup-manager.js +64 -29
- package/src/node/web-command.js +106 -0
- package/src/node/web-console-assets.js +26 -0
- package/src/node/web-console-client.js +56 -0
- package/src/node/web-console-dev-assets.js +258 -0
- package/src/node/web-console-server.js +3146 -0
- package/src/node/web-console-styles.generated.js +1 -0
- package/src/node/web-console-ui/config-editor-utils.js +616 -0
- package/src/node/web-console-ui/lib/utils.js +6 -0
- package/src/node/web-console-ui/rate-limit-utils.js +144 -0
- package/src/node/web-console-ui/select-search-utils.js +36 -0
- package/src/runtime/codex-request-transformer.js +46 -5
- package/src/runtime/codex-response-transformer.js +268 -35
- package/src/runtime/config.js +1394 -35
- package/src/runtime/handler/amp-gemini.js +913 -0
- package/src/runtime/handler/amp-response.js +308 -0
- package/src/runtime/handler/amp.js +290 -0
- package/src/runtime/handler/auth.js +17 -2
- package/src/runtime/handler/provider-call.js +168 -50
- package/src/runtime/handler/provider-translation.js +937 -26
- package/src/runtime/handler/request.js +149 -6
- package/src/runtime/handler/route-debug.js +22 -1
- package/src/runtime/handler.js +449 -9
- package/src/runtime/subscription-auth.js +1 -6
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/index.js +3 -1
- package/src/translator/request/openai-to-claude.js +217 -6
- package/src/translator/response/openai-to-claude.js +206 -58
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FORMATS } from "../../translator/index.js";
|
|
2
|
+
import { extractAmpGeminiRouteInfo } from "./amp-gemini.js";
|
|
2
3
|
import { toNonNegativeInteger } from "./utils.js";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024;
|
|
@@ -7,6 +8,130 @@ const MAX_MAX_REQUEST_BODY_BYTES = 20 * 1024 * 1024;
|
|
|
7
8
|
const DEFAULT_UPSTREAM_TIMEOUT_MS = 60_000;
|
|
8
9
|
const MIN_UPSTREAM_TIMEOUT_MS = 1_000;
|
|
9
10
|
const MAX_UPSTREAM_TIMEOUT_MS = 300_000;
|
|
11
|
+
const AMP_API_PROVIDER_PREFIX = "/api/provider/";
|
|
12
|
+
const AMP_MANAGEMENT_ROOT_PREFIXES = [
|
|
13
|
+
"/auth",
|
|
14
|
+
"/threads",
|
|
15
|
+
"/docs",
|
|
16
|
+
"/settings"
|
|
17
|
+
];
|
|
18
|
+
const AMP_MANAGEMENT_ROOT_EXACT_PATHS = new Set([
|
|
19
|
+
"/threads.rss",
|
|
20
|
+
"/news.rss"
|
|
21
|
+
]);
|
|
22
|
+
const AMP_MANAGEMENT_API_PREFIXES = [
|
|
23
|
+
"/api/auth",
|
|
24
|
+
"/api/user",
|
|
25
|
+
"/api/threads",
|
|
26
|
+
"/api/meta",
|
|
27
|
+
"/api/internal",
|
|
28
|
+
"/api/otel",
|
|
29
|
+
"/api/tab",
|
|
30
|
+
"/api/docs",
|
|
31
|
+
"/api/settings"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function hasPathPrefix(path, prefix) {
|
|
35
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveAmpProviderRoute(path, method) {
|
|
39
|
+
const isGet = method === "GET";
|
|
40
|
+
const isPost = method === "POST";
|
|
41
|
+
|
|
42
|
+
const geminiRoute = extractAmpGeminiRouteInfo(path);
|
|
43
|
+
if (geminiRoute) {
|
|
44
|
+
if (geminiRoute.type === "models" && isGet) {
|
|
45
|
+
return { type: "amp-gemini-models", clientType: "amp", providerHint: "google", requestKind: "gemini-models" };
|
|
46
|
+
}
|
|
47
|
+
if (geminiRoute.type === "model" && isGet) {
|
|
48
|
+
return { type: "amp-gemini-model", clientType: "amp", providerHint: "google", requestKind: "gemini-model", modelHint: geminiRoute.model };
|
|
49
|
+
}
|
|
50
|
+
if (geminiRoute.type === "request" && isPost) {
|
|
51
|
+
return {
|
|
52
|
+
type: "amp-gemini",
|
|
53
|
+
clientType: "amp",
|
|
54
|
+
providerHint: "google",
|
|
55
|
+
requestKind: "gemini",
|
|
56
|
+
modelHint: geminiRoute.model,
|
|
57
|
+
methodHint: geminiRoute.method,
|
|
58
|
+
streamHint: geminiRoute.stream
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!path.startsWith(AMP_API_PROVIDER_PREFIX)) return null;
|
|
64
|
+
|
|
65
|
+
const suffix = path.slice(AMP_API_PROVIDER_PREFIX.length);
|
|
66
|
+
const slashIndex = suffix.indexOf("/");
|
|
67
|
+
if (slashIndex <= 0) return null;
|
|
68
|
+
|
|
69
|
+
const providerHint = suffix.slice(0, slashIndex).trim().toLowerCase();
|
|
70
|
+
const providerPath = `/${suffix.slice(slashIndex + 1)}`;
|
|
71
|
+
|
|
72
|
+
if (isGet && ["/models", "/v1/models"].includes(providerPath)) {
|
|
73
|
+
return {
|
|
74
|
+
type: "models",
|
|
75
|
+
sourceFormat: providerHint === "anthropic" ? FORMATS.CLAUDE : FORMATS.OPENAI,
|
|
76
|
+
clientType: "amp",
|
|
77
|
+
providerHint,
|
|
78
|
+
requestKind: "models"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (providerHint === "google") {
|
|
83
|
+
return {
|
|
84
|
+
type: "amp-proxy",
|
|
85
|
+
clientType: "amp",
|
|
86
|
+
providerHint,
|
|
87
|
+
requestKind: "gemini-upstream-fallback"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isPost) return null;
|
|
92
|
+
|
|
93
|
+
if (["/messages", "/v1/messages"].includes(providerPath)) {
|
|
94
|
+
return {
|
|
95
|
+
type: "route",
|
|
96
|
+
sourceFormat: FORMATS.CLAUDE,
|
|
97
|
+
clientType: "amp",
|
|
98
|
+
providerHint,
|
|
99
|
+
requestKind: "messages"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (["/chat/completions", "/v1/chat/completions"].includes(providerPath)) {
|
|
104
|
+
return {
|
|
105
|
+
type: "route",
|
|
106
|
+
sourceFormat: FORMATS.OPENAI,
|
|
107
|
+
clientType: "amp",
|
|
108
|
+
providerHint,
|
|
109
|
+
requestKind: "chat-completions"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (["/completions", "/v1/completions"].includes(providerPath)) {
|
|
114
|
+
return {
|
|
115
|
+
type: "route",
|
|
116
|
+
sourceFormat: FORMATS.OPENAI,
|
|
117
|
+
clientType: "amp",
|
|
118
|
+
providerHint,
|
|
119
|
+
requestKind: "completions"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (["/responses", "/v1/responses"].includes(providerPath)) {
|
|
124
|
+
return {
|
|
125
|
+
type: "route",
|
|
126
|
+
sourceFormat: FORMATS.OPENAI,
|
|
127
|
+
clientType: "amp",
|
|
128
|
+
providerHint,
|
|
129
|
+
requestKind: "responses"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
10
135
|
|
|
11
136
|
export function resolveMaxRequestBodyBytes(env = {}) {
|
|
12
137
|
const configured = toNonNegativeInteger(
|
|
@@ -98,34 +223,52 @@ export function normalizePath(pathname) {
|
|
|
98
223
|
return pathname;
|
|
99
224
|
}
|
|
100
225
|
|
|
226
|
+
export function isAmpManagementPath(pathname) {
|
|
227
|
+
const path = normalizePath(pathname);
|
|
228
|
+
if (AMP_MANAGEMENT_ROOT_EXACT_PATHS.has(path)) return true;
|
|
229
|
+
if (AMP_MANAGEMENT_ROOT_PREFIXES.some((prefix) => hasPathPrefix(path, prefix))) return true;
|
|
230
|
+
return AMP_MANAGEMENT_API_PREFIXES.some((prefix) => hasPathPrefix(path, prefix));
|
|
231
|
+
}
|
|
232
|
+
|
|
101
233
|
export function resolveApiRoute(pathname, method) {
|
|
102
234
|
const path = normalizePath(pathname);
|
|
103
235
|
const isGet = method === "GET";
|
|
104
236
|
const isPost = method === "POST";
|
|
105
237
|
|
|
238
|
+
const ampRoute = resolveAmpProviderRoute(path, method);
|
|
239
|
+
if (ampRoute) return ampRoute;
|
|
240
|
+
|
|
106
241
|
if (isGet && ["/anthropic/v1/models", "/anthropic/models"].includes(path)) {
|
|
107
|
-
return { type: "models", sourceFormat: FORMATS.CLAUDE };
|
|
242
|
+
return { type: "models", sourceFormat: FORMATS.CLAUDE, requestKind: "models" };
|
|
108
243
|
}
|
|
109
244
|
|
|
110
245
|
if (isGet && ["/openai/v1/models", "/openai/models"].includes(path)) {
|
|
111
|
-
return { type: "models", sourceFormat: FORMATS.OPENAI };
|
|
246
|
+
return { type: "models", sourceFormat: FORMATS.OPENAI, requestKind: "models" };
|
|
112
247
|
}
|
|
113
248
|
|
|
114
249
|
if (isGet && ["/v1/models", "/models"].includes(path)) {
|
|
115
|
-
return { type: "models", sourceFormat: "auto" };
|
|
250
|
+
return { type: "models", sourceFormat: "auto", requestKind: "models" };
|
|
116
251
|
}
|
|
117
252
|
|
|
118
253
|
if (isPost && ["/v1/messages", "/messages", "/anthropic", "/anthropic/v1/messages", "/anthropic/messages"].includes(path)) {
|
|
119
|
-
return { type: "route", sourceFormat: FORMATS.CLAUDE };
|
|
254
|
+
return { type: "route", sourceFormat: FORMATS.CLAUDE, requestKind: "messages" };
|
|
120
255
|
}
|
|
121
256
|
|
|
122
257
|
if (isPost && ["/v1/chat/completions", "/chat/completions", "/openai", "/openai/v1/chat/completions", "/openai/chat/completions"].includes(path)) {
|
|
123
|
-
return { type: "route", sourceFormat: FORMATS.OPENAI };
|
|
258
|
+
return { type: "route", sourceFormat: FORMATS.OPENAI, requestKind: "chat-completions" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isPost && ["/v1/completions", "/completions", "/openai/v1/completions", "/openai/completions"].includes(path)) {
|
|
262
|
+
return { type: "route", sourceFormat: FORMATS.OPENAI, requestKind: "completions" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (isPost && ["/v1/responses", "/responses", "/openai/v1/responses", "/openai/responses"].includes(path)) {
|
|
266
|
+
return { type: "route", sourceFormat: FORMATS.OPENAI, requestKind: "responses" };
|
|
124
267
|
}
|
|
125
268
|
|
|
126
269
|
// Unified root endpoint: infer user format from request payload/headers.
|
|
127
270
|
if (isPost && ["/", "/v1", "/route", "/router"].includes(path)) {
|
|
128
|
-
return { type: "route", sourceFormat: "auto" };
|
|
271
|
+
return { type: "route", sourceFormat: "auto", requestKind: "unified" };
|
|
129
272
|
}
|
|
130
273
|
|
|
131
274
|
return null;
|
|
@@ -40,7 +40,9 @@ export function buildRouteDebugState(enabled, resolved) {
|
|
|
40
40
|
strategy: resolved?.routeStrategy || "ordered",
|
|
41
41
|
selectedCandidate: "",
|
|
42
42
|
skippedCandidates: [],
|
|
43
|
-
attempts: []
|
|
43
|
+
attempts: [],
|
|
44
|
+
toolTypes: "",
|
|
45
|
+
toolRouting: ""
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -70,6 +72,15 @@ export function setRouteSelectedCandidate(debugState, candidate, { overwrite = f
|
|
|
70
72
|
debugState.selectedCandidate = candidateRef(candidate);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
export function setRouteToolDebug(debugState, toolTypes, toolRouting = "") {
|
|
76
|
+
if (!debugState?.enabled) return;
|
|
77
|
+
const normalizedToolTypes = Array.isArray(toolTypes)
|
|
78
|
+
? toolTypes.map((value) => String(value || "").trim()).filter(Boolean)
|
|
79
|
+
: [];
|
|
80
|
+
debugState.toolTypes = normalizedToolTypes.join(",");
|
|
81
|
+
debugState.toolRouting = String(toolRouting || "").trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
export function withRouteDebugHeaders(response, debugState) {
|
|
74
85
|
if (!debugState?.enabled || !(response instanceof Response)) {
|
|
75
86
|
return response;
|
|
@@ -96,6 +107,16 @@ export function withRouteDebugHeaders(response, debugState) {
|
|
|
96
107
|
headers.set("x-llm-router-attempts", attempts);
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
const toolTypes = toSafeHeaderValue(debugState.toolTypes);
|
|
111
|
+
if (toolTypes) {
|
|
112
|
+
headers.set("x-llm-router-tool-types", toolTypes);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const toolRouting = toSafeHeaderValue(debugState.toolRouting);
|
|
116
|
+
if (toolRouting) {
|
|
117
|
+
headers.set("x-llm-router-tool-routing", toolRouting);
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
return new Response(response.body, {
|
|
100
121
|
status: response.status,
|
|
101
122
|
statusText: response.statusText,
|