@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.
- package/CHANGELOG.md +39 -0
- package/README.md +337 -41
- package/package.json +19 -3
- package/src/cli/router-module.js +7331 -3805
- package/src/cli/wrangler-toml.js +1 -1
- package/src/cli-entry.js +162 -24
- package/src/node/amp-client-config.js +426 -0
- package/src/node/coding-tool-config.js +763 -0
- package/src/node/config-store.js +49 -18
- package/src/node/instance-state.js +213 -12
- package/src/node/listen-port.js +5 -37
- package/src/node/local-server-settings.js +122 -0
- package/src/node/local-server.js +3 -2
- package/src/node/provider-probe.js +13 -0
- package/src/node/start-command.js +282 -40
- package/src/node/startup-manager.js +64 -29
- package/src/node/web-command.js +106 -0
- package/src/node/web-console-assets.js +26 -0
- package/src/node/web-console-client.js +56 -0
- package/src/node/web-console-dev-assets.js +258 -0
- package/src/node/web-console-server.js +3146 -0
- package/src/node/web-console-styles.generated.js +1 -0
- package/src/node/web-console-ui/config-editor-utils.js +616 -0
- package/src/node/web-console-ui/lib/utils.js +6 -0
- package/src/node/web-console-ui/rate-limit-utils.js +144 -0
- package/src/node/web-console-ui/select-search-utils.js +36 -0
- package/src/runtime/codex-request-transformer.js +46 -5
- package/src/runtime/codex-response-transformer.js +268 -35
- package/src/runtime/config.js +1394 -35
- package/src/runtime/handler/amp-gemini.js +913 -0
- package/src/runtime/handler/amp-response.js +308 -0
- package/src/runtime/handler/amp.js +290 -0
- package/src/runtime/handler/auth.js +17 -2
- package/src/runtime/handler/provider-call.js +168 -50
- package/src/runtime/handler/provider-translation.js +937 -26
- package/src/runtime/handler/request.js +149 -6
- package/src/runtime/handler/route-debug.js +22 -1
- package/src/runtime/handler.js +449 -9
- package/src/runtime/subscription-auth.js +1 -6
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/index.js +3 -1
- package/src/translator/request/openai-to-claude.js +217 -6
- package/src/translator/response/openai-to-claude.js +206 -58
package/src/node/config-store.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
migrateRuntimeConfig,
|
|
12
12
|
normalizeRuntimeConfig
|
|
13
13
|
} from "../runtime/config.js";
|
|
14
|
+
import { sanitizePersistedLocalServerConfig } from "./local-server-settings.js";
|
|
14
15
|
|
|
15
16
|
export const DEFAULT_CONFIG_FILENAME = ".llm-router.json";
|
|
16
17
|
|
|
@@ -18,40 +19,70 @@ export function getDefaultConfigPath() {
|
|
|
18
19
|
return path.join(os.homedir(), DEFAULT_CONFIG_FILENAME);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
function normalizePersistedConfig(config, normalizeOptions = undefined) {
|
|
23
|
+
return sanitizePersistedLocalServerConfig(
|
|
24
|
+
normalizeRuntimeConfig(config, normalizeOptions)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readConfigFileState(filePath = getDefaultConfigPath(), options = {}) {
|
|
22
29
|
const autoMigrate = options.autoMigrate !== false;
|
|
23
30
|
const persistMigrated = options.persistMigrated !== false;
|
|
24
31
|
const migrateToVersion = options && Object.prototype.hasOwnProperty.call(options, "migrateToVersion")
|
|
25
32
|
? options.migrateToVersion
|
|
26
33
|
: CONFIG_VERSION;
|
|
34
|
+
const normalizeOptions = autoMigrate ? { migrateToVersion } : undefined;
|
|
35
|
+
|
|
27
36
|
try {
|
|
28
37
|
const raw = await fs.readFile(filePath, "utf8");
|
|
29
38
|
const parsedRaw = raw.trim() ? JSON.parse(raw) : {};
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const normalized = normalizePersistedConfig(parsedRaw, normalizeOptions);
|
|
40
|
+
const payload = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
41
|
+
const changed = payload !== raw;
|
|
42
|
+
let persisted = false;
|
|
43
|
+
let persistError;
|
|
44
|
+
|
|
45
|
+
if (autoMigrate && persistMigrated && changed) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(filePath, payload, { encoding: "utf8", mode: 0o600 });
|
|
48
|
+
await fs.chmod(filePath, 0o600);
|
|
49
|
+
persisted = true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
persistError = error;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
config: normalized,
|
|
57
|
+
exists: true,
|
|
58
|
+
changed,
|
|
59
|
+
persisted,
|
|
60
|
+
persistError,
|
|
61
|
+
beforeVersion: detectRuntimeConfigVersion(parsedRaw),
|
|
62
|
+
afterVersion: normalized.version
|
|
63
|
+
};
|
|
46
64
|
} catch (error) {
|
|
47
65
|
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
48
|
-
const
|
|
49
|
-
return
|
|
66
|
+
const normalized = normalizePersistedConfig({}, normalizeOptions);
|
|
67
|
+
return {
|
|
68
|
+
config: normalized,
|
|
69
|
+
exists: false,
|
|
70
|
+
changed: false,
|
|
71
|
+
persisted: false,
|
|
72
|
+
persistError: undefined,
|
|
73
|
+
beforeVersion: undefined,
|
|
74
|
+
afterVersion: normalized.version
|
|
75
|
+
};
|
|
50
76
|
}
|
|
51
77
|
throw error;
|
|
52
78
|
}
|
|
53
79
|
}
|
|
54
80
|
|
|
81
|
+
export async function readConfigFile(filePath = getDefaultConfigPath(), options = {}) {
|
|
82
|
+
const result = await readConfigFileState(filePath, options);
|
|
83
|
+
return result.config;
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
export async function configFileExists(filePath = getDefaultConfigPath()) {
|
|
56
87
|
try {
|
|
57
88
|
await fs.access(filePath);
|
|
@@ -68,7 +99,7 @@ export async function writeConfigFile(config, filePath = getDefaultConfigPath(),
|
|
|
68
99
|
const normalizeOptions = options && Object.prototype.hasOwnProperty.call(options, "migrateToVersion")
|
|
69
100
|
? { migrateToVersion: options.migrateToVersion }
|
|
70
101
|
: undefined;
|
|
71
|
-
const normalized =
|
|
102
|
+
const normalized = normalizePersistedConfig(config, normalizeOptions);
|
|
72
103
|
const folder = path.dirname(filePath);
|
|
73
104
|
await fs.mkdir(folder, { recursive: true });
|
|
74
105
|
const payload = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
@@ -90,7 +121,7 @@ export async function migrateConfigFile(filePath = getDefaultConfigPath(), {
|
|
|
90
121
|
const rawConfig = rawText.trim() ? JSON.parse(rawText) : {};
|
|
91
122
|
const beforeVersion = detectRuntimeConfigVersion(rawConfig);
|
|
92
123
|
const migratedRaw = migrateRuntimeConfig(rawConfig, { targetVersion });
|
|
93
|
-
const normalized =
|
|
124
|
+
const normalized = normalizePersistedConfig(migratedRaw, { migrateToVersion: targetVersion });
|
|
94
125
|
const payload = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
95
126
|
const changed = payload !== rawText;
|
|
96
127
|
|
|
@@ -2,8 +2,10 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_INSTANCE_STATE_FILENAME = ".llm-router.runtime.json";
|
|
8
|
+
const MAX_START_OUTPUT_CHARS = 4000;
|
|
7
9
|
|
|
8
10
|
function sleep(ms) {
|
|
9
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -25,8 +27,8 @@ function normalizeRuntimeState(raw) {
|
|
|
25
27
|
|
|
26
28
|
return {
|
|
27
29
|
pid,
|
|
28
|
-
host: String(raw.host ||
|
|
29
|
-
port: Number.isFinite(Number(raw.port)) ? Number(raw.port) :
|
|
30
|
+
host: String(raw.host || FIXED_LOCAL_ROUTER_HOST),
|
|
31
|
+
port: Number.isFinite(Number(raw.port)) ? Number(raw.port) : FIXED_LOCAL_ROUTER_PORT,
|
|
30
32
|
configPath: String(raw.configPath || ""),
|
|
31
33
|
watchConfig: normalizeBoolean(raw.watchConfig, true),
|
|
32
34
|
watchBinary: normalizeBoolean(raw.watchBinary, true),
|
|
@@ -38,6 +40,46 @@ function normalizeRuntimeState(raw) {
|
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function normalizeHost(value, fallback = FIXED_LOCAL_ROUTER_HOST) {
|
|
44
|
+
return String(value || fallback).trim() || fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeCliPath(value) {
|
|
48
|
+
const target = String(value || "").trim();
|
|
49
|
+
if (!target) return "";
|
|
50
|
+
return path.isAbsolute(target) ? target : path.resolve(target);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function appendRecentOutput(current, chunk, maxChars = MAX_START_OUTPUT_CHARS) {
|
|
54
|
+
if (!chunk) return current;
|
|
55
|
+
const combined = `${current}${chunk}`;
|
|
56
|
+
return combined.length > maxChars ? combined.slice(-maxChars) : combined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatStartFailureMessage(baseMessage, { stderr = "", stdout = "" } = {}) {
|
|
60
|
+
const detail = String(stderr || "").trim() || String(stdout || "").trim();
|
|
61
|
+
return detail ? `${baseMessage}\n${detail}` : baseMessage;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runtimeMatchesStartOptions(runtime, {
|
|
65
|
+
configPath,
|
|
66
|
+
host = FIXED_LOCAL_ROUTER_HOST,
|
|
67
|
+
port = FIXED_LOCAL_ROUTER_PORT,
|
|
68
|
+
watchConfig = true,
|
|
69
|
+
watchBinary = true,
|
|
70
|
+
requireAuth = false
|
|
71
|
+
} = {}) {
|
|
72
|
+
const normalized = normalizeRuntimeState(runtime);
|
|
73
|
+
if (!normalized) return false;
|
|
74
|
+
|
|
75
|
+
return normalizeHost(normalized.host) === normalizeHost(host)
|
|
76
|
+
&& Number(normalized.port) === Number(port)
|
|
77
|
+
&& String(normalized.configPath || "").trim() === String(configPath || "").trim()
|
|
78
|
+
&& normalized.watchConfig === normalizeBoolean(watchConfig, true)
|
|
79
|
+
&& normalized.watchBinary === normalizeBoolean(watchBinary, true)
|
|
80
|
+
&& normalized.requireAuth === normalizeBoolean(requireAuth, false);
|
|
81
|
+
}
|
|
82
|
+
|
|
41
83
|
export function getRuntimeStatePath() {
|
|
42
84
|
return path.join(os.homedir(), DEFAULT_INSTANCE_STATE_FILENAME);
|
|
43
85
|
}
|
|
@@ -154,8 +196,8 @@ export function buildStartArgsFromState(state) {
|
|
|
154
196
|
if (!target) {
|
|
155
197
|
return {
|
|
156
198
|
configPath: "",
|
|
157
|
-
host:
|
|
158
|
-
port:
|
|
199
|
+
host: FIXED_LOCAL_ROUTER_HOST,
|
|
200
|
+
port: FIXED_LOCAL_ROUTER_PORT,
|
|
159
201
|
watchConfig: true,
|
|
160
202
|
watchBinary: true,
|
|
161
203
|
requireAuth: false
|
|
@@ -171,16 +213,42 @@ export function buildStartArgsFromState(state) {
|
|
|
171
213
|
};
|
|
172
214
|
}
|
|
173
215
|
|
|
174
|
-
export function
|
|
216
|
+
export async function waitForRuntimeMatch(options = {}, deps = {}) {
|
|
217
|
+
const getActiveRuntimeStateFn = typeof deps.getActiveRuntimeState === "function"
|
|
218
|
+
? deps.getActiveRuntimeState
|
|
219
|
+
: getActiveRuntimeState;
|
|
220
|
+
const timeoutMs = Number.isFinite(Number(deps.timeoutMs)) ? Math.max(250, Number(deps.timeoutMs)) : 8000;
|
|
221
|
+
const pollIntervalMs = Number.isFinite(Number(deps.pollIntervalMs)) ? Math.max(50, Number(deps.pollIntervalMs)) : 125;
|
|
222
|
+
const requireManagedByStartup = deps.requireManagedByStartup === true;
|
|
223
|
+
|
|
224
|
+
const startedAt = Date.now();
|
|
225
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
226
|
+
const runtime = await getActiveRuntimeStateFn().catch(() => null);
|
|
227
|
+
if (runtimeMatchesStartOptions(runtime, options)
|
|
228
|
+
&& (!requireManagedByStartup || runtime?.managedByStartup)) {
|
|
229
|
+
return runtime;
|
|
230
|
+
}
|
|
231
|
+
await sleep(pollIntervalMs);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function spawnStartProcess({
|
|
175
238
|
cliPath,
|
|
176
239
|
configPath,
|
|
177
|
-
host =
|
|
178
|
-
port =
|
|
240
|
+
host = FIXED_LOCAL_ROUTER_HOST,
|
|
241
|
+
port = FIXED_LOCAL_ROUTER_PORT,
|
|
179
242
|
watchConfig = true,
|
|
180
243
|
watchBinary = true,
|
|
181
244
|
requireAuth = false
|
|
182
|
-
}
|
|
183
|
-
|
|
245
|
+
}, {
|
|
246
|
+
detached = true,
|
|
247
|
+
stdio = "ignore",
|
|
248
|
+
unref = false,
|
|
249
|
+
env = process.env
|
|
250
|
+
} = {}) {
|
|
251
|
+
const finalCliPath = normalizeCliPath(cliPath || env.LLM_ROUTER_CLI_PATH || process.argv[1] || "");
|
|
184
252
|
if (!finalCliPath) throw new Error("Cannot spawn llm-router start: CLI path is unknown.");
|
|
185
253
|
|
|
186
254
|
const args = [
|
|
@@ -193,14 +261,147 @@ export function spawnDetachedStart({
|
|
|
193
261
|
`--watch-binary=${watchBinary ? "true" : "false"}`,
|
|
194
262
|
`--require-auth=${requireAuth ? "true" : "false"}`
|
|
195
263
|
];
|
|
264
|
+
|
|
196
265
|
const child = spawn(process.execPath, args, {
|
|
197
|
-
detached
|
|
198
|
-
stdio
|
|
266
|
+
detached,
|
|
267
|
+
stdio,
|
|
199
268
|
env: {
|
|
200
|
-
...
|
|
269
|
+
...env,
|
|
201
270
|
LLM_ROUTER_CLI_PATH: finalCliPath
|
|
202
271
|
}
|
|
203
272
|
});
|
|
273
|
+
|
|
274
|
+
if (unref) child.unref();
|
|
275
|
+
return child;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function startDetachedRouterService(options = {}, deps = {}) {
|
|
279
|
+
const getActiveRuntimeStateFn = typeof deps.getActiveRuntimeState === "function"
|
|
280
|
+
? deps.getActiveRuntimeState
|
|
281
|
+
: getActiveRuntimeState;
|
|
282
|
+
const spawnStartProcessFn = typeof deps.spawnStartProcess === "function"
|
|
283
|
+
? deps.spawnStartProcess
|
|
284
|
+
: spawnStartProcess;
|
|
285
|
+
const timeoutMs = Number.isFinite(Number(deps.timeoutMs)) ? Math.max(250, Number(deps.timeoutMs)) : 8000;
|
|
286
|
+
const pollIntervalMs = Number.isFinite(Number(deps.pollIntervalMs)) ? Math.max(50, Number(deps.pollIntervalMs)) : 125;
|
|
287
|
+
|
|
288
|
+
let child;
|
|
289
|
+
try {
|
|
290
|
+
child = spawnStartProcessFn(options, {
|
|
291
|
+
detached: true,
|
|
292
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
293
|
+
unref: false,
|
|
294
|
+
env: deps.env || process.env
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let childError = null;
|
|
304
|
+
let childExit = null;
|
|
305
|
+
let stdout = "";
|
|
306
|
+
let stderr = "";
|
|
307
|
+
const onStdout = (chunk) => {
|
|
308
|
+
stdout = appendRecentOutput(stdout, chunk);
|
|
309
|
+
};
|
|
310
|
+
const onStderr = (chunk) => {
|
|
311
|
+
stderr = appendRecentOutput(stderr, chunk);
|
|
312
|
+
};
|
|
313
|
+
child.stdout?.setEncoding?.("utf8");
|
|
314
|
+
child.stderr?.setEncoding?.("utf8");
|
|
315
|
+
child.stdout?.on?.("data", onStdout);
|
|
316
|
+
child.stderr?.on?.("data", onStderr);
|
|
317
|
+
child.once("error", (error) => {
|
|
318
|
+
childError = error;
|
|
319
|
+
});
|
|
320
|
+
child.once("exit", (code, signal) => {
|
|
321
|
+
childExit = { code, signal };
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const cleanupChildIo = () => {
|
|
325
|
+
child.stdout?.off?.("data", onStdout);
|
|
326
|
+
child.stderr?.off?.("data", onStderr);
|
|
327
|
+
child.stdout?.destroy?.();
|
|
328
|
+
child.stderr?.destroy?.();
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
333
|
+
const runtime = await getActiveRuntimeStateFn().catch(() => null);
|
|
334
|
+
if (runtimeMatchesStartOptions(runtime, options)) {
|
|
335
|
+
cleanupChildIo();
|
|
336
|
+
child.unref();
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
pid: child.pid,
|
|
340
|
+
runtime
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (childError) {
|
|
345
|
+
cleanupChildIo();
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
errorMessage: formatStartFailureMessage(
|
|
349
|
+
childError instanceof Error ? childError.message : String(childError),
|
|
350
|
+
{ stderr, stdout }
|
|
351
|
+
)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (childExit) {
|
|
356
|
+
cleanupChildIo();
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
errorMessage: formatStartFailureMessage(
|
|
360
|
+
`llm-router exited before becoming ready (${childExit.signal || childExit.code || "unknown"}).`,
|
|
361
|
+
{ stderr, stdout }
|
|
362
|
+
),
|
|
363
|
+
exitCode: childExit.code,
|
|
364
|
+
signal: childExit.signal || ""
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await sleep(pollIntervalMs);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
cleanupChildIo();
|
|
204
372
|
child.unref();
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
errorMessage: formatStartFailureMessage(
|
|
376
|
+
`Timed out waiting for llm-router to start on http://${normalizeHost(options.host)}:${Number(options.port || FIXED_LOCAL_ROUTER_PORT)}.`,
|
|
377
|
+
{ stderr, stdout }
|
|
378
|
+
),
|
|
379
|
+
pid: child.pid
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function spawnDetachedStart({
|
|
384
|
+
cliPath,
|
|
385
|
+
configPath,
|
|
386
|
+
host = FIXED_LOCAL_ROUTER_HOST,
|
|
387
|
+
port = FIXED_LOCAL_ROUTER_PORT,
|
|
388
|
+
watchConfig = true,
|
|
389
|
+
watchBinary = true,
|
|
390
|
+
requireAuth = false
|
|
391
|
+
}) {
|
|
392
|
+
const child = spawnStartProcess({
|
|
393
|
+
cliPath,
|
|
394
|
+
configPath,
|
|
395
|
+
host,
|
|
396
|
+
port,
|
|
397
|
+
watchConfig,
|
|
398
|
+
watchBinary,
|
|
399
|
+
requireAuth
|
|
400
|
+
}, {
|
|
401
|
+
detached: true,
|
|
402
|
+
stdio: "ignore",
|
|
403
|
+
unref: true,
|
|
404
|
+
env: process.env
|
|
405
|
+
});
|
|
205
406
|
return child.pid;
|
|
206
407
|
}
|
package/src/node/listen-port.js
CHANGED
|
@@ -1,43 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
const MIN_LISTEN_PORT = 1;
|
|
3
|
-
const MAX_LISTEN_PORT = 65535;
|
|
1
|
+
import { FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
if (value === undefined || value === null || value === "") return null;
|
|
7
|
-
const normalized = String(value).trim();
|
|
8
|
-
if (!/^\d+$/.test(normalized)) return null;
|
|
9
|
-
|
|
10
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
11
|
-
if (!Number.isInteger(parsed)) return null;
|
|
12
|
-
if (parsed < MIN_LISTEN_PORT || parsed > MAX_LISTEN_PORT) return null;
|
|
13
|
-
return parsed;
|
|
14
|
-
}
|
|
3
|
+
const DEFAULT_LISTEN_PORT = FIXED_LOCAL_ROUTER_PORT;
|
|
15
4
|
|
|
16
5
|
/**
|
|
17
|
-
* Resolve the local listen port
|
|
18
|
-
*
|
|
19
|
-
* 2) LLM_ROUTER_PORT env
|
|
20
|
-
* 3) PORT env (for generic hosting environments)
|
|
21
|
-
* 4) fallback (default 8787)
|
|
6
|
+
* Resolve the local router listen port.
|
|
7
|
+
* The router port is currently fixed and user overrides are ignored.
|
|
22
8
|
*/
|
|
23
|
-
export function resolveListenPort({
|
|
24
|
-
explicitPort,
|
|
25
|
-
env = process.env,
|
|
26
|
-
fallbackPort = DEFAULT_LISTEN_PORT
|
|
27
|
-
} = {}) {
|
|
28
|
-
const candidates = [
|
|
29
|
-
explicitPort,
|
|
30
|
-
env?.LLM_ROUTER_PORT,
|
|
31
|
-
env?.PORT,
|
|
32
|
-
fallbackPort,
|
|
33
|
-
DEFAULT_LISTEN_PORT
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
for (const candidate of candidates) {
|
|
37
|
-
const parsed = parsePortValue(candidate);
|
|
38
|
-
if (parsed !== null) return parsed;
|
|
39
|
-
}
|
|
40
|
-
|
|
9
|
+
export function resolveListenPort() {
|
|
41
10
|
return DEFAULT_LISTEN_PORT;
|
|
42
11
|
}
|
|
43
|
-
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOCAL_ROUTER_HOST as FIXED_LOCAL_ROUTER_HOST,
|
|
3
|
+
LOCAL_ROUTER_PORT as FIXED_LOCAL_ROUTER_PORT,
|
|
4
|
+
LOCAL_ROUTER_ORIGIN,
|
|
5
|
+
buildLocalRouterSettings,
|
|
6
|
+
buildPersistedLocalServerMetadata
|
|
7
|
+
} from "../shared/local-router-defaults.js";
|
|
8
|
+
|
|
9
|
+
export { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT };
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LOCAL_SERVER_SETTINGS = Object.freeze(buildLocalRouterSettings());
|
|
12
|
+
|
|
13
|
+
export function getDefaultLocalServerSettings() {
|
|
14
|
+
return { ...DEFAULT_LOCAL_SERVER_SETTINGS };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getFixedLocalRouterOrigin() {
|
|
18
|
+
return LOCAL_ROUTER_ORIGIN;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readLocalServerSettings(config, fallback = DEFAULT_LOCAL_SERVER_SETTINGS) {
|
|
22
|
+
return buildLocalRouterSettings(config?.metadata?.localServer, fallback);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function applyLocalServerSettings(config, settings) {
|
|
26
|
+
const next = config && typeof config === "object" && !Array.isArray(config)
|
|
27
|
+
? JSON.parse(JSON.stringify(config))
|
|
28
|
+
: {};
|
|
29
|
+
const resolved = readLocalServerSettings({ metadata: { localServer: settings } }, DEFAULT_LOCAL_SERVER_SETTINGS);
|
|
30
|
+
const persisted = buildPersistedLocalServerMetadata(resolved);
|
|
31
|
+
const metadata = next.metadata && typeof next.metadata === "object" && !Array.isArray(next.metadata)
|
|
32
|
+
? { ...next.metadata }
|
|
33
|
+
: {};
|
|
34
|
+
|
|
35
|
+
if (Object.keys(persisted).length > 0) {
|
|
36
|
+
next.metadata = {
|
|
37
|
+
...metadata,
|
|
38
|
+
localServer: persisted
|
|
39
|
+
};
|
|
40
|
+
} else if (Object.prototype.hasOwnProperty.call(metadata, "localServer")) {
|
|
41
|
+
delete metadata.localServer;
|
|
42
|
+
if (Object.keys(metadata).length > 0) {
|
|
43
|
+
next.metadata = metadata;
|
|
44
|
+
} else {
|
|
45
|
+
delete next.metadata;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sanitizePersistedLocalServerConfig(config) {
|
|
53
|
+
const next = config && typeof config === "object" && !Array.isArray(config)
|
|
54
|
+
? JSON.parse(JSON.stringify(config))
|
|
55
|
+
: {};
|
|
56
|
+
const metadata = next.metadata;
|
|
57
|
+
|
|
58
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalizedLocalServer = buildPersistedLocalServerMetadata(metadata.localServer);
|
|
63
|
+
const nextMetadata = { ...metadata };
|
|
64
|
+
|
|
65
|
+
if (Object.keys(normalizedLocalServer).length > 0) {
|
|
66
|
+
nextMetadata.localServer = normalizedLocalServer;
|
|
67
|
+
} else {
|
|
68
|
+
delete nextMetadata.localServer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Object.keys(nextMetadata).length > 0) {
|
|
72
|
+
next.metadata = nextMetadata;
|
|
73
|
+
} else {
|
|
74
|
+
delete next.metadata;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function areLocalServerSettingsEqual(left, right) {
|
|
81
|
+
const a = readLocalServerSettings({ metadata: { localServer: left } }, DEFAULT_LOCAL_SERVER_SETTINGS);
|
|
82
|
+
const b = readLocalServerSettings({ metadata: { localServer: right } }, DEFAULT_LOCAL_SERVER_SETTINGS);
|
|
83
|
+
return a.host === b.host
|
|
84
|
+
&& a.port === b.port
|
|
85
|
+
&& a.watchConfig === b.watchConfig
|
|
86
|
+
&& a.watchBinary === b.watchBinary
|
|
87
|
+
&& a.requireAuth === b.requireAuth;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatStartupLabel(startup = {}) {
|
|
91
|
+
const managerMap = {
|
|
92
|
+
launchd: "LaunchAgent",
|
|
93
|
+
"systemd-user": "Systemd User",
|
|
94
|
+
unknown: "Startup service",
|
|
95
|
+
unsupported: "Startup service"
|
|
96
|
+
};
|
|
97
|
+
const manager = managerMap[startup.manager] || startup.manager || "Startup service";
|
|
98
|
+
if (!startup.installed) return `${manager} not installed`;
|
|
99
|
+
return startup.running ? `${manager} is running` : `${manager} is installed`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatStartupDetail(startup = {}) {
|
|
103
|
+
const managerMap = {
|
|
104
|
+
launchd: "Managed by launchd",
|
|
105
|
+
"systemd-user": "Managed by systemd --user",
|
|
106
|
+
unknown: "Managed by startup service",
|
|
107
|
+
unsupported: "Startup not supported"
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (startup.detail && typeof startup.detail === "string") {
|
|
111
|
+
const text = startup.detail.trim();
|
|
112
|
+
if (text) {
|
|
113
|
+
if (text.includes("Could not find service") || text.includes("service could not be found")) {
|
|
114
|
+
return "No installed startup service was found.";
|
|
115
|
+
}
|
|
116
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
117
|
+
if (compact.length <= 140) return compact;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return managerMap[startup.manager] || "Startup service status unavailable";
|
|
122
|
+
}
|
package/src/node/local-server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { watch as fsWatch } from "node:fs";
|
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
9
|
import { createFetchHandler } from "../runtime/handler.js";
|
|
10
10
|
import { readConfigFile, getDefaultConfigPath } from "./config-store.js";
|
|
11
|
+
import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS = 300;
|
|
13
14
|
const MAX_CONFIG_RELOAD_DEBOUNCE_MS = 5000;
|
|
@@ -233,8 +234,8 @@ async function writeFetchResponseToNode(res, response) {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export async function startLocalRouteServer({
|
|
236
|
-
port =
|
|
237
|
-
host =
|
|
237
|
+
port = FIXED_LOCAL_ROUTER_PORT,
|
|
238
|
+
host = FIXED_LOCAL_ROUTER_HOST,
|
|
238
239
|
configPath = getDefaultConfigPath(),
|
|
239
240
|
watchConfig = true,
|
|
240
241
|
configReloadDebounceMs = process.env.LLM_ROUTER_CONFIG_RELOAD_DEBOUNCE_MS,
|
|
@@ -1039,6 +1039,19 @@ export async function probeProviderEndpointMatrix(options) {
|
|
|
1039
1039
|
unresolvedModelSet.delete(modelId);
|
|
1040
1040
|
break;
|
|
1041
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
const resolvedFormats = modelFormatsMap[modelId] ? [...modelFormatsMap[modelId]] : [];
|
|
1044
|
+
const resolvedOwner = modelOwnerById.get(modelId) || null;
|
|
1045
|
+
emitProgress({
|
|
1046
|
+
phase: "model-done",
|
|
1047
|
+
model: modelId,
|
|
1048
|
+
confirmed: resolvedFormats.length > 0,
|
|
1049
|
+
formats: resolvedFormats,
|
|
1050
|
+
endpoint: resolvedOwner?.endpoint || null,
|
|
1051
|
+
format: resolvedOwner?.format || null,
|
|
1052
|
+
completedModels: models.length - unresolvedModelSet.size,
|
|
1053
|
+
totalModels: models.length
|
|
1054
|
+
});
|
|
1042
1055
|
}
|
|
1043
1056
|
|
|
1044
1057
|
for (const row of endpointRows) {
|