@khanglvm/llm-router 1.0.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.
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { realpathSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getDefaultConfigPath } from "./node/config-store.js";
7
+ import { runStartCommand } from "./node/start-command.js";
8
+
9
+ function parseSimpleArgs(argv) {
10
+ const positional = [];
11
+ const args = {};
12
+ let wantsHelp = false;
13
+
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const token = argv[i];
16
+
17
+ if (token === "-h" || token === "--help" || token === "help") {
18
+ wantsHelp = true;
19
+ continue;
20
+ }
21
+
22
+ if (token.startsWith("--")) {
23
+ const body = token.slice(2);
24
+ const separator = body.indexOf("=");
25
+ if (separator >= 0) {
26
+ args[body.slice(0, separator)] = body.slice(separator + 1);
27
+ } else {
28
+ const next = argv[i + 1];
29
+ if (next && !next.startsWith("-")) {
30
+ args[body] = next;
31
+ i += 1;
32
+ } else {
33
+ args[body] = true;
34
+ }
35
+ }
36
+ continue;
37
+ }
38
+
39
+ positional.push(token);
40
+ }
41
+
42
+ return { positional, args, wantsHelp };
43
+ }
44
+
45
+ function parseBoolean(value, fallback = true) {
46
+ if (value === undefined || value === null || value === "") return fallback;
47
+ if (typeof value === "boolean") return value;
48
+ const normalized = String(value).trim().toLowerCase();
49
+ if (["1", "true", "yes", "y"].includes(normalized)) return true;
50
+ if (["0", "false", "no", "n"].includes(normalized)) return false;
51
+ return fallback;
52
+ }
53
+
54
+ function parseNumber(value, fallback = 8787) {
55
+ if (value === undefined || value === null || value === "") return fallback;
56
+ const parsed = Number(value);
57
+ return Number.isFinite(parsed) ? parsed : fallback;
58
+ }
59
+
60
+ async function runStartFastPath(args) {
61
+ const result = await runStartCommand({
62
+ configPath: args.config || args.configPath || getDefaultConfigPath(),
63
+ host: args.host || "127.0.0.1",
64
+ port: parseNumber(args.port, 8787),
65
+ watchConfig: parseBoolean(args["watch-config"] ?? args.watchConfig, true),
66
+ watchBinary: parseBoolean(args["watch-binary"] ?? args.watchBinary, true),
67
+ requireAuth: parseBoolean(args["require-auth"] ?? args.requireAuth, false),
68
+ cliPathForWatch: process.argv[1],
69
+ onLine: (line) => console.log(line),
70
+ onError: (line) => console.error(line)
71
+ });
72
+
73
+ if (!result.ok && result.errorMessage) {
74
+ console.error(result.errorMessage);
75
+ }
76
+
77
+ return result.exitCode ?? (result.ok ? 0 : 1);
78
+ }
79
+
80
+ async function runSnapCli(argv, isTTY) {
81
+ const [{ createRegistry, runSingleModuleCli }, { default: routerModule }] = await Promise.all([
82
+ import("@levu/snap/dist/index.js"),
83
+ import("./cli/router-module.js")
84
+ ]);
85
+
86
+ const registry = createRegistry([routerModule]);
87
+ return runSingleModuleCli({
88
+ registry,
89
+ argv,
90
+ moduleId: "router",
91
+ defaultActionId: "config",
92
+ helpDefaultTarget: "module",
93
+ isTTY
94
+ });
95
+ }
96
+
97
+ export async function runCli(argv = process.argv.slice(2), isTTY = undefined) {
98
+ const parsed = parseSimpleArgs(argv);
99
+ const first = parsed.positional[0];
100
+ const firstIsStart = first === "start";
101
+
102
+ // Bare invocation opens the interactive config manager.
103
+ if (!first && !parsed.wantsHelp) {
104
+ return runSnapCli(["config"], isTTY);
105
+ }
106
+
107
+ // Fast-path explicit local start without loading Snap to minimize startup overhead.
108
+ if (firstIsStart && !parsed.wantsHelp) {
109
+ const startArgs = argv.slice(1);
110
+ const parsedStart = parseSimpleArgs(startArgs);
111
+ return runStartFastPath(parsedStart.args);
112
+ }
113
+
114
+ const normalized = [...argv];
115
+ if (normalized[0] === "help") normalized[0] = "--help";
116
+ if (normalized[0] === "setup") normalized[0] = "config";
117
+ return runSnapCli(normalized, isTTY);
118
+ }
119
+
120
+ function resolveExecutablePath(filePath) {
121
+ if (!filePath) return "";
122
+ try {
123
+ return realpathSync(filePath);
124
+ } catch {
125
+ return path.resolve(filePath);
126
+ }
127
+ }
128
+
129
+ const isMain = (() => {
130
+ const modulePath = resolveExecutablePath(fileURLToPath(import.meta.url));
131
+ const argvPath = resolveExecutablePath(process.argv[1]);
132
+ return Boolean(argvPath) && modulePath === argvPath;
133
+ })();
134
+
135
+ if (isMain) {
136
+ runCli()
137
+ .then((code) => {
138
+ process.exitCode = code;
139
+ })
140
+ .catch((error) => {
141
+ console.error(error instanceof Error ? error.message : String(error));
142
+ process.exitCode = 1;
143
+ });
144
+ }
package/src/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Cloudflare Worker entrypoint.
3
+ * Runtime config is supplied via LLM_ROUTER_CONFIG_JSON (or aliases) env/secret.
4
+ */
5
+
6
+ import { createFetchHandler } from "./runtime/handler.js";
7
+ import { runtimeConfigFromEnv } from "./runtime/config.js";
8
+
9
+ const workerFetch = createFetchHandler({
10
+ getConfig: async (env) => runtimeConfigFromEnv(env)
11
+ });
12
+
13
+ export default {
14
+ async fetch(request, env, ctx) {
15
+ return workerFetch(request, env, ctx);
16
+ }
17
+ };
18
+
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Local config persistence for ~/.llm-router.json.
3
+ */
4
+
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { promises as fs } from "node:fs";
8
+ import { normalizeRuntimeConfig } from "../runtime/config.js";
9
+
10
+ export const DEFAULT_CONFIG_FILENAME = ".llm-router.json";
11
+
12
+ export function getDefaultConfigPath() {
13
+ return path.join(os.homedir(), DEFAULT_CONFIG_FILENAME);
14
+ }
15
+
16
+ export async function readConfigFile(filePath = getDefaultConfigPath()) {
17
+ try {
18
+ const raw = await fs.readFile(filePath, "utf8");
19
+ return normalizeRuntimeConfig(JSON.parse(raw));
20
+ } catch (error) {
21
+ if (error && typeof error === "object" && error.code === "ENOENT") {
22
+ return normalizeRuntimeConfig({});
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ export async function configFileExists(filePath = getDefaultConfigPath()) {
29
+ try {
30
+ await fs.access(filePath);
31
+ return true;
32
+ } catch (error) {
33
+ if (error && typeof error === "object" && error.code === "ENOENT") {
34
+ return false;
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ export async function writeConfigFile(config, filePath = getDefaultConfigPath()) {
41
+ const normalized = normalizeRuntimeConfig(config);
42
+ const folder = path.dirname(filePath);
43
+ await fs.mkdir(folder, { recursive: true });
44
+ const payload = `${JSON.stringify(normalized, null, 2)}\n`;
45
+ await fs.writeFile(filePath, payload, { encoding: "utf8", mode: 0o600 });
46
+ await fs.chmod(filePath, 0o600);
47
+ return normalized;
48
+ }
49
+
50
+ export function upsertProvider(config, provider) {
51
+ const normalized = normalizeRuntimeConfig(config);
52
+ const idx = normalized.providers.findIndex((item) => item.id === provider.id);
53
+ if (idx >= 0) {
54
+ normalized.providers[idx] = {
55
+ ...normalized.providers[idx],
56
+ ...provider
57
+ };
58
+ } else {
59
+ normalized.providers.push(provider);
60
+ }
61
+ return normalizeRuntimeConfig(normalized);
62
+ }
63
+
64
+ export function setMasterKey(config, masterKey) {
65
+ const normalized = normalizeRuntimeConfig(config);
66
+ normalized.masterKey = masterKey;
67
+ return normalized;
68
+ }
69
+
70
+ export function removeProvider(config, providerId) {
71
+ const normalized = normalizeRuntimeConfig(config);
72
+ normalized.providers = normalized.providers.filter((provider) => provider.id !== providerId);
73
+ return normalizeRuntimeConfig(normalized);
74
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Higher-level config workflows used by CLI actions.
3
+ */
4
+
5
+ import { normalizeRuntimeConfig, validateRuntimeConfig } from "../runtime/config.js";
6
+
7
+ function dedupe(values) {
8
+ return [...new Set((values || []).filter(Boolean).map((value) => String(value).trim()).filter(Boolean))];
9
+ }
10
+
11
+ function normalizeBaseUrlByFormatInput(input) {
12
+ const source = input?.baseUrlByFormat && typeof input.baseUrlByFormat === "object"
13
+ ? input.baseUrlByFormat
14
+ : {};
15
+ const openai = String(
16
+ source.openai ||
17
+ input?.openaiBaseUrl ||
18
+ input?.["openai-base-url"] ||
19
+ ""
20
+ ).trim();
21
+ const claude = String(
22
+ source.claude ||
23
+ source.anthropic ||
24
+ input?.claudeBaseUrl ||
25
+ input?.anthropicBaseUrl ||
26
+ input?.["claude-base-url"] ||
27
+ input?.["anthropic-base-url"] ||
28
+ ""
29
+ ).trim();
30
+
31
+ const out = {};
32
+ if (openai) out.openai = openai;
33
+ if (claude) out.claude = claude;
34
+ return Object.keys(out).length > 0 ? out : undefined;
35
+ }
36
+
37
+ export function parseModelListInput(raw) {
38
+ if (!raw) return [];
39
+ if (Array.isArray(raw)) return dedupe(raw);
40
+ return dedupe(String(raw).split(/[,\n;\s]+/g));
41
+ }
42
+
43
+ function normalizeModelArray(models) {
44
+ const rows = Array.isArray(models) ? models : dedupe(models).map((id) => ({ id }));
45
+ return rows
46
+ .map((entry) => {
47
+ if (typeof entry === "string") return { id: entry };
48
+ if (!entry || typeof entry !== "object") return null;
49
+ const id = String(entry.id || entry.name || "").trim();
50
+ if (!id) return null;
51
+ const formats = dedupe(entry.formats || entry.format || []).filter((value) => value === "openai" || value === "claude");
52
+ return {
53
+ id,
54
+ ...(formats.length > 0 ? { formats } : {})
55
+ };
56
+ })
57
+ .filter(Boolean);
58
+ }
59
+
60
+ function buildModelsWithPreferredFormat(modelIds, modelSupport = {}, modelPreferredFormat = {}) {
61
+ return normalizeModelArray(modelIds.map((id) => {
62
+ const preferred = modelPreferredFormat[id];
63
+ if (preferred) {
64
+ return { id, formats: [preferred] };
65
+ }
66
+ return { id, formats: modelSupport[id] || [] };
67
+ }));
68
+ }
69
+
70
+ function summarizeEndpointMatrix(endpointMatrix) {
71
+ if (!Array.isArray(endpointMatrix)) return undefined;
72
+ return endpointMatrix.map((row) => ({
73
+ endpoint: row.endpoint,
74
+ supportedFormats: row.supportedFormats || [],
75
+ workingFormats: row.workingFormats || [],
76
+ modelsByFormat: row.modelsByFormat || {},
77
+ authByFormat: row.authByFormat || {}
78
+ }));
79
+ }
80
+
81
+ export function buildProviderFromConfigInput(input) {
82
+ const providerId = input.providerId || input.id || input.name;
83
+ const baseUrlByFormat = normalizeBaseUrlByFormatInput(input);
84
+ const explicitModelIds = parseModelListInput(input.models);
85
+ const probeModelSupport = input.probe?.modelSupport && typeof input.probe.modelSupport === "object"
86
+ ? input.probe.modelSupport
87
+ : {};
88
+ const probeModelPreferredFormat = input.probe?.modelPreferredFormat && typeof input.probe.modelPreferredFormat === "object"
89
+ ? input.probe.modelPreferredFormat
90
+ : {};
91
+ const explicitModels = explicitModelIds.length > 0
92
+ ? buildModelsWithPreferredFormat(explicitModelIds, probeModelSupport, probeModelPreferredFormat)
93
+ : [];
94
+ const probeModels = input.probe?.models?.length
95
+ ? buildModelsWithPreferredFormat(input.probe.models, probeModelSupport, probeModelPreferredFormat)
96
+ : [];
97
+ const mergedModels = explicitModels.length > 0 ? explicitModels : probeModels;
98
+ const endpointFormats = baseUrlByFormat ? Object.keys(baseUrlByFormat) : [];
99
+
100
+ const preferredFormat = input.probe?.preferredFormat || input.format;
101
+ const supportedFormats = dedupe([
102
+ ...(input.probe?.formats || []),
103
+ ...endpointFormats,
104
+ ...(input.formats || []),
105
+ ...(preferredFormat ? [preferredFormat] : [])
106
+ ]);
107
+ const baseUrl = String(input.baseUrl || "").trim()
108
+ || (preferredFormat ? baseUrlByFormat?.[preferredFormat] : "")
109
+ || baseUrlByFormat?.openai
110
+ || baseUrlByFormat?.claude
111
+ || "";
112
+
113
+ return normalizeRuntimeConfig({
114
+ providers: [{
115
+ id: providerId,
116
+ name: input.name || providerId,
117
+ baseUrl,
118
+ baseUrlByFormat,
119
+ apiKey: input.apiKey,
120
+ format: preferredFormat,
121
+ formats: supportedFormats,
122
+ auth: input.probe?.auth || input.auth,
123
+ authByFormat: input.probe?.authByFormat || input.authByFormat,
124
+ anthropicVersion: input.anthropicVersion,
125
+ anthropicBeta: input.anthropicBeta,
126
+ headers: input.headers || {},
127
+ models: mergedModels,
128
+ lastProbe: input.probe
129
+ ? {
130
+ ok: Boolean(input.probe.ok),
131
+ at: new Date().toISOString(),
132
+ formats: input.probe.formats || [],
133
+ workingFormats: input.probe.workingFormats || [],
134
+ models: input.probe.models || [],
135
+ modelSupport: input.probe.modelSupport || undefined,
136
+ modelPreferredFormat: input.probe.modelPreferredFormat || undefined,
137
+ endpointMatrix: summarizeEndpointMatrix(input.probe.endpointMatrix),
138
+ warnings: input.probe.warnings || undefined
139
+ }
140
+ : undefined
141
+ }]
142
+ }).providers[0];
143
+ }
144
+
145
+ export function ensureProviderHasModels(provider) {
146
+ if (Array.isArray(provider.models) && provider.models.length > 0) return provider;
147
+ return {
148
+ ...provider,
149
+ models: []
150
+ };
151
+ }
152
+
153
+ function mergeProviderModelsWithExistingFallbacks(existingProvider, incomingProvider) {
154
+ const existingModelById = new Map((existingProvider?.models || []).map((model) => [model.id, model]));
155
+ const mergedModels = (incomingProvider?.models || []).map((model) => {
156
+ const previous = existingModelById.get(model.id);
157
+ const hasExplicitFallbacks = Object.prototype.hasOwnProperty.call(model, "fallbackModels");
158
+ if (hasExplicitFallbacks || !previous) return model;
159
+ if (!Object.prototype.hasOwnProperty.call(previous, "fallbackModels")) return model;
160
+ return {
161
+ ...model,
162
+ fallbackModels: previous.fallbackModels || []
163
+ };
164
+ });
165
+
166
+ return {
167
+ ...incomingProvider,
168
+ models: mergedModels
169
+ };
170
+ }
171
+
172
+ export function applyConfigChanges(existingConfig, {
173
+ provider,
174
+ masterKey,
175
+ setDefaultModel = true
176
+ }) {
177
+ const normalized = normalizeRuntimeConfig(existingConfig);
178
+ const providers = [...normalized.providers];
179
+ const existingIndex = providers.findIndex((item) => item.id === provider.id);
180
+
181
+ if (existingIndex >= 0) {
182
+ const mergedProvider = mergeProviderModelsWithExistingFallbacks(
183
+ providers[existingIndex],
184
+ ensureProviderHasModels(provider)
185
+ );
186
+ providers[existingIndex] = {
187
+ ...providers[existingIndex],
188
+ ...mergedProvider
189
+ };
190
+ } else {
191
+ providers.push(ensureProviderHasModels(provider));
192
+ }
193
+
194
+ const nextConfig = normalizeRuntimeConfig({
195
+ ...normalized,
196
+ providers,
197
+ masterKey: masterKey ?? normalized.masterKey,
198
+ defaultModel: normalized.defaultModel
199
+ });
200
+
201
+ if (setDefaultModel) {
202
+ const bestDefaultModel =
203
+ nextConfig.defaultModel ||
204
+ (provider.models?.[0] ? `${provider.id}/${provider.models[0].id}` : undefined);
205
+
206
+ if (bestDefaultModel) {
207
+ nextConfig.defaultModel = bestDefaultModel;
208
+ }
209
+ }
210
+
211
+ return nextConfig;
212
+ }
213
+
214
+ export function buildWorkerConfigPayload(config, { masterKey } = {}) {
215
+ const normalized = normalizeRuntimeConfig({
216
+ ...config,
217
+ masterKey: masterKey ?? config.masterKey
218
+ });
219
+
220
+ const errors = validateRuntimeConfig(normalized, { requireProvider: true, requireMasterKey: true });
221
+ if (errors.length > 0) {
222
+ throw new Error(errors.join(" "));
223
+ }
224
+
225
+ const workingProviders = (normalized.providers || []).filter((provider) => {
226
+ const probe = provider.lastProbe;
227
+ return !probe || probe.ok !== false;
228
+ });
229
+
230
+ if (workingProviders.length === 0) {
231
+ throw new Error("At least one working provider is required for worker export.");
232
+ }
233
+
234
+ const payload = {
235
+ ...normalized,
236
+ providers: normalized.providers.map((provider) => ({
237
+ ...provider,
238
+ // Keep apiKey in payload for all-in-one secret export. Users can later convert to apiKeyEnv if preferred.
239
+ apiKey: provider.apiKey,
240
+ lastProbe: provider.lastProbe
241
+ }))
242
+ };
243
+
244
+ return payload;
245
+ }
@@ -0,0 +1,206 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+
6
+ const DEFAULT_INSTANCE_STATE_FILENAME = ".llm-router.runtime.json";
7
+
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ function normalizeBoolean(value, fallback = false) {
13
+ if (value === undefined || value === null || value === "") return fallback;
14
+ if (typeof value === "boolean") return value;
15
+ const text = String(value).trim().toLowerCase();
16
+ if (["1", "true", "yes", "y"].includes(text)) return true;
17
+ if (["0", "false", "no", "n"].includes(text)) return false;
18
+ return fallback;
19
+ }
20
+
21
+ function normalizeRuntimeState(raw) {
22
+ if (!raw || typeof raw !== "object") return null;
23
+ const pid = Number(raw.pid);
24
+ if (!Number.isInteger(pid) || pid <= 0) return null;
25
+
26
+ return {
27
+ pid,
28
+ host: String(raw.host || "127.0.0.1"),
29
+ port: Number.isFinite(Number(raw.port)) ? Number(raw.port) : 8787,
30
+ configPath: String(raw.configPath || ""),
31
+ watchConfig: normalizeBoolean(raw.watchConfig, true),
32
+ watchBinary: normalizeBoolean(raw.watchBinary, true),
33
+ requireAuth: normalizeBoolean(raw.requireAuth, false),
34
+ managedByStartup: normalizeBoolean(raw.managedByStartup, false),
35
+ cliPath: String(raw.cliPath || ""),
36
+ startedAt: String(raw.startedAt || new Date().toISOString()),
37
+ version: String(raw.version || "")
38
+ };
39
+ }
40
+
41
+ export function getRuntimeStatePath() {
42
+ return path.join(os.homedir(), DEFAULT_INSTANCE_STATE_FILENAME);
43
+ }
44
+
45
+ export async function readRuntimeState(filePath = getRuntimeStatePath()) {
46
+ try {
47
+ const raw = await fs.readFile(filePath, "utf8");
48
+ return normalizeRuntimeState(JSON.parse(raw));
49
+ } catch (error) {
50
+ if (error && typeof error === "object" && error.code === "ENOENT") {
51
+ return null;
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ export async function writeRuntimeState(state, filePath = getRuntimeStatePath()) {
58
+ const normalized = normalizeRuntimeState(state);
59
+ if (!normalized) throw new Error("Invalid runtime state.");
60
+
61
+ const folder = path.dirname(filePath);
62
+ await fs.mkdir(folder, { recursive: true });
63
+ await fs.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
64
+ await fs.chmod(filePath, 0o600);
65
+ return normalized;
66
+ }
67
+
68
+ export async function clearRuntimeState({ pid } = {}, filePath = getRuntimeStatePath()) {
69
+ if (pid !== undefined && pid !== null) {
70
+ const current = await readRuntimeState(filePath);
71
+ if (!current) return false;
72
+ if (Number(current.pid) !== Number(pid)) return false;
73
+ }
74
+
75
+ try {
76
+ await fs.rm(filePath, { force: true });
77
+ return true;
78
+ } catch (error) {
79
+ if (error && typeof error === "object" && error.code === "ENOENT") {
80
+ return false;
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ export function isProcessRunning(pid) {
87
+ const id = Number(pid);
88
+ if (!Number.isInteger(id) || id <= 0) return false;
89
+ try {
90
+ process.kill(id, 0);
91
+ return true;
92
+ } catch (error) {
93
+ if (error && typeof error === "object" && error.code === "ESRCH") {
94
+ return false;
95
+ }
96
+ return true;
97
+ }
98
+ }
99
+
100
+ export async function getActiveRuntimeState({ cleanupStale = true } = {}) {
101
+ const state = await readRuntimeState();
102
+ if (!state) return null;
103
+ if (isProcessRunning(state.pid)) return state;
104
+
105
+ if (cleanupStale) {
106
+ await clearRuntimeState({ pid: state.pid });
107
+ }
108
+ return null;
109
+ }
110
+
111
+ export async function stopProcessByPid(pid, { graceMs = 4000 } = {}) {
112
+ const id = Number(pid);
113
+ if (!Number.isInteger(id) || id <= 0) {
114
+ return { ok: false, reason: "Invalid pid." };
115
+ }
116
+
117
+ if (!isProcessRunning(id)) {
118
+ return { ok: true, alreadyStopped: true };
119
+ }
120
+
121
+ try {
122
+ process.kill(id, "SIGTERM");
123
+ } catch (error) {
124
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
125
+ }
126
+
127
+ const startedAt = Date.now();
128
+ while (Date.now() - startedAt < graceMs) {
129
+ if (!isProcessRunning(id)) {
130
+ return { ok: true, signal: "SIGTERM" };
131
+ }
132
+ await sleep(150);
133
+ }
134
+
135
+ if (!isProcessRunning(id)) {
136
+ return { ok: true, signal: "SIGTERM" };
137
+ }
138
+
139
+ try {
140
+ process.kill(id, "SIGKILL");
141
+ } catch (error) {
142
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
143
+ }
144
+
145
+ await sleep(150);
146
+ if (isProcessRunning(id)) {
147
+ return { ok: false, reason: `Process ${id} is still running after SIGKILL.` };
148
+ }
149
+ return { ok: true, signal: "SIGKILL" };
150
+ }
151
+
152
+ export function buildStartArgsFromState(state) {
153
+ const target = normalizeRuntimeState(state);
154
+ if (!target) {
155
+ return {
156
+ configPath: "",
157
+ host: "127.0.0.1",
158
+ port: 8787,
159
+ watchConfig: true,
160
+ watchBinary: true,
161
+ requireAuth: false
162
+ };
163
+ }
164
+ return {
165
+ configPath: target.configPath,
166
+ host: target.host,
167
+ port: target.port,
168
+ watchConfig: target.watchConfig,
169
+ watchBinary: target.watchBinary,
170
+ requireAuth: target.requireAuth
171
+ };
172
+ }
173
+
174
+ export function spawnDetachedStart({
175
+ cliPath,
176
+ configPath,
177
+ host = "127.0.0.1",
178
+ port = 8787,
179
+ watchConfig = true,
180
+ watchBinary = true,
181
+ requireAuth = false
182
+ }) {
183
+ const finalCliPath = String(cliPath || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim();
184
+ if (!finalCliPath) throw new Error("Cannot spawn llm-router start: CLI path is unknown.");
185
+
186
+ const args = [
187
+ finalCliPath,
188
+ "start",
189
+ `--config=${configPath}`,
190
+ `--host=${host}`,
191
+ `--port=${port}`,
192
+ `--watch-config=${watchConfig ? "true" : "false"}`,
193
+ `--watch-binary=${watchBinary ? "true" : "false"}`,
194
+ `--require-auth=${requireAuth ? "true" : "false"}`
195
+ ];
196
+ const child = spawn(process.execPath, args, {
197
+ detached: true,
198
+ stdio: "ignore",
199
+ env: {
200
+ ...process.env,
201
+ LLM_ROUTER_CLI_PATH: finalCliPath
202
+ }
203
+ });
204
+ child.unref();
205
+ return child.pid;
206
+ }