@khanglvm/llm-router 2.3.5 → 2.3.7
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 +12 -0
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/cli/router-module.js +6 -3
- package/src/cli-entry.js +17 -2
- package/src/node/coding-tool-config.js +434 -41
- package/src/node/config-store.js +6 -1
- package/src/node/instance-state.js +4 -1
- package/src/node/local-server.js +40 -0
- package/src/node/router-supervisor.js +543 -0
- package/src/node/start-command.js +392 -61
- package/src/node/upgrade-command.js +90 -62
- package/src/node/web-console-client.js +20 -20
- package/src/node/web-console-server.js +84 -28
- package/src/shared/coding-tool-bindings.js +154 -0
- package/src/shared/local-router-defaults.js +15 -2
- package/src/shared/timeout-signal.js +6 -7
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./local-server-settings.js";
|
|
12
12
|
import { resolveListenPort } from "./listen-port.js";
|
|
13
13
|
import { startLocalRouteServer } from "./local-server.js";
|
|
14
|
+
import { startRouterSupervisor } from "./router-supervisor.js";
|
|
14
15
|
import { reclaimPort, stopStartupManagedListener } from "./port-reclaim.js";
|
|
15
16
|
import { installStartup, startupStatus } from "./startup-manager.js";
|
|
16
17
|
import { configHasProvider, sanitizeConfigForDisplay } from "../runtime/config.js";
|
|
@@ -134,10 +135,21 @@ function snapshotCliVersionState(cliPath) {
|
|
|
134
135
|
return { cliPath, realpath, packageJsonPath, version };
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
function buildStartArgs({
|
|
138
|
+
function buildStartArgs({
|
|
139
|
+
configPath,
|
|
140
|
+
host = FIXED_LOCAL_ROUTER_HOST,
|
|
141
|
+
port = FIXED_LOCAL_ROUTER_PORT,
|
|
142
|
+
watchConfig,
|
|
143
|
+
watchBinary,
|
|
144
|
+
requireAuth,
|
|
145
|
+
useConfigDefaults = false,
|
|
146
|
+
command = "start"
|
|
147
|
+
}) {
|
|
138
148
|
const args = [
|
|
139
|
-
|
|
140
|
-
`--config=${configPath}
|
|
149
|
+
command,
|
|
150
|
+
`--config=${configPath}`,
|
|
151
|
+
`--host=${host}`,
|
|
152
|
+
`--port=${port}`
|
|
141
153
|
];
|
|
142
154
|
if (useConfigDefaults) return args;
|
|
143
155
|
args.push(
|
|
@@ -361,8 +373,9 @@ async function attemptServerStartAfterStartupStop(buildLocalServerOptions, deps
|
|
|
361
373
|
return { ok: false, error: lastError };
|
|
362
374
|
}
|
|
363
375
|
|
|
364
|
-
|
|
376
|
+
async function runRouterRuntimeCommand(options = {}) {
|
|
365
377
|
const configPath = options.configPath || getDefaultConfigPath();
|
|
378
|
+
const backendMode = options.backendMode === true;
|
|
366
379
|
const requestedWatchConfig = options.watchConfig;
|
|
367
380
|
const requestedWatchBinary = options.watchBinary;
|
|
368
381
|
const binaryWatchIntervalMs = Math.max(
|
|
@@ -373,6 +386,7 @@ export async function runStartCommand(options = {}) {
|
|
|
373
386
|
const onStartupConflict = typeof options.onStartupConflict === "function" ? options.onStartupConflict : null;
|
|
374
387
|
const managedByStartup = options.managedByStartup === true || process.env.LLM_ROUTER_MANAGED_BY_STARTUP === "1";
|
|
375
388
|
const cliPathForWatch = String(options.cliPathForWatch || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "");
|
|
389
|
+
const startCommand = String(options.startCommand || (backendMode ? "start-runtime" : "start")).trim() || "start";
|
|
376
390
|
const line = typeof options.onLine === "function" ? options.onLine : console.log;
|
|
377
391
|
const error = typeof options.onError === "function" ? options.onError : console.error;
|
|
378
392
|
const startLocalRouteServerFn = typeof options.startLocalRouteServer === "function" ? options.startLocalRouteServer : startLocalRouteServer;
|
|
@@ -421,8 +435,12 @@ export async function runStartCommand(options = {}) {
|
|
|
421
435
|
|
|
422
436
|
let config = configState.config;
|
|
423
437
|
const persistedLocalServer = readLocalServerSettings(config);
|
|
424
|
-
const host =
|
|
425
|
-
|
|
438
|
+
const host = backendMode
|
|
439
|
+
? String(options.host || FIXED_LOCAL_ROUTER_HOST).trim() || FIXED_LOCAL_ROUTER_HOST
|
|
440
|
+
: FIXED_LOCAL_ROUTER_HOST;
|
|
441
|
+
const port = backendMode
|
|
442
|
+
? Math.max(1, Number(options.port || FIXED_LOCAL_ROUTER_PORT))
|
|
443
|
+
: resolveListenPort({ explicitPort: persistedLocalServer.port });
|
|
426
444
|
const watchConfig = requestedWatchConfig === undefined ? persistedLocalServer.watchConfig : toBoolean(requestedWatchConfig, persistedLocalServer.watchConfig);
|
|
427
445
|
const watchBinary = requestedWatchBinary === undefined ? persistedLocalServer.watchBinary : toBoolean(requestedWatchBinary, persistedLocalServer.watchBinary);
|
|
428
446
|
const requireAuth = requestedRequireAuth === undefined ? persistedLocalServer.requireAuth : toBoolean(requestedRequireAuth, persistedLocalServer.requireAuth);
|
|
@@ -464,8 +482,8 @@ export async function runStartCommand(options = {}) {
|
|
|
464
482
|
requireAuth
|
|
465
483
|
};
|
|
466
484
|
|
|
467
|
-
const startup = await startupStatusFn().catch(() => null);
|
|
468
|
-
if (!managedByStartup && startup?.installed) {
|
|
485
|
+
const startup = backendMode ? null : await startupStatusFn().catch(() => null);
|
|
486
|
+
if (!backendMode && !managedByStartup && startup?.installed) {
|
|
469
487
|
const handoff = await handoffToStartupManagedWithLatest({
|
|
470
488
|
runtimeState: null,
|
|
471
489
|
fallbackStartArgs: requestedStartArgs,
|
|
@@ -502,6 +520,7 @@ export async function runStartCommand(options = {}) {
|
|
|
502
520
|
}
|
|
503
521
|
|
|
504
522
|
let restartRequestedByConfig = false;
|
|
523
|
+
let requestGracefulRelaunch = async () => {};
|
|
505
524
|
|
|
506
525
|
const buildLocalServerOptions = () => ({
|
|
507
526
|
port,
|
|
@@ -524,26 +543,12 @@ export async function runStartCommand(options = {}) {
|
|
|
524
543
|
if (!areLocalServerSettingsEqual(nextLocalServer, resolvedLocalServer)) {
|
|
525
544
|
if (restartRequestedByConfig) return;
|
|
526
545
|
restartRequestedByConfig = true;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
await shutdown();
|
|
536
|
-
const launch = await spawnReplacementCli({
|
|
537
|
-
cliPath: cliPathForWatch || process.argv[1],
|
|
538
|
-
startArgs: buildStartArgs({ configPath, ...nextLocalServer })
|
|
539
|
-
});
|
|
540
|
-
if (!launch.ok) {
|
|
541
|
-
error(`Failed to relaunch LLM Router after the config runtime change: ${launch.error instanceof Error ? launch.error.message : String(launch.error)}`);
|
|
542
|
-
process.exit(1);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
process.exit(0);
|
|
546
|
-
})();
|
|
546
|
+
void requestGracefulRelaunch({
|
|
547
|
+
reasonMessage: `Local server settings changed in config (${reason}). Restarting to apply local router settings...`,
|
|
548
|
+
manualRestartMessage: "Local server settings changed, but this process cannot resolve its CLI path. Restart `llr start` manually to apply them.",
|
|
549
|
+
configPath,
|
|
550
|
+
...nextLocalServer
|
|
551
|
+
});
|
|
547
552
|
return;
|
|
548
553
|
}
|
|
549
554
|
|
|
@@ -702,7 +707,7 @@ export async function runStartCommand(options = {}) {
|
|
|
702
707
|
let binaryWatchTimer = null;
|
|
703
708
|
let binaryState = watchBinary && cliPathForWatch ? snapshotCliVersionState(cliPathForWatch) : null;
|
|
704
709
|
let binaryNoticeSent = false;
|
|
705
|
-
let
|
|
710
|
+
let relaunchInProgress = false;
|
|
706
711
|
const runtimeVersion = binaryState?.version || readPackageVersion(resolvePackageJsonPathFromCliPath(safeRealpath(cliPathForWatch)));
|
|
707
712
|
|
|
708
713
|
try {
|
|
@@ -748,9 +753,62 @@ export async function runStartCommand(options = {}) {
|
|
|
748
753
|
resolveDone();
|
|
749
754
|
};
|
|
750
755
|
|
|
756
|
+
requestGracefulRelaunch = async ({
|
|
757
|
+
reasonMessage = "",
|
|
758
|
+
manualRestartMessage = "",
|
|
759
|
+
cliPath = cliPathForWatch || process.argv[1],
|
|
760
|
+
configPath: nextConfigPath = configPath,
|
|
761
|
+
host: nextHost = host,
|
|
762
|
+
port: nextPort = port,
|
|
763
|
+
watchConfig: nextWatchConfig = watchConfig,
|
|
764
|
+
watchBinary: nextWatchBinary = watchBinary,
|
|
765
|
+
requireAuth: nextRequireAuth = requireAuth
|
|
766
|
+
} = {}) => {
|
|
767
|
+
if (shuttingDown || relaunchInProgress) return;
|
|
768
|
+
relaunchInProgress = true;
|
|
769
|
+
|
|
770
|
+
if (reasonMessage) {
|
|
771
|
+
line(reasonMessage);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (managedByStartup) {
|
|
775
|
+
await shutdown();
|
|
776
|
+
process.exit(0);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (!cliPath) {
|
|
781
|
+
relaunchInProgress = false;
|
|
782
|
+
error(manualRestartMessage || "LLM Router needs a manual restart because its CLI path cannot be resolved.");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
await shutdown();
|
|
787
|
+
const launch = await spawnReplacementCli({
|
|
788
|
+
cliPath,
|
|
789
|
+
startArgs: buildStartArgs({
|
|
790
|
+
command: startCommand,
|
|
791
|
+
configPath: nextConfigPath,
|
|
792
|
+
host: nextHost,
|
|
793
|
+
port: nextPort,
|
|
794
|
+
watchConfig: nextWatchConfig,
|
|
795
|
+
watchBinary: nextWatchBinary,
|
|
796
|
+
requireAuth: nextRequireAuth
|
|
797
|
+
})
|
|
798
|
+
});
|
|
799
|
+
if (!launch.ok) {
|
|
800
|
+
error(`Failed to relaunch LLM Router: ${launch.error instanceof Error ? launch.error.message : String(launch.error)}`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
line(`Started the replacement LLM Router process (pid ${launch.pid || "unknown"}).`);
|
|
806
|
+
process.exit(0);
|
|
807
|
+
};
|
|
808
|
+
|
|
751
809
|
if (watchBinary && binaryState) {
|
|
752
810
|
binaryWatchTimer = setInterval(() => {
|
|
753
|
-
if (shuttingDown ||
|
|
811
|
+
if (shuttingDown || relaunchInProgress) return;
|
|
754
812
|
const nextState = snapshotCliVersionState(binaryState.cliPath);
|
|
755
813
|
const changed =
|
|
756
814
|
nextState.realpath !== binaryState.realpath ||
|
|
@@ -762,16 +820,8 @@ export async function runStartCommand(options = {}) {
|
|
|
762
820
|
const to = nextState.version || nextState.realpath || "(unknown)";
|
|
763
821
|
binaryState = nextState;
|
|
764
822
|
|
|
765
|
-
if (managedByStartup) {
|
|
766
|
-
line(`Detected LLM Router update (${from} -> ${to}). Exiting for the startup manager to relaunch the latest version.`);
|
|
767
|
-
void shutdown().then(() => {
|
|
768
|
-
process.exit(0);
|
|
769
|
-
});
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
823
|
const cliPath = nextState.cliPath || cliPathForWatch || process.argv[1];
|
|
774
|
-
if (!cliPath) {
|
|
824
|
+
if (!managedByStartup && !cliPath) {
|
|
775
825
|
if (!binaryNoticeSent) {
|
|
776
826
|
binaryNoticeSent = true;
|
|
777
827
|
line(`Detected LLM Router update (${from} -> ${to}). Restart this process to run the new version.`);
|
|
@@ -779,35 +829,309 @@ export async function runStartCommand(options = {}) {
|
|
|
779
829
|
return;
|
|
780
830
|
}
|
|
781
831
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
error(`Failed to relaunch the updated LLM Router process: ${launch.error instanceof Error ? launch.error.message : String(launch.error)}`);
|
|
793
|
-
process.exit(1);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
line(`Started the updated LLM Router process (pid ${launch.pid || "unknown"}).`);
|
|
798
|
-
process.exit(0);
|
|
799
|
-
} catch (relaunchError) {
|
|
800
|
-
error(`Failed during LLM Router auto-relaunch: ${relaunchError instanceof Error ? relaunchError.message : String(relaunchError)}`);
|
|
801
|
-
process.exit(1);
|
|
802
|
-
}
|
|
803
|
-
})();
|
|
832
|
+
void requestGracefulRelaunch({
|
|
833
|
+
reasonMessage: managedByStartup
|
|
834
|
+
? `Detected LLM Router update (${from} -> ${to}). Draining current requests before the startup manager relaunches the latest version.`
|
|
835
|
+
: `Detected LLM Router update (${from} -> ${to}). Gracefully relaunching the latest version...`,
|
|
836
|
+
manualRestartMessage: "Detected an updated LLM Router binary, but this process cannot resolve its CLI path. Restart it manually to run the new version.",
|
|
837
|
+
cliPath
|
|
838
|
+
}).catch((relaunchError) => {
|
|
839
|
+
error(`Failed during LLM Router auto-relaunch: ${relaunchError instanceof Error ? relaunchError.message : String(relaunchError)}`);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
});
|
|
804
842
|
}, binaryWatchIntervalMs);
|
|
805
843
|
}
|
|
806
844
|
|
|
807
845
|
process.once("SIGINT", () => { void shutdown(); });
|
|
808
846
|
process.once("SIGTERM", () => { void shutdown(); });
|
|
847
|
+
process.once("SIGUSR2", () => {
|
|
848
|
+
void requestGracefulRelaunch({
|
|
849
|
+
reasonMessage: managedByStartup
|
|
850
|
+
? "Received runtime upgrade signal. Draining current requests before the startup manager relaunches the latest version..."
|
|
851
|
+
: "Received runtime upgrade signal. Gracefully restarting to activate the newly installed version...",
|
|
852
|
+
manualRestartMessage: "Received a runtime upgrade signal, but this process cannot resolve its CLI path. Restart it manually to activate the new version."
|
|
853
|
+
}).catch((relaunchError) => {
|
|
854
|
+
error(`Failed during the runtime upgrade relaunch: ${relaunchError instanceof Error ? relaunchError.message : String(relaunchError)}`);
|
|
855
|
+
process.exit(1);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
await donePromise;
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
ok: true,
|
|
863
|
+
exitCode: 0,
|
|
864
|
+
data: "Server stopped."
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function runRouterSupervisorCommand(options = {}) {
|
|
869
|
+
const configPath = options.configPath || getDefaultConfigPath();
|
|
870
|
+
const requestedWatchConfig = options.watchConfig;
|
|
871
|
+
const requestedWatchBinary = options.watchBinary;
|
|
872
|
+
const requestedRequireAuth = options.requireAuth;
|
|
873
|
+
const managedByStartup = options.managedByStartup === true || process.env.LLM_ROUTER_MANAGED_BY_STARTUP === "1";
|
|
874
|
+
const cliPathForWatch = String(options.cliPathForWatch || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "");
|
|
875
|
+
const line = typeof options.onLine === "function" ? options.onLine : console.log;
|
|
876
|
+
const error = typeof options.onError === "function" ? options.onError : console.error;
|
|
877
|
+
const getActiveRuntimeStateFn = typeof options.getActiveRuntimeState === "function" ? options.getActiveRuntimeState : getActiveRuntimeState;
|
|
878
|
+
const stopProcessByPidFn = typeof options.stopProcessByPid === "function" ? options.stopProcessByPid : stopProcessByPid;
|
|
879
|
+
const clearRuntimeStateFn = typeof options.clearRuntimeState === "function" ? options.clearRuntimeState : clearRuntimeState;
|
|
880
|
+
const installStartupFn = typeof options.installStartup === "function" ? options.installStartup : installStartup;
|
|
881
|
+
const startupStatusFn = typeof options.startupStatus === "function" ? options.startupStatus : startupStatus;
|
|
882
|
+
const reclaimPortFn = typeof options.reclaimPort === "function"
|
|
883
|
+
? options.reclaimPort
|
|
884
|
+
: (args) => reclaimPort(args, options);
|
|
885
|
+
const waitForRuntimeMatchFn = typeof options.waitForRuntimeMatch === "function"
|
|
886
|
+
? options.waitForRuntimeMatch
|
|
887
|
+
: (startOptions, waitOptions = {}) => waitForRuntimeMatch(startOptions, waitOptions);
|
|
888
|
+
const startRouterSupervisorFn = typeof options.startRouterSupervisor === "function"
|
|
889
|
+
? options.startRouterSupervisor
|
|
890
|
+
: (startOptions) => startRouterSupervisor(startOptions, options);
|
|
891
|
+
|
|
892
|
+
if (!(await configFileExists(configPath))) {
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
exitCode: 2,
|
|
896
|
+
errorMessage: [
|
|
897
|
+
`Config file not found: ${configPath}`,
|
|
898
|
+
"Run 'llr config' to create provider config or 'llr -h' for help."
|
|
899
|
+
].join("\n")
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
let configState;
|
|
904
|
+
try {
|
|
905
|
+
configState = await readConfigFileState(configPath);
|
|
906
|
+
} catch (readConfigError) {
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
exitCode: 2,
|
|
910
|
+
errorMessage: `Failed to load config from ${configPath}: ${readConfigError instanceof Error ? readConfigError.message : String(readConfigError)}`
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const configMigrationMessage = formatStartupConfigMigrationMessage(configState, configPath);
|
|
915
|
+
if (configMigrationMessage) {
|
|
916
|
+
if (configState.persistError) {
|
|
917
|
+
error(configMigrationMessage);
|
|
918
|
+
} else {
|
|
919
|
+
line(configMigrationMessage);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
let config = configState.config;
|
|
924
|
+
const persistedLocalServer = readLocalServerSettings(config);
|
|
925
|
+
const host = FIXED_LOCAL_ROUTER_HOST;
|
|
926
|
+
const port = resolveListenPort({ explicitPort: persistedLocalServer.port });
|
|
927
|
+
const watchConfig = requestedWatchConfig === undefined ? persistedLocalServer.watchConfig : toBoolean(requestedWatchConfig, persistedLocalServer.watchConfig);
|
|
928
|
+
const watchBinary = requestedWatchBinary === undefined ? persistedLocalServer.watchBinary : toBoolean(requestedWatchBinary, persistedLocalServer.watchBinary);
|
|
929
|
+
const requireAuth = requestedRequireAuth === undefined ? persistedLocalServer.requireAuth : toBoolean(requestedRequireAuth, persistedLocalServer.requireAuth);
|
|
930
|
+
const resolvedLocalServer = { host, port, watchConfig, watchBinary, requireAuth };
|
|
931
|
+
|
|
932
|
+
if (!areLocalServerSettingsEqual(persistedLocalServer, resolvedLocalServer)) {
|
|
933
|
+
config = await readConfigFile(configPath, { persistMigrated: false });
|
|
934
|
+
config = applyLocalServerSettings(config, resolvedLocalServer);
|
|
935
|
+
config = await writeConfigFile(config, configPath);
|
|
936
|
+
}
|
|
937
|
+
if (!configHasProvider(config)) {
|
|
938
|
+
return {
|
|
939
|
+
ok: false,
|
|
940
|
+
exitCode: 2,
|
|
941
|
+
errorMessage: [
|
|
942
|
+
`No providers configured in ${configPath}`,
|
|
943
|
+
"Run 'llr config' to add a provider or 'llr -h' for help."
|
|
944
|
+
].join("\n")
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (requireAuth && !config.masterKey) {
|
|
949
|
+
return {
|
|
950
|
+
ok: false,
|
|
951
|
+
exitCode: 2,
|
|
952
|
+
errorMessage: [
|
|
953
|
+
`Local auth requires masterKey in ${configPath}.`,
|
|
954
|
+
"Run 'llr config --operation=set-master-key --master-key=...' or start without --require-auth."
|
|
955
|
+
].join("\n")
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const requestedStartArgs = {
|
|
960
|
+
configPath,
|
|
961
|
+
host,
|
|
962
|
+
port,
|
|
963
|
+
watchConfig,
|
|
964
|
+
watchBinary,
|
|
965
|
+
requireAuth
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const startup = await startupStatusFn().catch(() => null);
|
|
969
|
+
if (!managedByStartup && startup?.installed) {
|
|
970
|
+
const handoff = await handoffToStartupManagedWithLatest({
|
|
971
|
+
runtimeState: null,
|
|
972
|
+
fallbackStartArgs: requestedStartArgs,
|
|
973
|
+
cliPath: cliPathForWatch,
|
|
974
|
+
line,
|
|
975
|
+
error
|
|
976
|
+
}, {
|
|
977
|
+
getActiveRuntimeState: getActiveRuntimeStateFn,
|
|
978
|
+
stopProcessByPid: stopProcessByPidFn,
|
|
979
|
+
clearRuntimeState: clearRuntimeStateFn,
|
|
980
|
+
reclaimPort: reclaimPortFn,
|
|
981
|
+
installStartup: installStartupFn,
|
|
982
|
+
waitForRuntimeMatch: waitForRuntimeMatchFn,
|
|
983
|
+
startupStatus: startupStatusFn,
|
|
984
|
+
onLine: line,
|
|
985
|
+
onError: error
|
|
986
|
+
});
|
|
987
|
+
if (!handoff.ok) {
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
exitCode: 1,
|
|
991
|
+
errorMessage: handoff.errorMessage
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
ok: true,
|
|
996
|
+
exitCode: 0,
|
|
997
|
+
data: [
|
|
998
|
+
`Startup-managed LLM Router is active on http://${handoff.runtime.host}:${handoff.runtime.port}.`,
|
|
999
|
+
`manager=${handoff.detail?.manager || startup.manager || "unknown"}`,
|
|
1000
|
+
`service=${handoff.detail?.serviceId || startup.serviceId || "unknown"}`
|
|
1001
|
+
].join("\n")
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const activeRuntime = await getActiveRuntimeStateFn().catch(() => null);
|
|
1006
|
+
if (activeRuntime && Number(activeRuntime.pid) !== Number(process.pid)) {
|
|
1007
|
+
return {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
exitCode: 1,
|
|
1010
|
+
errorMessage: `Another LLM Router instance is already running at http://${activeRuntime.host}:${activeRuntime.port}. Stop it before starting a new one.`
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
let server;
|
|
1015
|
+
try {
|
|
1016
|
+
server = await startRouterSupervisorFn({
|
|
1017
|
+
host,
|
|
1018
|
+
port,
|
|
1019
|
+
configPath,
|
|
1020
|
+
watchConfig,
|
|
1021
|
+
watchBinary,
|
|
1022
|
+
requireAuth,
|
|
1023
|
+
cliPath: cliPathForWatch,
|
|
1024
|
+
onLine: line,
|
|
1025
|
+
onError: error
|
|
1026
|
+
});
|
|
1027
|
+
} catch (startError) {
|
|
1028
|
+
if (startError?.code !== "EADDRINUSE") {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
exitCode: 1,
|
|
1032
|
+
errorMessage: `Failed to start LLM Router on http://${host}:${port}: ${startError instanceof Error ? startError.message : String(startError)}`
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const reclaimed = await reclaimPortFn({ port, line, error });
|
|
1037
|
+
if (!reclaimed.ok) {
|
|
1038
|
+
return {
|
|
1039
|
+
ok: false,
|
|
1040
|
+
exitCode: 1,
|
|
1041
|
+
errorMessage: reclaimed.errorMessage
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
server = await startRouterSupervisorFn({
|
|
1047
|
+
host,
|
|
1048
|
+
port,
|
|
1049
|
+
configPath,
|
|
1050
|
+
watchConfig,
|
|
1051
|
+
watchBinary,
|
|
1052
|
+
requireAuth,
|
|
1053
|
+
cliPath: cliPathForWatch,
|
|
1054
|
+
onLine: line,
|
|
1055
|
+
onError: error
|
|
1056
|
+
});
|
|
1057
|
+
line(`Port ${port} reclaimed successfully.`);
|
|
1058
|
+
} catch (retryError) {
|
|
1059
|
+
return {
|
|
1060
|
+
ok: false,
|
|
1061
|
+
exitCode: 1,
|
|
1062
|
+
errorMessage: `Failed to start LLM Router after reclaiming port ${port}: ${retryError instanceof Error ? retryError.message : String(retryError)}`
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const runtimeVersion = readPackageVersion(resolvePackageJsonPathFromCliPath(safeRealpath(cliPathForWatch)));
|
|
1068
|
+
try {
|
|
1069
|
+
await writeRuntimeState({
|
|
1070
|
+
pid: process.pid,
|
|
1071
|
+
host,
|
|
1072
|
+
port,
|
|
1073
|
+
configPath,
|
|
1074
|
+
watchConfig,
|
|
1075
|
+
watchBinary,
|
|
1076
|
+
requireAuth,
|
|
1077
|
+
managedByStartup,
|
|
1078
|
+
cliPath: cliPathForWatch,
|
|
1079
|
+
startedAt: new Date().toISOString(),
|
|
1080
|
+
version: runtimeVersion
|
|
1081
|
+
});
|
|
1082
|
+
} catch (stateError) {
|
|
1083
|
+
error(`Failed to write runtime state file: ${stateError instanceof Error ? stateError.message : String(stateError)}`);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
line(`LLM Router started on http://${host}:${port}`);
|
|
1087
|
+
line(`Anthropic base URL: http://${host}:${port}/anthropic`);
|
|
1088
|
+
line(`OpenAI base URL: http://${host}:${port}/openai`);
|
|
1089
|
+
for (const row of summarizeConfig(config, configPath)) {
|
|
1090
|
+
line(row);
|
|
1091
|
+
}
|
|
1092
|
+
line(`Local auth: ${requireAuth ? "required (masterKey)" : "disabled"}`);
|
|
1093
|
+
line(`Config hot reload: ${watchConfig ? "enabled" : "disabled"} (backend hot reload via supervisor)`);
|
|
1094
|
+
line(`Binary update watch: ${watchBinary ? "enabled" : "disabled"} (backend hot-swap via supervisor)`);
|
|
1095
|
+
line("Press Ctrl+C to stop.");
|
|
1096
|
+
|
|
1097
|
+
let shuttingDown = false;
|
|
1098
|
+
let shutdownPromise = null;
|
|
1099
|
+
const donePromise = new Promise((resolve) => {
|
|
1100
|
+
server.once("close", resolve);
|
|
1101
|
+
});
|
|
1102
|
+
const shutdown = async () => {
|
|
1103
|
+
if (shutdownPromise) return shutdownPromise;
|
|
1104
|
+
shuttingDown = true;
|
|
1105
|
+
shutdownPromise = (async () => {
|
|
1106
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
1107
|
+
await clearRuntimeStateFn({ pid: process.pid });
|
|
1108
|
+
})();
|
|
1109
|
+
return shutdownPromise;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const handleSigInt = () => { void shutdown(); };
|
|
1113
|
+
const handleSigTerm = () => { void shutdown(); };
|
|
1114
|
+
const handleSigUsr2 = () => {
|
|
1115
|
+
void server.requestBackendUpgrade("SIGUSR2").then((result) => {
|
|
1116
|
+
if (!result?.ok) {
|
|
1117
|
+
error(`Failed forwarding runtime upgrade signal to router backend: ${result?.reason || "unknown error"}`);
|
|
1118
|
+
}
|
|
1119
|
+
}).catch((upgradeError) => {
|
|
1120
|
+
error(`Failed forwarding runtime upgrade signal to router backend: ${upgradeError instanceof Error ? upgradeError.message : String(upgradeError)}`);
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
process.once("SIGINT", handleSigInt);
|
|
1124
|
+
process.once("SIGTERM", handleSigTerm);
|
|
1125
|
+
process.on("SIGUSR2", handleSigUsr2);
|
|
809
1126
|
|
|
810
1127
|
await donePromise;
|
|
1128
|
+
if (shutdownPromise) {
|
|
1129
|
+
await shutdownPromise;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
process.removeListener("SIGINT", handleSigInt);
|
|
1133
|
+
process.removeListener("SIGTERM", handleSigTerm);
|
|
1134
|
+
process.removeListener("SIGUSR2", handleSigUsr2);
|
|
811
1135
|
|
|
812
1136
|
return {
|
|
813
1137
|
ok: true,
|
|
@@ -815,3 +1139,10 @@ export async function runStartCommand(options = {}) {
|
|
|
815
1139
|
data: "Server stopped."
|
|
816
1140
|
};
|
|
817
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
export async function runStartCommand(options = {}) {
|
|
1144
|
+
if (options.backendMode === true) {
|
|
1145
|
+
return runRouterRuntimeCommand(options);
|
|
1146
|
+
}
|
|
1147
|
+
return runRouterSupervisorCommand(options);
|
|
1148
|
+
}
|