@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +337 -41
  3. package/package.json +19 -3
  4. package/src/cli/router-module.js +7331 -3805
  5. package/src/cli/wrangler-toml.js +1 -1
  6. package/src/cli-entry.js +162 -24
  7. package/src/node/amp-client-config.js +426 -0
  8. package/src/node/coding-tool-config.js +763 -0
  9. package/src/node/config-store.js +49 -18
  10. package/src/node/instance-state.js +213 -12
  11. package/src/node/listen-port.js +5 -37
  12. package/src/node/local-server-settings.js +122 -0
  13. package/src/node/local-server.js +3 -2
  14. package/src/node/provider-probe.js +13 -0
  15. package/src/node/start-command.js +282 -40
  16. package/src/node/startup-manager.js +64 -29
  17. package/src/node/web-command.js +106 -0
  18. package/src/node/web-console-assets.js +26 -0
  19. package/src/node/web-console-client.js +56 -0
  20. package/src/node/web-console-dev-assets.js +258 -0
  21. package/src/node/web-console-server.js +3146 -0
  22. package/src/node/web-console-styles.generated.js +1 -0
  23. package/src/node/web-console-ui/config-editor-utils.js +616 -0
  24. package/src/node/web-console-ui/lib/utils.js +6 -0
  25. package/src/node/web-console-ui/rate-limit-utils.js +144 -0
  26. package/src/node/web-console-ui/select-search-utils.js +36 -0
  27. package/src/runtime/codex-request-transformer.js +46 -5
  28. package/src/runtime/codex-response-transformer.js +268 -35
  29. package/src/runtime/config.js +1394 -35
  30. package/src/runtime/handler/amp-gemini.js +913 -0
  31. package/src/runtime/handler/amp-response.js +308 -0
  32. package/src/runtime/handler/amp.js +290 -0
  33. package/src/runtime/handler/auth.js +17 -2
  34. package/src/runtime/handler/provider-call.js +168 -50
  35. package/src/runtime/handler/provider-translation.js +937 -26
  36. package/src/runtime/handler/request.js +149 -6
  37. package/src/runtime/handler/route-debug.js +22 -1
  38. package/src/runtime/handler.js +449 -9
  39. package/src/runtime/subscription-auth.js +1 -6
  40. package/src/shared/local-router-defaults.js +62 -0
  41. package/src/translator/index.js +3 -1
  42. package/src/translator/request/openai-to-claude.js +217 -6
  43. 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,