@khanglvm/llm-router 2.2.7 → 2.3.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.
@@ -32,15 +32,20 @@ import {
32
32
  import {
33
33
  ensureClaudeCodeSettingsFileExists,
34
34
  ensureCodexCliConfigFileExists,
35
+ ensureFactoryDroidSettingsFileExists,
35
36
  patchClaudeCodeEffortLevel,
36
37
  patchClaudeCodeSettingsFile,
37
38
  patchCodexCliConfigFile,
39
+ patchFactoryDroidSettingsFile,
38
40
  readClaudeCodeRoutingState,
39
41
  readCodexCliRoutingState,
42
+ readFactoryDroidRoutingState,
40
43
  resolveClaudeCodeSettingsFilePath,
41
44
  resolveCodexCliConfigFilePath,
45
+ resolveFactoryDroidSettingsFilePath,
42
46
  unpatchClaudeCodeSettingsFile,
43
- unpatchCodexCliConfigFile
47
+ unpatchCodexCliConfigFile,
48
+ unpatchFactoryDroidSettingsFile
44
49
  } from "./coding-tool-config.js";
45
50
  import { loginSubscription } from "../runtime/subscription-provider.js";
46
51
  import {
@@ -62,7 +67,8 @@ import {
62
67
  CODEX_CLI_INHERIT_MODEL_VALUE,
63
68
  isCodexCliInheritModelBinding,
64
69
  normalizeClaudeCodeEffortLevel,
65
- normalizeCodexCliReasoningEffort
70
+ normalizeCodexCliReasoningEffort,
71
+ normalizeFactoryDroidReasoningEffort
66
72
  } from "../shared/coding-tool-bindings.js";
67
73
  import { applyActivityLogSettings, readActivityLogSettings } from "../shared/local-router-defaults.js";
68
74
  import {
@@ -435,6 +441,11 @@ function buildClaudeCodeEndpointUrl(settings = {}) {
435
441
  return origin ? `${origin}/anthropic` : "";
436
442
  }
437
443
 
444
+ function buildFactoryDroidEndpointUrl(settings = {}) {
445
+ const origin = buildAmpClientEndpointUrl(settings);
446
+ return origin ? `${origin}/openai/v1` : "";
447
+ }
448
+
438
449
  function buildRouterEndpoints({ host, port, running }) {
439
450
  if (!running) return [];
440
451
  const origin = getFixedLocalRouterOrigin();
@@ -1414,6 +1425,76 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
1414
1425
  }
1415
1426
  }
1416
1427
 
1428
+ async function readFactoryDroidGlobalRoutingState(settings = {}, config = null) {
1429
+ const endpointUrl = buildAmpClientEndpointUrl(settings);
1430
+ try {
1431
+ const state = await readFactoryDroidRoutingState({
1432
+ endpointUrl
1433
+ });
1434
+ return {
1435
+ ...state,
1436
+ endpointUrl,
1437
+ error: ""
1438
+ };
1439
+ } catch (error) {
1440
+ return {
1441
+ tool: "factory-droid",
1442
+ settingsFilePath: resolveFactoryDroidSettingsFilePath({}),
1443
+ backupFilePath: "",
1444
+ settingsExists: false,
1445
+ backupExists: false,
1446
+ routedViaRouter: false,
1447
+ configuredBaseUrl: "",
1448
+ bindings: {
1449
+ defaultModel: "",
1450
+ reasoningEffort: ""
1451
+ },
1452
+ endpointUrl,
1453
+ error: error instanceof Error ? error.message : String(error)
1454
+ };
1455
+ }
1456
+ }
1457
+
1458
+ async function syncFactoryDroidRoutingIfNeeded({
1459
+ previousConfig = null,
1460
+ nextConfig = null,
1461
+ previousSettings = {},
1462
+ nextSettings = {}
1463
+ } = {}) {
1464
+ const previousEndpointUrl = buildAmpClientEndpointUrl(previousSettings);
1465
+ const nextEndpointUrl = buildAmpClientEndpointUrl(nextSettings);
1466
+ const previousMasterKey = String(previousConfig?.masterKey || "").trim();
1467
+ const nextMasterKey = String(nextConfig?.masterKey || "").trim();
1468
+ const endpointOrKeyChanged = Boolean(
1469
+ previousEndpointUrl
1470
+ && nextEndpointUrl
1471
+ && (previousEndpointUrl !== nextEndpointUrl || previousMasterKey !== nextMasterKey)
1472
+ );
1473
+
1474
+ if (!endpointOrKeyChanged) return false;
1475
+
1476
+ const routingState = await readFactoryDroidGlobalRoutingState(previousSettings, previousConfig);
1477
+ if (routingState.error) {
1478
+ addLog("warn", "Factory Droid route check failed.", routingState.error);
1479
+ return false;
1480
+ }
1481
+ if (!routingState.routedViaRouter) return false;
1482
+
1483
+ try {
1484
+ await patchFactoryDroidSettingsFile({
1485
+ endpointUrl: nextEndpointUrl,
1486
+ apiKey: nextMasterKey,
1487
+ bindings: routingState.bindings,
1488
+ captureBackup: false
1489
+ });
1490
+ addLog("info", "Updated Factory Droid route to match the local router.", buildFactoryDroidEndpointUrl(nextSettings));
1491
+ return true;
1492
+ } catch (error) {
1493
+ addLog("warn", "Factory Droid route update failed.", error instanceof Error ? error.message : String(error));
1494
+ return false;
1495
+ }
1496
+ }
1497
+
1417
1498
  async function resolveProbeApiKey(apiKeyEnv, apiKey, { context = "testing config" } = {}) {
1418
1499
  const resolvedApiKeyEnv = String(apiKeyEnv || "").trim();
1419
1500
  const resolvedApiKey = String(apiKey || "").trim();
@@ -2056,6 +2137,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
2056
2137
  previousSettings: previousLocalServer,
2057
2138
  nextSettings: nextLocalServer
2058
2139
  });
2140
+ await syncFactoryDroidRoutingIfNeeded({
2141
+ previousConfig,
2142
+ nextConfig: savedConfig,
2143
+ previousSettings: previousLocalServer,
2144
+ nextSettings: nextLocalServer
2145
+ });
2059
2146
 
2060
2147
  const snapshot = await broadcastState();
2061
2148
  return {
@@ -2112,6 +2199,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
2112
2199
  const ampClientGlobal = await readAmpGlobalRoutingState(configLocalServer);
2113
2200
  const codexCliGlobal = await readCodexCliGlobalRoutingState(configLocalServer, configState.normalizedConfig);
2114
2201
  const claudeCodeGlobal = await readClaudeCodeGlobalRoutingState(configLocalServer, configState.normalizedConfig);
2202
+ const factoryDroidGlobal = await readFactoryDroidGlobalRoutingState(configLocalServer, configState.normalizedConfig);
2115
2203
  const webSearch = await readWebSearchState(configState.normalizedConfig).catch(() => null);
2116
2204
 
2117
2205
  return {
@@ -2140,7 +2228,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
2140
2228
  ampWebSearch: webSearch,
2141
2229
  codingTools: {
2142
2230
  codexCli: codexCliGlobal,
2143
- claudeCode: claudeCodeGlobal
2231
+ claudeCode: claudeCodeGlobal,
2232
+ factoryDroid: factoryDroidGlobal
2144
2233
  },
2145
2234
  defaults: {
2146
2235
  providerUserAgent: DEFAULT_PROVIDER_USER_AGENT
@@ -2343,6 +2432,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
2343
2432
  previousSettings: persistedLocalServer.previousSettings,
2344
2433
  nextSettings: persistedLocalServer.savedSettings
2345
2434
  });
2435
+ await syncFactoryDroidRoutingIfNeeded({
2436
+ previousConfig: persistedLocalServer.previousConfig,
2437
+ nextConfig: persistedLocalServer.savedConfig,
2438
+ previousSettings: persistedLocalServer.previousSettings,
2439
+ nextSettings: persistedLocalServer.savedSettings
2440
+ });
2346
2441
  result.snapshot = await buildSnapshot();
2347
2442
  }
2348
2443
  return result;
@@ -2387,6 +2482,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
2387
2482
  previousSettings: persistedLocalServer.previousSettings,
2388
2483
  nextSettings: persistedLocalServer.savedSettings
2389
2484
  });
2485
+ await syncFactoryDroidRoutingIfNeeded({
2486
+ previousConfig: persistedLocalServer.previousConfig,
2487
+ nextConfig: persistedLocalServer.savedConfig,
2488
+ previousSettings: persistedLocalServer.previousSettings,
2489
+ nextSettings: persistedLocalServer.savedSettings
2490
+ });
2390
2491
  }
2391
2492
  return {
2392
2493
  message: restart ? "Router restarted." : "Router started.",
@@ -3181,6 +3282,119 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
3181
3282
  return;
3182
3283
  }
3183
3284
 
3285
+ if (method === "POST" && requestUrl.pathname === "/api/factory-droid/global-route") {
3286
+ const body = await readJsonBody(req);
3287
+ const enabled = body?.enabled !== false;
3288
+ if (!enabled) {
3289
+ const unpatchResult = await unpatchFactoryDroidSettingsFile({});
3290
+ addLog("info", "Factory Droid routing disabled.");
3291
+ const snapshot = await broadcastState();
3292
+ sendJson(res, 200, {
3293
+ ...snapshot,
3294
+ message: "Factory Droid now routes directly.",
3295
+ codingTools: {
3296
+ ...(snapshot.codingTools || {}),
3297
+ factoryDroid: {
3298
+ ...(snapshot.codingTools?.factoryDroid || {}),
3299
+ unpatchResult
3300
+ }
3301
+ }
3302
+ });
3303
+ return;
3304
+ }
3305
+
3306
+ let parsed;
3307
+ try {
3308
+ if (body?.config && typeof body.config === "object" && !Array.isArray(body.config)) {
3309
+ parsed = body.config;
3310
+ } else {
3311
+ const rawText = String(body?.rawText || "");
3312
+ parsed = rawText.trim() ? JSON.parse(rawText) : {};
3313
+ }
3314
+ } catch (error) {
3315
+ sendJson(res, 400, {
3316
+ error: `Config JSON parse failed: ${error instanceof Error ? error.message : String(error)}`
3317
+ });
3318
+ return;
3319
+ }
3320
+
3321
+ const nextConfig = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3322
+ const endpointUrl = String(body?.endpointUrl || buildAmpClientEndpointUrl(getConfigLocalServer({
3323
+ normalizedConfig: nextConfig,
3324
+ parseError: ""
3325
+ }))).trim();
3326
+ const apiKey = String(body?.apiKey || nextConfig?.masterKey || "").trim();
3327
+ if (!endpointUrl || !apiKey) {
3328
+ sendJson(res, 400, { error: "Factory Droid routing needs a valid local router URL and gateway key." });
3329
+ return;
3330
+ }
3331
+
3332
+ const bindings = {
3333
+ defaultModel: String(body?.bindings?.defaultModel || "").trim(),
3334
+ reasoningEffort: normalizeFactoryDroidReasoningEffort(body?.bindings?.reasoningEffort)
3335
+ };
3336
+ const patchResult = await patchFactoryDroidSettingsFile({
3337
+ endpointUrl,
3338
+ apiKey,
3339
+ bindings,
3340
+ captureBackup: true
3341
+ });
3342
+ addLog("success", "Factory Droid routing enabled.", patchResult.baseUrl);
3343
+ const snapshot = await broadcastState();
3344
+ sendJson(res, 200, {
3345
+ ...snapshot,
3346
+ message: "Factory Droid now routes via LLM Router.",
3347
+ codingTools: {
3348
+ ...(snapshot.codingTools || {}),
3349
+ factoryDroid: {
3350
+ ...(snapshot.codingTools?.factoryDroid || {}),
3351
+ patchResult
3352
+ }
3353
+ }
3354
+ });
3355
+ return;
3356
+ }
3357
+
3358
+ if (method === "POST" && requestUrl.pathname === "/api/factory-droid/model-bindings") {
3359
+ const body = await readJsonBody(req);
3360
+ const configState = await readConfigState(configPath);
3361
+ const configLocalServer = getConfigLocalServer(configState);
3362
+ const endpointUrl = buildAmpClientEndpointUrl(configLocalServer);
3363
+ const apiKey = String(configState.normalizedConfig?.masterKey || "").trim();
3364
+ if (!endpointUrl || !apiKey) {
3365
+ sendJson(res, 400, { error: "Factory Droid bindings need a running local router URL and gateway key." });
3366
+ return;
3367
+ }
3368
+
3369
+ const routingState = await readFactoryDroidGlobalRoutingState(configLocalServer, configState.normalizedConfig);
3370
+ if (routingState.error) {
3371
+ sendJson(res, 400, { error: routingState.error });
3372
+ return;
3373
+ }
3374
+ if (!routingState.routedViaRouter) {
3375
+ sendJson(res, 400, { error: "Connect Factory Droid to LLM Router before updating model bindings." });
3376
+ return;
3377
+ }
3378
+
3379
+ const bindings = {
3380
+ defaultModel: String(body?.bindings?.defaultModel || "").trim(),
3381
+ reasoningEffort: normalizeFactoryDroidReasoningEffort(body?.bindings?.reasoningEffort)
3382
+ };
3383
+ const patchResult = await patchFactoryDroidSettingsFile({
3384
+ endpointUrl,
3385
+ apiKey,
3386
+ bindings,
3387
+ captureBackup: false
3388
+ });
3389
+ addLog("success", "Factory Droid model bindings updated.", patchResult.bindings.defaultModel || "Default");
3390
+ const snapshot = await broadcastState();
3391
+ sendJson(res, 200, {
3392
+ ...snapshot,
3393
+ message: "Factory Droid model bindings updated."
3394
+ });
3395
+ return;
3396
+ }
3397
+
3184
3398
  if (method === "POST" && requestUrl.pathname === "/api/config/open") {
3185
3399
  const body = await readJsonBody(req);
3186
3400
  const editorId = String(body?.editorId || "default").trim() || "default";
@@ -3288,6 +3502,21 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
3288
3502
  return;
3289
3503
  }
3290
3504
 
3505
+ if (method === "POST" && requestUrl.pathname === "/api/factory-droid/config/open") {
3506
+ const body = await readJsonBody(req);
3507
+ const editorId = String(body?.editorId || "default").trim() || "default";
3508
+ const ensured = await ensureFactoryDroidSettingsFileExists({});
3509
+ await openFileInEditorFn(editorId, ensured.settingsFilePath);
3510
+ addLog("info", `Opened Factory Droid config file in ${editorId}.`, ensured.settingsFilePath);
3511
+ sendJson(res, 200, {
3512
+ ok: true,
3513
+ editorId,
3514
+ filePath: ensured.settingsFilePath,
3515
+ backupFilePath: ensured.backupFilePath
3516
+ });
3517
+ return;
3518
+ }
3519
+
3291
3520
  if (method === "POST" && requestUrl.pathname === "/api/router/start") {
3292
3521
  const body = await readJsonBody(req);
3293
3522
  const { message, snapshot } = await startManagedRouter(body);
@@ -52,3 +52,16 @@ export function mapClaudeCodeThinkingTokensToLevel(value) {
52
52
 
53
53
  export const normalizeClaudeCodeEffortLevel = normalizeClaudeCodeThinkingLevel;
54
54
  export const migrateLegacyThinkingTokensToEffortLevel = mapClaudeCodeThinkingTokensToLevel;
55
+
56
+ export const FACTORY_DROID_REASONING_EFFORT_VALUES = Object.freeze([
57
+ "off",
58
+ "none",
59
+ "low",
60
+ "medium",
61
+ "high"
62
+ ]);
63
+
64
+ export function normalizeFactoryDroidReasoningEffort(value) {
65
+ const normalized = String(value || "").trim().toLowerCase();
66
+ return FACTORY_DROID_REASONING_EFFORT_VALUES.includes(normalized) ? normalized : "";
67
+ }