@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +337 -41
  3. package/package.json +19 -3
  4. package/src/cli/router-module.js +7331 -3805
  5. package/src/cli/wrangler-toml.js +1 -1
  6. package/src/cli-entry.js +162 -24
  7. package/src/node/amp-client-config.js +426 -0
  8. package/src/node/coding-tool-config.js +763 -0
  9. package/src/node/config-store.js +49 -18
  10. package/src/node/instance-state.js +213 -12
  11. package/src/node/listen-port.js +5 -37
  12. package/src/node/local-server-settings.js +122 -0
  13. package/src/node/local-server.js +3 -2
  14. package/src/node/provider-probe.js +13 -0
  15. package/src/node/start-command.js +282 -40
  16. package/src/node/startup-manager.js +64 -29
  17. package/src/node/web-command.js +106 -0
  18. package/src/node/web-console-assets.js +26 -0
  19. package/src/node/web-console-client.js +56 -0
  20. package/src/node/web-console-dev-assets.js +258 -0
  21. package/src/node/web-console-server.js +3146 -0
  22. package/src/node/web-console-styles.generated.js +1 -0
  23. package/src/node/web-console-ui/config-editor-utils.js +616 -0
  24. package/src/node/web-console-ui/lib/utils.js +6 -0
  25. package/src/node/web-console-ui/rate-limit-utils.js +144 -0
  26. package/src/node/web-console-ui/select-search-utils.js +36 -0
  27. package/src/runtime/codex-request-transformer.js +46 -5
  28. package/src/runtime/codex-response-transformer.js +268 -35
  29. package/src/runtime/config.js +1394 -35
  30. package/src/runtime/handler/amp-gemini.js +913 -0
  31. package/src/runtime/handler/amp-response.js +308 -0
  32. package/src/runtime/handler/amp.js +290 -0
  33. package/src/runtime/handler/auth.js +17 -2
  34. package/src/runtime/handler/provider-call.js +168 -50
  35. package/src/runtime/handler/provider-translation.js +937 -26
  36. package/src/runtime/handler/request.js +149 -6
  37. package/src/runtime/handler/route-debug.js +22 -1
  38. package/src/runtime/handler.js +449 -9
  39. package/src/runtime/subscription-auth.js +1 -6
  40. package/src/shared/local-router-defaults.js +62 -0
  41. package/src/translator/index.js +3 -1
  42. package/src/translator/request/openai-to-claude.js +217 -6
  43. package/src/translator/response/openai-to-claude.js +206 -58
@@ -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
- export async function readConfigFile(filePath = getDefaultConfigPath(), options = {}) {
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 normalizeOptions = autoMigrate ? { migrateToVersion } : undefined;
31
- const normalized = normalizeRuntimeConfig(parsedRaw, normalizeOptions);
32
-
33
- if (autoMigrate && persistMigrated) {
34
- const payload = `${JSON.stringify(normalized, null, 2)}\n`;
35
- if (payload !== raw) {
36
- try {
37
- await fs.writeFile(filePath, payload, { encoding: "utf8", mode: 0o600 });
38
- await fs.chmod(filePath, 0o600);
39
- } catch {
40
- // Silent best-effort persistence: keep migrated config in memory even if disk write fails.
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 normalized;
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 normalizeOptions = autoMigrate ? { migrateToVersion } : undefined;
49
- return normalizeRuntimeConfig({}, normalizeOptions);
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 = normalizeRuntimeConfig(config, normalizeOptions);
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 = normalizeRuntimeConfig(migratedRaw, { migrateToVersion: targetVersion });
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 || "127.0.0.1"),
29
- port: Number.isFinite(Number(raw.port)) ? Number(raw.port) : 8787,
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: "127.0.0.1",
158
- port: 8787,
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 spawnDetachedStart({
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 = "127.0.0.1",
178
- port = 8787,
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
- const finalCliPath = String(cliPath || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim();
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: true,
198
- stdio: "ignore",
266
+ detached,
267
+ stdio,
199
268
  env: {
200
- ...process.env,
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
  }
@@ -1,43 +1,11 @@
1
- const DEFAULT_LISTEN_PORT = 8787;
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
- function parsePortValue(value) {
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 using this precedence:
18
- * 1) explicit CLI/API input
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
+ }
@@ -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 = 8787,
237
- host = "127.0.0.1",
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) {