@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.
- package/CHANGELOG.md +5 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/cli/router-module.js +129 -4
- package/src/node/coding-tool-config.js +216 -1
- package/src/node/web-console-client.js +26 -26
- package/src/node/web-console-server.js +232 -3
- package/src/shared/coding-tool-bindings.js +13 -0
|
@@ -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
|
+
}
|