@khanglvm/llm-router 2.2.3 → 2.2.5
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/package.json +1 -1
- package/src/cli/router-module.js +5 -0
- package/src/cli-entry.js +9 -0
- package/src/node/upgrade-command.js +128 -0
- package/src/node/web-command.js +68 -17
package/package.json
CHANGED
package/src/cli/router-module.js
CHANGED
|
@@ -8481,6 +8481,11 @@ async function runWebAction(context) {
|
|
|
8481
8481
|
routerRequireAuth: toBoolean(readArg(args, ["router-require-auth", "routerRequireAuth"], false), false),
|
|
8482
8482
|
allowRemoteClients: toBoolean(readArg(args, ["allow-remote-clients", "allowRemoteClients"], false), false),
|
|
8483
8483
|
cliPathForRouter: process.argv[1],
|
|
8484
|
+
onPortConflict: canPrompt() && typeof context?.prompts?.confirm === "function"
|
|
8485
|
+
? ({ port }) => context.prompts.confirm({
|
|
8486
|
+
message: `Port ${port} is already in use. Kill the existing listener and reclaim the port?`
|
|
8487
|
+
})
|
|
8488
|
+
: undefined,
|
|
8484
8489
|
onLine: (line) => context.terminal.line(line),
|
|
8485
8490
|
onError: (line) => context.terminal.error(line)
|
|
8486
8491
|
});
|
package/src/cli-entry.js
CHANGED
|
@@ -9,6 +9,7 @@ import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./node/local-s
|
|
|
9
9
|
import { resolveListenPort } from "./node/listen-port.js";
|
|
10
10
|
import { runStartCommand } from "./node/start-command.js";
|
|
11
11
|
import { runWebCommand } from "./node/web-command.js";
|
|
12
|
+
import { runUpgradeCommand } from "./node/upgrade-command.js";
|
|
12
13
|
|
|
13
14
|
function parseSimpleArgs(argv) {
|
|
14
15
|
const positional = [];
|
|
@@ -198,6 +199,14 @@ export async function runCli(argv = process.argv.slice(2), isTTY = undefined, ov
|
|
|
198
199
|
return runWebFastPath(configArgs.args, runWebCommandImpl);
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
if ((first === "upgrade" || first === "update") && !parsed.wantsHelp) {
|
|
203
|
+
const result = await runUpgradeCommand({
|
|
204
|
+
onLine: (msg) => console.log(msg),
|
|
205
|
+
onError: (msg) => console.error(msg)
|
|
206
|
+
});
|
|
207
|
+
return result.exitCode ?? (result.ok ? 0 : 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
201
210
|
if (firstIsSetup && !parsed.wantsHelp) {
|
|
202
211
|
const setupArgs = argv.slice(1);
|
|
203
212
|
const parsedSetup = parseSimpleArgs(setupArgs);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
getActiveRuntimeState,
|
|
7
|
+
stopProcessByPid,
|
|
8
|
+
clearRuntimeState,
|
|
9
|
+
spawnDetachedStart
|
|
10
|
+
} from "./instance-state.js";
|
|
11
|
+
|
|
12
|
+
const PKG_NAME = "@khanglvm/llm-router";
|
|
13
|
+
|
|
14
|
+
function readInstalledVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
17
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
18
|
+
return pkg.version || "unknown";
|
|
19
|
+
} catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fetchLatestVersion() {
|
|
25
|
+
try {
|
|
26
|
+
return execSync(`npm view ${PKG_NAME} version`, { encoding: "utf8" }).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectPackageManager() {
|
|
33
|
+
try {
|
|
34
|
+
const npmGlobalRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
|
|
35
|
+
const entryReal = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
36
|
+
if (entryReal.startsWith(npmGlobalRoot)) return "npm";
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const out = execSync("pnpm list -g --json 2>/dev/null", { encoding: "utf8" });
|
|
41
|
+
if (out.includes(PKG_NAME)) return "pnpm";
|
|
42
|
+
} catch { /* ignore */ }
|
|
43
|
+
|
|
44
|
+
return "npm";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runUpgradeCommand({ onLine, onError } = {}) {
|
|
48
|
+
const line = typeof onLine === "function" ? onLine : (msg) => console.log(msg);
|
|
49
|
+
const error = typeof onError === "function" ? onError : (msg) => console.error(msg);
|
|
50
|
+
|
|
51
|
+
const currentVersion = readInstalledVersion();
|
|
52
|
+
line(`Current version: ${currentVersion}`);
|
|
53
|
+
|
|
54
|
+
// Check latest
|
|
55
|
+
line("Checking for updates...");
|
|
56
|
+
const latestVersion = fetchLatestVersion();
|
|
57
|
+
if (!latestVersion) {
|
|
58
|
+
error("Could not fetch latest version from npm registry.");
|
|
59
|
+
return { ok: false, exitCode: 1 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (latestVersion === currentVersion) {
|
|
63
|
+
line(`Already on the latest version (${currentVersion}).`);
|
|
64
|
+
return { ok: true, exitCode: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
line(`New version available: ${currentVersion} → ${latestVersion}`);
|
|
68
|
+
|
|
69
|
+
// Stop running instance
|
|
70
|
+
let wasRunning = false;
|
|
71
|
+
let savedState = null;
|
|
72
|
+
try {
|
|
73
|
+
const runtime = await getActiveRuntimeState();
|
|
74
|
+
if (runtime) {
|
|
75
|
+
wasRunning = true;
|
|
76
|
+
savedState = { ...runtime };
|
|
77
|
+
line(`Stopping running server (pid ${runtime.pid})...`);
|
|
78
|
+
const stopResult = await stopProcessByPid(runtime.pid);
|
|
79
|
+
if (stopResult.ok) {
|
|
80
|
+
await clearRuntimeState({ pid: runtime.pid });
|
|
81
|
+
line("Server stopped.");
|
|
82
|
+
} else {
|
|
83
|
+
error(`Warning: could not stop server cleanly — ${stopResult.reason || "unknown"}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// instance-state not available, skip
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Install latest
|
|
91
|
+
const pm = detectPackageManager();
|
|
92
|
+
const installCmd = pm === "pnpm"
|
|
93
|
+
? `pnpm add -g ${PKG_NAME}@latest`
|
|
94
|
+
: `npm install -g ${PKG_NAME}@latest`;
|
|
95
|
+
|
|
96
|
+
line(`Upgrading via: ${installCmd}`);
|
|
97
|
+
try {
|
|
98
|
+
execSync(installCmd, { stdio: "inherit" });
|
|
99
|
+
} catch {
|
|
100
|
+
error("Upgrade failed. You may need to run with sudo or fix npm permissions.");
|
|
101
|
+
return { ok: false, exitCode: 1 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const newVersion = fetchLatestVersion() || latestVersion;
|
|
105
|
+
line(`Upgraded to ${newVersion}.`);
|
|
106
|
+
|
|
107
|
+
// Restart server if it was running
|
|
108
|
+
if (wasRunning && savedState) {
|
|
109
|
+
line("Restarting server...");
|
|
110
|
+
try {
|
|
111
|
+
spawnDetachedStart({
|
|
112
|
+
cliPath: savedState.cliPath || "",
|
|
113
|
+
configPath: savedState.configPath || "",
|
|
114
|
+
host: savedState.host || "127.0.0.1",
|
|
115
|
+
port: savedState.port || 18080,
|
|
116
|
+
watchConfig: savedState.watchConfig ?? true,
|
|
117
|
+
watchBinary: savedState.watchBinary ?? true,
|
|
118
|
+
requireAuth: savedState.requireAuth ?? false,
|
|
119
|
+
});
|
|
120
|
+
line("Server restarted.");
|
|
121
|
+
} catch (err) {
|
|
122
|
+
error(`Could not restart server: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
line("Start manually with: llr start");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { ok: true, exitCode: 0 };
|
|
128
|
+
}
|
package/src/node/web-command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { getDefaultConfigPath } from "./config-store.js";
|
|
3
3
|
import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
4
|
+
import { reclaimPort } from "./port-reclaim.js";
|
|
4
5
|
import { startWebConsoleServer } from "./web-console-server.js";
|
|
5
6
|
|
|
6
7
|
function toBoolean(value, fallback) {
|
|
@@ -49,26 +50,76 @@ export async function runWebCommand(options = {}) {
|
|
|
49
50
|
const line = typeof options.onLine === "function" ? options.onLine : console.log;
|
|
50
51
|
const error = typeof options.onError === "function" ? options.onError : console.error;
|
|
51
52
|
|
|
53
|
+
const onPortConflict = typeof options.onPortConflict === "function" ? options.onPortConflict : null;
|
|
54
|
+
const reclaimPortFn = typeof options.reclaimPort === "function" ? options.reclaimPort : (args) => reclaimPort(args, options);
|
|
55
|
+
|
|
56
|
+
const buildServerOptions = () => ({
|
|
57
|
+
host,
|
|
58
|
+
port,
|
|
59
|
+
configPath,
|
|
60
|
+
routerHost: FIXED_LOCAL_ROUTER_HOST,
|
|
61
|
+
routerPort: FIXED_LOCAL_ROUTER_PORT,
|
|
62
|
+
routerWatchConfig: toBoolean(options.routerWatchConfig ?? options["router-watch-config"], true),
|
|
63
|
+
routerWatchBinary: toBoolean(options.routerWatchBinary ?? options["router-watch-binary"], true),
|
|
64
|
+
routerRequireAuth: toBoolean(options.routerRequireAuth ?? options["router-require-auth"], false),
|
|
65
|
+
allowRemoteClients: toBoolean(options.allowRemoteClients ?? options["allow-remote-clients"], false),
|
|
66
|
+
cliPathForRouter: String(options.cliPathForRouter || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim()
|
|
67
|
+
});
|
|
68
|
+
|
|
52
69
|
let server;
|
|
53
70
|
try {
|
|
54
|
-
server = await startWebConsoleServer(
|
|
55
|
-
host,
|
|
56
|
-
port,
|
|
57
|
-
configPath,
|
|
58
|
-
routerHost: FIXED_LOCAL_ROUTER_HOST,
|
|
59
|
-
routerPort: FIXED_LOCAL_ROUTER_PORT,
|
|
60
|
-
routerWatchConfig: toBoolean(options.routerWatchConfig ?? options["router-watch-config"], true),
|
|
61
|
-
routerWatchBinary: toBoolean(options.routerWatchBinary ?? options["router-watch-binary"], true),
|
|
62
|
-
routerRequireAuth: toBoolean(options.routerRequireAuth ?? options["router-require-auth"], false),
|
|
63
|
-
allowRemoteClients: toBoolean(options.allowRemoteClients ?? options["allow-remote-clients"], false),
|
|
64
|
-
cliPathForRouter: String(options.cliPathForRouter || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim()
|
|
65
|
-
});
|
|
71
|
+
server = await startWebConsoleServer(buildServerOptions());
|
|
66
72
|
} catch (startError) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
if (startError?.code !== "EADDRINUSE") {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
errorMessage: `Failed to start the LLM Router web console: ${startError instanceof Error ? startError.message : String(startError)}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!onPortConflict) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
exitCode: 1,
|
|
85
|
+
errorMessage: `Port ${port} is already in use. Stop the existing listener or use a different port (--port=<number> or LLM_ROUTER_WEB_PORT env).`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let userChoice;
|
|
90
|
+
try {
|
|
91
|
+
userChoice = await onPortConflict({ port, host });
|
|
92
|
+
} catch {
|
|
93
|
+
userChoice = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!userChoice) {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
data: `Port ${port} is in use. Web console launch cancelled.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reclaimed = await reclaimPortFn({ port, line, error });
|
|
105
|
+
if (!reclaimed.ok) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
exitCode: 1,
|
|
109
|
+
errorMessage: reclaimed.errorMessage
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
server = await startWebConsoleServer(buildServerOptions());
|
|
115
|
+
line(`Port ${port} reclaimed successfully.`);
|
|
116
|
+
} catch (retryError) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
errorMessage: `Failed to start the LLM Router web console after reclaiming port ${port}: ${retryError instanceof Error ? retryError.message : String(retryError)}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
124
|
|
|
74
125
|
line(`LLM Router web console started on ${server.url}`);
|