@khanglvm/llm-router 2.0.0-beta.1 → 2.0.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 +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export const LITELLM_CONTEXT_CATALOG_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
2
|
+
export const LITELLM_CONTEXT_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
|
|
4
|
+
function normalizeModelLookupName(value) {
|
|
5
|
+
return String(value || "").trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function tokenizeModelLookupName(value) {
|
|
9
|
+
return normalizeModelLookupName(value)
|
|
10
|
+
.split(/[^a-z0-9]+/g)
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildCanonicalModelLookupVariants(value, provider = "") {
|
|
15
|
+
const normalizedValue = normalizeModelLookupName(value);
|
|
16
|
+
const normalizedProvider = normalizeModelLookupName(provider);
|
|
17
|
+
const variants = new Set();
|
|
18
|
+
|
|
19
|
+
if (!normalizedValue) return variants;
|
|
20
|
+
variants.add(normalizedValue);
|
|
21
|
+
|
|
22
|
+
if (!normalizedProvider) return variants;
|
|
23
|
+
for (const separator of ["/", ":"]) {
|
|
24
|
+
const prefix = `${normalizedProvider}${separator}`;
|
|
25
|
+
if (!normalizedValue.startsWith(prefix)) continue;
|
|
26
|
+
const strippedValue = normalizedValue.slice(prefix.length).trim();
|
|
27
|
+
if (strippedValue) variants.add(strippedValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return variants;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isCanonicalExactModelNameMatch(query, candidate, provider = "") {
|
|
34
|
+
const queryVariants = buildCanonicalModelLookupVariants(query, provider);
|
|
35
|
+
const candidateVariants = buildCanonicalModelLookupVariants(candidate, provider);
|
|
36
|
+
|
|
37
|
+
if (queryVariants.size === 0 || candidateVariants.size === 0) return false;
|
|
38
|
+
for (const variant of queryVariants) {
|
|
39
|
+
if (candidateVariants.has(variant)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scoreLooseModelNameMatch(query, candidate) {
|
|
45
|
+
const normalizedQuery = normalizeModelLookupName(query);
|
|
46
|
+
const normalizedCandidate = normalizeModelLookupName(candidate);
|
|
47
|
+
if (!normalizedQuery || !normalizedCandidate) return 0;
|
|
48
|
+
if (normalizedQuery === normalizedCandidate) return 1000;
|
|
49
|
+
if (normalizedCandidate.includes(normalizedQuery)) return 600 - (normalizedCandidate.length - normalizedQuery.length);
|
|
50
|
+
if (normalizedQuery.includes(normalizedCandidate)) return 500 - (normalizedQuery.length - normalizedCandidate.length);
|
|
51
|
+
|
|
52
|
+
const queryTokens = tokenizeModelLookupName(normalizedQuery);
|
|
53
|
+
const candidateTokens = tokenizeModelLookupName(normalizedCandidate);
|
|
54
|
+
if (queryTokens.length === 0 || candidateTokens.length === 0) return 0;
|
|
55
|
+
|
|
56
|
+
let score = 0;
|
|
57
|
+
for (const token of queryTokens) {
|
|
58
|
+
if (candidateTokens.includes(token)) {
|
|
59
|
+
score += token.length * 10;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const partialMatch = candidateTokens.find((candidateToken) => candidateToken.includes(token) || token.includes(candidateToken));
|
|
63
|
+
if (partialMatch) score += Math.min(token.length, partialMatch.length) * 4;
|
|
64
|
+
}
|
|
65
|
+
return score;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractLiteLlmContextWindow(entry) {
|
|
69
|
+
const maxInputTokens = Number(entry?.max_input_tokens);
|
|
70
|
+
if (Number.isFinite(maxInputTokens) && maxInputTokens > 0) return Math.floor(maxInputTokens);
|
|
71
|
+
const maxTokens = Number(entry?.max_tokens);
|
|
72
|
+
if (Number.isFinite(maxTokens) && maxTokens > 0) return Math.floor(maxTokens);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createLiteLlmLookupResult(modelName, entry = {}) {
|
|
77
|
+
return {
|
|
78
|
+
model: String(modelName || "").trim(),
|
|
79
|
+
contextWindow: extractLiteLlmContextWindow(entry),
|
|
80
|
+
provider: String(entry?.litellm_provider || "").trim(),
|
|
81
|
+
mode: String(entry?.mode || "").trim()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractMedianContextWindow(exactMatch, suggestions = []) {
|
|
86
|
+
const exactContextWindow = Number(exactMatch?.contextWindow);
|
|
87
|
+
if (Number.isFinite(exactContextWindow) && exactContextWindow > 0) {
|
|
88
|
+
return Math.floor(exactContextWindow);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const contextWindows = (Array.isArray(suggestions) ? suggestions : [])
|
|
92
|
+
.map((entry) => Number(entry?.contextWindow))
|
|
93
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
94
|
+
.sort((left, right) => left - right);
|
|
95
|
+
|
|
96
|
+
if (contextWindows.length === 0) return null;
|
|
97
|
+
return contextWindows[Math.floor((contextWindows.length - 1) / 2)];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createLiteLlmContextLookupHelper({
|
|
101
|
+
fetchImpl = fetch,
|
|
102
|
+
catalogUrl = LITELLM_CONTEXT_CATALOG_URL,
|
|
103
|
+
cacheTtlMs = LITELLM_CONTEXT_CACHE_TTL_MS
|
|
104
|
+
} = {}) {
|
|
105
|
+
let cachedCatalog = null;
|
|
106
|
+
let cachedAt = 0;
|
|
107
|
+
let inFlightPromise = null;
|
|
108
|
+
|
|
109
|
+
async function loadCatalog() {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (cachedCatalog && (now - cachedAt) < cacheTtlMs) {
|
|
112
|
+
return cachedCatalog;
|
|
113
|
+
}
|
|
114
|
+
if (inFlightPromise) return inFlightPromise;
|
|
115
|
+
|
|
116
|
+
inFlightPromise = (async () => {
|
|
117
|
+
const response = await fetchImpl(catalogUrl);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(`LiteLLM context catalog request failed with status ${response.status}.`);
|
|
120
|
+
}
|
|
121
|
+
const payload = await response.json();
|
|
122
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
123
|
+
throw new Error("LiteLLM context catalog returned an invalid payload.");
|
|
124
|
+
}
|
|
125
|
+
cachedCatalog = payload;
|
|
126
|
+
cachedAt = Date.now();
|
|
127
|
+
return cachedCatalog;
|
|
128
|
+
})();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
return await inFlightPromise;
|
|
132
|
+
} finally {
|
|
133
|
+
inFlightPromise = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return async function lookupLiteLlmContextWindow({ models = [], limit = 8 } = {}) {
|
|
138
|
+
const catalog = await loadCatalog();
|
|
139
|
+
const catalogEntries = Object.entries(catalog);
|
|
140
|
+
const resolvedLimit = Math.max(1, Math.min(Number(limit) || 8, 20));
|
|
141
|
+
|
|
142
|
+
return (Array.isArray(models) ? models : [])
|
|
143
|
+
.map((model) => String(model || "").trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((modelName) => {
|
|
146
|
+
const rankedMatches = catalogEntries
|
|
147
|
+
.map(([candidateName, entry]) => ({
|
|
148
|
+
result: createLiteLlmLookupResult(candidateName, entry),
|
|
149
|
+
score: scoreLooseModelNameMatch(modelName, candidateName)
|
|
150
|
+
}))
|
|
151
|
+
.filter((entry) => entry.score > 0 && Number.isFinite(entry.result.contextWindow))
|
|
152
|
+
.sort((left, right) => {
|
|
153
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
154
|
+
return left.result.model.localeCompare(right.result.model);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const exactMatch = rankedMatches.find((entry) => isCanonicalExactModelNameMatch(
|
|
158
|
+
modelName,
|
|
159
|
+
entry.result.model,
|
|
160
|
+
entry.result.provider
|
|
161
|
+
))?.result || null;
|
|
162
|
+
|
|
163
|
+
const suggestions = rankedMatches
|
|
164
|
+
.filter((entry) => {
|
|
165
|
+
if (!exactMatch) return true;
|
|
166
|
+
return !(
|
|
167
|
+
entry.result.model === exactMatch.model
|
|
168
|
+
&& entry.result.provider === exactMatch.provider
|
|
169
|
+
&& entry.result.mode === exactMatch.mode
|
|
170
|
+
&& entry.result.contextWindow === exactMatch.contextWindow
|
|
171
|
+
);
|
|
172
|
+
})
|
|
173
|
+
.slice(0, resolvedLimit)
|
|
174
|
+
.map((entry) => entry.result);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
query: modelName,
|
|
178
|
+
exactMatch,
|
|
179
|
+
suggestions,
|
|
180
|
+
medianContextWindow: extractMedianContextWindow(exactMatch, suggestions)
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/node/local-server.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Readable } from "node:stream";
|
|
|
9
9
|
import { createFetchHandler } from "../runtime/handler.js";
|
|
10
10
|
import { readConfigFile, getDefaultConfigPath } from "./config-store.js";
|
|
11
11
|
import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
12
|
+
import { readActivityLogSettings } from "../shared/local-router-defaults.js";
|
|
13
|
+
import { appendActivityLogEntry, resolveActivityLogPath } from "./activity-log.js";
|
|
12
14
|
|
|
13
15
|
const DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS = 300;
|
|
14
16
|
const MAX_CONFIG_RELOAD_DEBOUNCE_MS = 5000;
|
|
@@ -237,6 +239,7 @@ export async function startLocalRouteServer({
|
|
|
237
239
|
port = FIXED_LOCAL_ROUTER_PORT,
|
|
238
240
|
host = FIXED_LOCAL_ROUTER_HOST,
|
|
239
241
|
configPath = getDefaultConfigPath(),
|
|
242
|
+
activityLogPath = "",
|
|
240
243
|
watchConfig = true,
|
|
241
244
|
configReloadDebounceMs = process.env.LLM_ROUTER_CONFIG_RELOAD_DEBOUNCE_MS,
|
|
242
245
|
validateConfig,
|
|
@@ -245,20 +248,37 @@ export async function startLocalRouteServer({
|
|
|
245
248
|
requireAuth = false
|
|
246
249
|
} = {}) {
|
|
247
250
|
const reloadDebounceMs = resolveReloadDebounceMs(configReloadDebounceMs);
|
|
251
|
+
const resolvedActivityLogPath = resolveActivityLogPath(configPath, activityLogPath);
|
|
252
|
+
let activityLogEnabled = true;
|
|
248
253
|
const configStore = createLiveConfigStore({
|
|
249
254
|
configPath,
|
|
250
255
|
watchConfig,
|
|
251
256
|
reloadDebounceMs,
|
|
252
257
|
validateConfig,
|
|
253
|
-
onReload:
|
|
258
|
+
onReload: (nextConfig, reason) => {
|
|
259
|
+
activityLogEnabled = readActivityLogSettings(nextConfig).enabled;
|
|
260
|
+
if (typeof onConfigReload === "function") {
|
|
261
|
+
onConfigReload(nextConfig, reason);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
254
264
|
onReloadError: onConfigReloadError
|
|
255
265
|
});
|
|
256
|
-
await configStore.getConfig();
|
|
266
|
+
const initialConfig = await configStore.getConfig();
|
|
267
|
+
activityLogEnabled = readActivityLogSettings(initialConfig).enabled;
|
|
257
268
|
|
|
258
269
|
const fetchHandler = createFetchHandler({
|
|
259
270
|
ignoreAuth: !requireAuth,
|
|
260
271
|
getConfig: () => configStore.getConfig(),
|
|
261
|
-
defaultStateStoreBackend: "file"
|
|
272
|
+
defaultStateStoreBackend: "file",
|
|
273
|
+
onActivityLog: (entry) => {
|
|
274
|
+
if (!activityLogEnabled) return;
|
|
275
|
+
void appendActivityLogEntry(resolvedActivityLogPath, {
|
|
276
|
+
...entry,
|
|
277
|
+
source: entry?.source || "runtime"
|
|
278
|
+
}).catch((error) => {
|
|
279
|
+
console.warn(`[llm-router] Failed writing activity log: ${formatError(error)}`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
262
282
|
});
|
|
263
283
|
|
|
264
284
|
const fallbackHost = formatHostForUrl(host, port);
|
package/src/node/port-reclaim.js
CHANGED
|
@@ -130,7 +130,7 @@ export async function stopStartupManagedListener({ port, line, error }, deps = {
|
|
|
130
130
|
|
|
131
131
|
if (!shouldStopStartup) return { ok: true, attempted: false };
|
|
132
132
|
|
|
133
|
-
line(`Detected startup-managed
|
|
133
|
+
line(`Detected a startup-managed LLM Router instance on port ${port}. Stopping the startup service before reclaim.`);
|
|
134
134
|
try {
|
|
135
135
|
await stopStartupFn();
|
|
136
136
|
await clearRuntimeStateFn();
|
|
@@ -140,7 +140,7 @@ export async function stopStartupManagedListener({ port, line, error }, deps = {
|
|
|
140
140
|
return {
|
|
141
141
|
ok: false,
|
|
142
142
|
attempted: true,
|
|
143
|
-
errorMessage: `Port ${port} is occupied by a startup-managed
|
|
143
|
+
errorMessage: `Port ${port} is occupied by a startup-managed LLM Router service and could not be stopped automatically. Stop it with 'llr stop' or 'llr config --operation=startup-uninstall' and retry.`
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -286,11 +286,11 @@ async function handoffToStartupManagedWithLatest({
|
|
|
286
286
|
if (!stopped?.ok) {
|
|
287
287
|
return {
|
|
288
288
|
ok: false,
|
|
289
|
-
errorMessage: stopped?.reason || `Failed to stop existing
|
|
289
|
+
errorMessage: stopped?.reason || `Failed to stop existing LLM Router pid ${activeRuntime.pid}.`
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
292
|
await clearRuntimeStateFn({ pid: activeRuntime.pid });
|
|
293
|
-
line(`Stopped manual
|
|
293
|
+
line(`Stopped manual LLM Router on http://${activeRuntime.host}:${activeRuntime.port} so the startup service can own the router.`);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
const reclaimed = await reclaimPortFn({ port: startArgs.port, line, error });
|
|
@@ -319,7 +319,7 @@ async function handoffToStartupManagedWithLatest({
|
|
|
319
319
|
if (!runtime) {
|
|
320
320
|
return {
|
|
321
321
|
ok: false,
|
|
322
|
-
errorMessage: `Startup-managed
|
|
322
|
+
errorMessage: `Startup-managed LLM Router did not become ready on http://${startArgs.host}:${startArgs.port}.`
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
325
|
return {
|
|
@@ -333,7 +333,7 @@ async function handoffToStartupManagedWithLatest({
|
|
|
333
333
|
error(`Failed restarting startup-managed service: ${message}`);
|
|
334
334
|
return {
|
|
335
335
|
ok: false,
|
|
336
|
-
errorMessage: `Failed to restart startup-managed
|
|
336
|
+
errorMessage: `Failed to restart the startup-managed LLM Router instance with the latest installed version: ${message}`
|
|
337
337
|
};
|
|
338
338
|
}
|
|
339
339
|
}
|
|
@@ -394,7 +394,7 @@ export async function runStartCommand(options = {}) {
|
|
|
394
394
|
exitCode: 2,
|
|
395
395
|
errorMessage: [
|
|
396
396
|
`Config file not found: ${configPath}`,
|
|
397
|
-
"Run '
|
|
397
|
+
"Run 'llr config' to create provider config or 'llr -h' for help."
|
|
398
398
|
].join("\n")
|
|
399
399
|
};
|
|
400
400
|
}
|
|
@@ -439,7 +439,7 @@ export async function runStartCommand(options = {}) {
|
|
|
439
439
|
exitCode: 2,
|
|
440
440
|
errorMessage: [
|
|
441
441
|
`No providers configured in ${configPath}`,
|
|
442
|
-
"Run '
|
|
442
|
+
"Run 'llr config' to add a provider or 'llr -h' for help."
|
|
443
443
|
].join("\n")
|
|
444
444
|
};
|
|
445
445
|
}
|
|
@@ -450,7 +450,7 @@ export async function runStartCommand(options = {}) {
|
|
|
450
450
|
exitCode: 2,
|
|
451
451
|
errorMessage: [
|
|
452
452
|
`Local auth requires masterKey in ${configPath}.`,
|
|
453
|
-
"Run '
|
|
453
|
+
"Run 'llr config --operation=set-master-key --master-key=...' or start without --require-auth."
|
|
454
454
|
].join("\n")
|
|
455
455
|
};
|
|
456
456
|
}
|
|
@@ -494,7 +494,7 @@ export async function runStartCommand(options = {}) {
|
|
|
494
494
|
ok: true,
|
|
495
495
|
exitCode: 0,
|
|
496
496
|
data: [
|
|
497
|
-
`Startup-managed
|
|
497
|
+
`Startup-managed LLM Router is active on http://${handoff.runtime.host}:${handoff.runtime.port}.`,
|
|
498
498
|
`manager=${handoff.detail?.manager || startup.manager || "unknown"}`,
|
|
499
499
|
`service=${handoff.detail?.serviceId || startup.serviceId || "unknown"}`
|
|
500
500
|
].join("\n")
|
|
@@ -538,7 +538,7 @@ export async function runStartCommand(options = {}) {
|
|
|
538
538
|
startArgs: buildStartArgs({ configPath, ...nextLocalServer })
|
|
539
539
|
});
|
|
540
540
|
if (!launch.ok) {
|
|
541
|
-
error(`Failed to relaunch
|
|
541
|
+
error(`Failed to relaunch LLM Router after the config runtime change: ${launch.error instanceof Error ? launch.error.message : String(launch.error)}`);
|
|
542
542
|
process.exit(1);
|
|
543
543
|
return;
|
|
544
544
|
}
|
|
@@ -562,7 +562,7 @@ export async function runStartCommand(options = {}) {
|
|
|
562
562
|
return {
|
|
563
563
|
ok: false,
|
|
564
564
|
exitCode: 1,
|
|
565
|
-
errorMessage: `Another
|
|
565
|
+
errorMessage: `Another LLM Router instance is already running at http://${activeRuntime.host}:${activeRuntime.port}. Stop it before starting a new one.`
|
|
566
566
|
};
|
|
567
567
|
}
|
|
568
568
|
|
|
@@ -574,7 +574,7 @@ export async function runStartCommand(options = {}) {
|
|
|
574
574
|
return {
|
|
575
575
|
ok: false,
|
|
576
576
|
exitCode: 1,
|
|
577
|
-
errorMessage: `Failed to start
|
|
577
|
+
errorMessage: `Failed to start LLM Router on http://${host}:${port}: ${startError instanceof Error ? startError.message : String(startError)}`
|
|
578
578
|
};
|
|
579
579
|
}
|
|
580
580
|
|
|
@@ -622,7 +622,7 @@ export async function runStartCommand(options = {}) {
|
|
|
622
622
|
ok: true,
|
|
623
623
|
exitCode: 0,
|
|
624
624
|
data: [
|
|
625
|
-
"Restarted startup-managed
|
|
625
|
+
"Restarted the startup-managed LLM Router instance with the latest installed version.",
|
|
626
626
|
`manager=${restarted.detail?.manager || "unknown"}`,
|
|
627
627
|
`service=${restarted.detail?.serviceId || "unknown"}`
|
|
628
628
|
].join("\n")
|
|
@@ -633,7 +633,7 @@ export async function runStartCommand(options = {}) {
|
|
|
633
633
|
return {
|
|
634
634
|
ok: true,
|
|
635
635
|
exitCode: 0,
|
|
636
|
-
data: `Startup-managed
|
|
636
|
+
data: `Startup-managed LLM Router is still running on port ${port}. Exiting without changes.`
|
|
637
637
|
};
|
|
638
638
|
}
|
|
639
639
|
|
|
@@ -647,7 +647,7 @@ export async function runStartCommand(options = {}) {
|
|
|
647
647
|
};
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
line("Startup-managed instance stopped. Starting
|
|
650
|
+
line("Startup-managed instance stopped. Starting LLM Router in this terminal...");
|
|
651
651
|
const takeoverStart = await attemptServerStartAfterStartupStop(buildLocalServerOptions, { startLocalRouteServer: startLocalRouteServerFn });
|
|
652
652
|
if (takeoverStart.ok) {
|
|
653
653
|
server = takeoverStart.server;
|
|
@@ -656,7 +656,7 @@ export async function runStartCommand(options = {}) {
|
|
|
656
656
|
return {
|
|
657
657
|
ok: false,
|
|
658
658
|
exitCode: 1,
|
|
659
|
-
errorMessage: `Failed to start
|
|
659
|
+
errorMessage: `Failed to start LLM Router on http://${host}:${port}: ${takeoverStart.error instanceof Error ? takeoverStart.error.message : String(takeoverStart.error)}`
|
|
660
660
|
};
|
|
661
661
|
}
|
|
662
662
|
}
|
|
@@ -682,7 +682,7 @@ export async function runStartCommand(options = {}) {
|
|
|
682
682
|
return {
|
|
683
683
|
ok: false,
|
|
684
684
|
exitCode: 1,
|
|
685
|
-
errorMessage: `Failed to start
|
|
685
|
+
errorMessage: `Failed to start LLM Router after reclaiming port ${port}: ${retryError instanceof Error ? retryError.message : String(retryError)}`
|
|
686
686
|
};
|
|
687
687
|
}
|
|
688
688
|
}
|
|
@@ -763,7 +763,7 @@ export async function runStartCommand(options = {}) {
|
|
|
763
763
|
binaryState = nextState;
|
|
764
764
|
|
|
765
765
|
if (managedByStartup) {
|
|
766
|
-
line(`Detected
|
|
766
|
+
line(`Detected LLM Router update (${from} -> ${to}). Exiting for the startup manager to relaunch the latest version.`);
|
|
767
767
|
void shutdown().then(() => {
|
|
768
768
|
process.exit(0);
|
|
769
769
|
});
|
|
@@ -774,7 +774,7 @@ export async function runStartCommand(options = {}) {
|
|
|
774
774
|
if (!cliPath) {
|
|
775
775
|
if (!binaryNoticeSent) {
|
|
776
776
|
binaryNoticeSent = true;
|
|
777
|
-
line(`Detected
|
|
777
|
+
line(`Detected LLM Router update (${from} -> ${to}). Restart this process to run the new version.`);
|
|
778
778
|
}
|
|
779
779
|
return;
|
|
780
780
|
}
|
|
@@ -782,22 +782,22 @@ export async function runStartCommand(options = {}) {
|
|
|
782
782
|
binaryRelaunching = true;
|
|
783
783
|
void (async () => {
|
|
784
784
|
try {
|
|
785
|
-
line(`Detected
|
|
785
|
+
line(`Detected LLM Router update (${from} -> ${to}). Relaunching the latest version...`);
|
|
786
786
|
await shutdown();
|
|
787
787
|
const launch = await spawnReplacementCli({
|
|
788
788
|
cliPath,
|
|
789
789
|
startArgs: buildStartArgs({ configPath, host, port, watchConfig, watchBinary, requireAuth })
|
|
790
790
|
});
|
|
791
791
|
if (!launch.ok) {
|
|
792
|
-
error(`Failed to relaunch updated
|
|
792
|
+
error(`Failed to relaunch the updated LLM Router process: ${launch.error instanceof Error ? launch.error.message : String(launch.error)}`);
|
|
793
793
|
process.exit(1);
|
|
794
794
|
return;
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
-
line(`Started updated
|
|
797
|
+
line(`Started the updated LLM Router process (pid ${launch.pid || "unknown"}).`);
|
|
798
798
|
process.exit(0);
|
|
799
799
|
} catch (relaunchError) {
|
|
800
|
-
error(`Failed during
|
|
800
|
+
error(`Failed during LLM Router auto-relaunch: ${relaunchError instanceof Error ? relaunchError.message : String(relaunchError)}`);
|
|
801
801
|
process.exit(1);
|
|
802
802
|
}
|
|
803
803
|
})();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OS startup integration for
|
|
2
|
+
* OS startup integration for LLM Router.
|
|
3
3
|
* Supports macOS LaunchAgent and Linux systemd --user service.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -58,14 +58,14 @@ export function resolveStartupCliEntryPath({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const nodeBinDir = path.dirname(execPath);
|
|
61
|
-
for (const binName of ["llm-router", "llm-router-route"]) {
|
|
61
|
+
for (const binName of ["llr", "llm-router", "llm-router-route"]) {
|
|
62
62
|
const candidate = path.join(nodeBinDir, binName);
|
|
63
63
|
if (exists(candidate)) return candidate;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
if (envCliPath) return envCliPath;
|
|
67
67
|
if (argvCliPath) return path.resolve(argvCliPath);
|
|
68
|
-
throw new Error("Unable to resolve
|
|
68
|
+
throw new Error("Unable to resolve the LLM Router CLI entry path.");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function makeExecArgs({ configPath }) {
|
package/src/node/web-command.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function runWebCommand(options = {}) {
|
|
|
67
67
|
return {
|
|
68
68
|
ok: false,
|
|
69
69
|
exitCode: 1,
|
|
70
|
-
errorMessage: `Failed to start
|
|
70
|
+
errorMessage: `Failed to start the LLM Router web console: ${startError instanceof Error ? startError.message : String(startError)}`
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|