@khanglvm/llm-router 2.0.0-beta.1 → 2.0.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 +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getFixedLocalRouterOrigin,
|
|
16
16
|
readLocalServerSettings
|
|
17
17
|
} from "./local-server-settings.js";
|
|
18
|
+
import { appendActivityLogEntry, clearActivityLogFile, createActivityLogEntry, readActivityLogEntries, resolveActivityLogPath } from "./activity-log.js";
|
|
18
19
|
import { listListeningPids, reclaimPort } from "./port-reclaim.js";
|
|
19
20
|
import { probeProvider, probeProviderEndpointMatrix } from "./provider-probe.js";
|
|
20
21
|
import { installStartup, startupStatus, stopStartup, uninstallStartup } from "./startup-manager.js";
|
|
@@ -50,11 +51,32 @@ import {
|
|
|
50
51
|
resolveProviderApiKey,
|
|
51
52
|
validateRuntimeConfig
|
|
52
53
|
} from "../runtime/config.js";
|
|
54
|
+
import {
|
|
55
|
+
buildAmpWebSearchSnapshot,
|
|
56
|
+
testHostedWebSearchProviderRoute
|
|
57
|
+
} from "../runtime/handler/amp-web-search.js";
|
|
58
|
+
import { resolveRuntimeFlags, resolveStateStoreOptions } from "../runtime/handler/runtime-policy.js";
|
|
59
|
+
import { createStateStore } from "../runtime/state-store.js";
|
|
60
|
+
import {
|
|
61
|
+
CODEX_CLI_INHERIT_MODEL_VALUE,
|
|
62
|
+
isCodexCliInheritModelBinding,
|
|
63
|
+
normalizeClaudeCodeThinkingLevel,
|
|
64
|
+
normalizeCodexCliReasoningEffort
|
|
65
|
+
} from "../shared/coding-tool-bindings.js";
|
|
66
|
+
import { applyActivityLogSettings, readActivityLogSettings } from "../shared/local-router-defaults.js";
|
|
67
|
+
import {
|
|
68
|
+
createLiteLlmContextLookupHelper,
|
|
69
|
+
LITELLM_CONTEXT_CATALOG_URL
|
|
70
|
+
} from "./litellm-context-catalog.js";
|
|
53
71
|
|
|
54
72
|
const JSON_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
55
73
|
const MAX_LOG_ENTRIES = 150;
|
|
56
74
|
const WEB_CONSOLE_APP_JS = readFileSync(fileURLToPath(new URL("./web-console-client.js", import.meta.url)), "utf8");
|
|
57
75
|
|
|
76
|
+
function createLogStateSignature(entries = []) {
|
|
77
|
+
return JSON.stringify((entries || []).map((entry) => `${entry?.id || ""}:${entry?.time || ""}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
58
80
|
async function loadWebConsoleDevAssets() {
|
|
59
81
|
const module = await import("./web-console-dev-assets.js");
|
|
60
82
|
return module.startWebConsoleDevAssets;
|
|
@@ -576,6 +598,19 @@ async function ensureJsonObjectFileExists(filePath, initialValue = {}) {
|
|
|
576
598
|
}
|
|
577
599
|
}
|
|
578
600
|
|
|
601
|
+
async function ensureTextFileExists(filePath, initialValue = "") {
|
|
602
|
+
try {
|
|
603
|
+
await fs.access(filePath);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
609
|
+
await fs.writeFile(filePath, String(initialValue || ""), { encoding: "utf8", mode: 0o600 });
|
|
610
|
+
await fs.chmod(filePath, 0o600);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
579
614
|
export async function openFileInEditor(editorId, filePath, platform = process.platform) {
|
|
580
615
|
const targetPath = path.resolve(filePath);
|
|
581
616
|
|
|
@@ -774,6 +809,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
774
809
|
host = "127.0.0.1",
|
|
775
810
|
port = 8788,
|
|
776
811
|
configPath = getDefaultConfigPath(),
|
|
812
|
+
activityLogPath = "",
|
|
777
813
|
routerHost = FIXED_LOCAL_ROUTER_HOST,
|
|
778
814
|
routerPort = FIXED_LOCAL_ROUTER_PORT,
|
|
779
815
|
routerWatchConfig = true,
|
|
@@ -809,10 +845,32 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
809
845
|
const ampClientCwd = typeof deps.ampClientCwd === "string" && deps.ampClientCwd.trim() ? deps.ampClientCwd : process.cwd();
|
|
810
846
|
const codexCliEnv = deps.codexCliEnv && typeof deps.codexCliEnv === "object" ? deps.codexCliEnv : process.env;
|
|
811
847
|
const claudeCodeEnv = deps.claudeCodeEnv && typeof deps.claudeCodeEnv === "object" ? deps.claudeCodeEnv : process.env;
|
|
848
|
+
const runtimeEnv = deps.runtimeEnv && typeof deps.runtimeEnv === "object" ? deps.runtimeEnv : process.env;
|
|
812
849
|
const loadWebConsoleDevAssetsFn = typeof deps.loadWebConsoleDevAssets === "function"
|
|
813
850
|
? deps.loadWebConsoleDevAssets
|
|
814
851
|
: loadWebConsoleDevAssets;
|
|
815
852
|
const resolvedRouterCliPath = String(cliPathForRouter || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim();
|
|
853
|
+
const resolvedActivityLogPath = resolveActivityLogPath(configPath, activityLogPath);
|
|
854
|
+
|
|
855
|
+
async function readWebSearchState(config = null) {
|
|
856
|
+
if (!config || typeof config !== "object") return null;
|
|
857
|
+
const runtimeFlags = resolveRuntimeFlags({ runtime: "node" }, runtimeEnv);
|
|
858
|
+
const stateStore = await createStateStore(resolveStateStoreOptions({
|
|
859
|
+
runtime: "node",
|
|
860
|
+
defaultStateStoreBackend: "file"
|
|
861
|
+
}, runtimeEnv, runtimeFlags));
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
return await buildAmpWebSearchSnapshot(config, {
|
|
865
|
+
env: runtimeEnv,
|
|
866
|
+
stateStore
|
|
867
|
+
});
|
|
868
|
+
} finally {
|
|
869
|
+
if (typeof stateStore?.close === "function") {
|
|
870
|
+
await stateStore.close();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
816
874
|
|
|
817
875
|
async function resolvePreferredAmpSettingsTarget() {
|
|
818
876
|
const envOverride = String(ampClientEnv?.AMP_SETTINGS_FILE || "").trim();
|
|
@@ -910,15 +968,23 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
910
968
|
|
|
911
969
|
function normalizeCodexBindingsInput(bindings = {}, config = {}) {
|
|
912
970
|
const source = bindings && typeof bindings === "object" && !Array.isArray(bindings) ? bindings : {};
|
|
971
|
+
const defaultModel = String(source.defaultModel || "").trim();
|
|
913
972
|
return {
|
|
914
|
-
defaultModel:
|
|
973
|
+
defaultModel: isCodexCliInheritModelBinding(defaultModel)
|
|
974
|
+
? CODEX_CLI_INHERIT_MODEL_VALUE
|
|
975
|
+
: String(defaultModel || pickDefaultManagedRoute(config) || "").trim(),
|
|
976
|
+
thinkingLevel: normalizeCodexCliReasoningEffort(source.thinkingLevel)
|
|
915
977
|
};
|
|
916
978
|
}
|
|
917
979
|
|
|
918
980
|
function normalizeCodexBindingState(bindings = {}) {
|
|
919
981
|
const source = bindings && typeof bindings === "object" && !Array.isArray(bindings) ? bindings : {};
|
|
982
|
+
const defaultModel = String(source.defaultModel || "").trim();
|
|
920
983
|
return {
|
|
921
|
-
defaultModel:
|
|
984
|
+
defaultModel: isCodexCliInheritModelBinding(defaultModel)
|
|
985
|
+
? CODEX_CLI_INHERIT_MODEL_VALUE
|
|
986
|
+
: defaultModel,
|
|
987
|
+
thinkingLevel: normalizeCodexCliReasoningEffort(source.thinkingLevel)
|
|
922
988
|
};
|
|
923
989
|
}
|
|
924
990
|
|
|
@@ -990,10 +1056,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
990
1056
|
description,
|
|
991
1057
|
default_reasoning_level: "medium",
|
|
992
1058
|
supported_reasoning_levels: [
|
|
1059
|
+
{ effort: "minimal", description: "Minimum reasoning for the fastest supported responses" },
|
|
993
1060
|
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
994
1061
|
{ effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
|
|
995
1062
|
{ effort: "high", description: "Greater reasoning depth for complex problems" },
|
|
996
|
-
{ effort: "xhigh", description: "Extra high reasoning depth for complex problems" }
|
|
1063
|
+
{ effort: "xhigh", description: "Extra high reasoning depth for complex problems on supported models" }
|
|
997
1064
|
],
|
|
998
1065
|
shell_type: "shell_command",
|
|
999
1066
|
visibility: "list",
|
|
@@ -1024,6 +1091,9 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1024
1091
|
? config.modelAliases
|
|
1025
1092
|
: {};
|
|
1026
1093
|
const boundModel = String(bindings?.defaultModel || "").trim();
|
|
1094
|
+
if (isCodexCliInheritModelBinding(boundModel)) {
|
|
1095
|
+
return { models: [] };
|
|
1096
|
+
}
|
|
1027
1097
|
const catalogEntries = new Map();
|
|
1028
1098
|
const aliasIds = new Set(
|
|
1029
1099
|
Object.keys(aliases)
|
|
@@ -1063,7 +1133,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1063
1133
|
defaultOpusModel: String(source.defaultOpusModel || "").trim(),
|
|
1064
1134
|
defaultSonnetModel: String(source.defaultSonnetModel || "").trim(),
|
|
1065
1135
|
defaultHaikuModel: String(source.defaultHaikuModel || "").trim(),
|
|
1066
|
-
subagentModel: String(source.subagentModel || "").trim()
|
|
1136
|
+
subagentModel: String(source.subagentModel || "").trim(),
|
|
1137
|
+
thinkingLevel: normalizeClaudeCodeThinkingLevel(source.thinkingLevel)
|
|
1067
1138
|
};
|
|
1068
1139
|
}
|
|
1069
1140
|
|
|
@@ -1072,7 +1143,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1072
1143
|
}
|
|
1073
1144
|
|
|
1074
1145
|
function areCodexBindingsEqual(left = {}, right = {}) {
|
|
1075
|
-
|
|
1146
|
+
const normalizedLeft = normalizeCodexBindingState(left);
|
|
1147
|
+
const normalizedRight = normalizeCodexBindingState(right);
|
|
1148
|
+
return (
|
|
1149
|
+
normalizedLeft.defaultModel === normalizedRight.defaultModel
|
|
1150
|
+
&& normalizedLeft.thinkingLevel === normalizedRight.thinkingLevel
|
|
1151
|
+
);
|
|
1076
1152
|
}
|
|
1077
1153
|
|
|
1078
1154
|
function areClaudeBindingsEqual(left = {}, right = {}) {
|
|
@@ -1082,6 +1158,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1082
1158
|
&& String(left?.defaultSonnetModel || "").trim() === String(right?.defaultSonnetModel || "").trim()
|
|
1083
1159
|
&& String(left?.defaultHaikuModel || "").trim() === String(right?.defaultHaikuModel || "").trim()
|
|
1084
1160
|
&& String(left?.subagentModel || "").trim() === String(right?.subagentModel || "").trim()
|
|
1161
|
+
&& normalizeClaudeCodeThinkingLevel(left?.thinkingLevel) === normalizeClaudeCodeThinkingLevel(right?.thinkingLevel)
|
|
1085
1162
|
);
|
|
1086
1163
|
}
|
|
1087
1164
|
|
|
@@ -1091,13 +1168,26 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1091
1168
|
|
|
1092
1169
|
function reconcileCodexBindingsForConfig(bindings = {}, previousConfig = {}, nextConfig = {}) {
|
|
1093
1170
|
const currentBindings = normalizeCodexBindingState(bindings);
|
|
1171
|
+
if (isCodexCliInheritModelBinding(currentBindings.defaultModel)) {
|
|
1172
|
+
return {
|
|
1173
|
+
defaultModel: CODEX_CLI_INHERIT_MODEL_VALUE,
|
|
1174
|
+
thinkingLevel: currentBindings.thinkingLevel
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1094
1177
|
const rewriteContext = buildManagedRouteRewriteContext(previousConfig, nextConfig);
|
|
1095
1178
|
const nextDefaultModel = reconcileManagedRouteBinding(currentBindings.defaultModel, rewriteContext);
|
|
1096
1179
|
return {
|
|
1097
|
-
defaultModel: nextDefaultModel || pickDefaultManagedRoute(nextConfig)
|
|
1180
|
+
defaultModel: nextDefaultModel || pickDefaultManagedRoute(nextConfig),
|
|
1181
|
+
thinkingLevel: currentBindings.thinkingLevel
|
|
1098
1182
|
};
|
|
1099
1183
|
}
|
|
1100
1184
|
|
|
1185
|
+
function formatCodexBindingLabel(defaultModel = "") {
|
|
1186
|
+
return isCodexCliInheritModelBinding(defaultModel)
|
|
1187
|
+
? "Inherit Codex CLI model"
|
|
1188
|
+
: (String(defaultModel || "").trim() || "No model selected");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1101
1191
|
function reconcileClaudeBindingsForConfig(bindings = {}, previousConfig = {}, nextConfig = {}) {
|
|
1102
1192
|
const currentBindings = normalizeClaudeBindingState(bindings);
|
|
1103
1193
|
const rewriteContext = buildManagedRouteRewriteContext(previousConfig, nextConfig);
|
|
@@ -1106,7 +1196,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1106
1196
|
defaultOpusModel: reconcileManagedRouteBinding(currentBindings.defaultOpusModel, rewriteContext),
|
|
1107
1197
|
defaultSonnetModel: reconcileManagedRouteBinding(currentBindings.defaultSonnetModel, rewriteContext),
|
|
1108
1198
|
defaultHaikuModel: reconcileManagedRouteBinding(currentBindings.defaultHaikuModel, rewriteContext),
|
|
1109
|
-
subagentModel: reconcileManagedRouteBinding(currentBindings.subagentModel, rewriteContext)
|
|
1199
|
+
subagentModel: reconcileManagedRouteBinding(currentBindings.subagentModel, rewriteContext),
|
|
1200
|
+
thinkingLevel: currentBindings.thinkingLevel
|
|
1110
1201
|
};
|
|
1111
1202
|
}
|
|
1112
1203
|
|
|
@@ -1134,7 +1225,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1134
1225
|
routedViaRouter: false,
|
|
1135
1226
|
configuredBaseUrl: "",
|
|
1136
1227
|
bindings: {
|
|
1137
|
-
defaultModel: ""
|
|
1228
|
+
defaultModel: "",
|
|
1229
|
+
thinkingLevel: ""
|
|
1138
1230
|
},
|
|
1139
1231
|
endpointUrl,
|
|
1140
1232
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -1170,7 +1262,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1170
1262
|
defaultOpusModel: "",
|
|
1171
1263
|
defaultSonnetModel: "",
|
|
1172
1264
|
defaultHaikuModel: "",
|
|
1173
|
-
subagentModel: ""
|
|
1265
|
+
subagentModel: "",
|
|
1266
|
+
thinkingLevel: ""
|
|
1174
1267
|
},
|
|
1175
1268
|
endpointUrl,
|
|
1176
1269
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -1263,7 +1356,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1263
1356
|
if (endpointOrKeyChanged) {
|
|
1264
1357
|
addLog("info", "Updated Codex CLI route to match the local router.", buildCodexCliEndpointUrl(nextSettings));
|
|
1265
1358
|
} else {
|
|
1266
|
-
addLog("info", "Updated Codex CLI bindings to match the saved router config.", bindings.defaultModel
|
|
1359
|
+
addLog("info", "Updated Codex CLI bindings to match the saved router config.", formatCodexBindingLabel(bindings.defaultModel));
|
|
1267
1360
|
}
|
|
1268
1361
|
return true;
|
|
1269
1362
|
} catch (error) {
|
|
@@ -1411,10 +1504,45 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1411
1504
|
onProgress
|
|
1412
1505
|
});
|
|
1413
1506
|
};
|
|
1507
|
+
const testHostedWebSearchProviderFn = typeof deps.testHostedWebSearchProvider === "function"
|
|
1508
|
+
? deps.testHostedWebSearchProvider
|
|
1509
|
+
: async ({ runtimeConfig, providerId, modelId }) => {
|
|
1510
|
+
const resolvedProviderId = String(providerId || "").trim();
|
|
1511
|
+
const resolvedModelId = String(modelId || "").trim();
|
|
1512
|
+
if (!resolvedProviderId || !resolvedModelId) {
|
|
1513
|
+
const error = new Error("Provider id and model id are required before testing hosted web search.");
|
|
1514
|
+
error.statusCode = 400;
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
try {
|
|
1519
|
+
return await testHostedWebSearchProviderRoute({
|
|
1520
|
+
runtimeConfig,
|
|
1521
|
+
routeId: `${resolvedProviderId}/${resolvedModelId}`,
|
|
1522
|
+
env: runtimeEnv
|
|
1523
|
+
});
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1526
|
+
if (error && typeof error === "object" && Number.isFinite(error.statusCode)) {
|
|
1527
|
+
throw error;
|
|
1528
|
+
}
|
|
1529
|
+
const wrapped = new Error(message);
|
|
1530
|
+
wrapped.statusCode = (
|
|
1531
|
+
message.includes("not configured")
|
|
1532
|
+
|| message.includes("not OpenAI-compatible")
|
|
1533
|
+
|| message.includes("is required")
|
|
1534
|
+
) ? 400 : 502;
|
|
1535
|
+
throw wrapped;
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
const lookupLiteLlmContextWindowFn = typeof deps.lookupLiteLlmContextWindow === "function"
|
|
1539
|
+
? deps.lookupLiteLlmContextWindow
|
|
1540
|
+
: createLiteLlmContextLookupHelper();
|
|
1414
1541
|
|
|
1415
1542
|
const eventClients = new Set();
|
|
1416
1543
|
const devEventClients = new Set();
|
|
1417
1544
|
const logs = [];
|
|
1545
|
+
let logStateSignature = createLogStateSignature(logs);
|
|
1418
1546
|
let webServer = null;
|
|
1419
1547
|
let closing = false;
|
|
1420
1548
|
let resolveDone;
|
|
@@ -1423,7 +1551,10 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1423
1551
|
let actualWebPort = Number(port);
|
|
1424
1552
|
let configWatcher = null;
|
|
1425
1553
|
let configWatchTimer = null;
|
|
1554
|
+
let activityLogWatcher = null;
|
|
1555
|
+
let activityLogWatchTimer = null;
|
|
1426
1556
|
let ignoreConfigWatchUntil = 0;
|
|
1557
|
+
let activityLogEnabled = true;
|
|
1427
1558
|
|
|
1428
1559
|
const routerState = {
|
|
1429
1560
|
host: FIXED_LOCAL_ROUTER_HOST,
|
|
@@ -1456,20 +1587,89 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1456
1587
|
}
|
|
1457
1588
|
}
|
|
1458
1589
|
|
|
1590
|
+
function replaceLogs(nextEntries = []) {
|
|
1591
|
+
logs.splice(0, logs.length, ...nextEntries);
|
|
1592
|
+
logStateSignature = createLogStateSignature(logs);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function resolveActivityLogSnapshot(config = undefined) {
|
|
1596
|
+
if (config !== undefined) {
|
|
1597
|
+
const settings = readActivityLogSettings(config);
|
|
1598
|
+
activityLogEnabled = settings.enabled;
|
|
1599
|
+
return settings;
|
|
1600
|
+
}
|
|
1601
|
+
return { enabled: activityLogEnabled };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function emitLogState(config = undefined) {
|
|
1605
|
+
pushEvent("logs", {
|
|
1606
|
+
logs,
|
|
1607
|
+
activityLog: resolveActivityLogSnapshot(config)
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async function syncLogsFromFile({ config = undefined, broadcast = false } = {}) {
|
|
1612
|
+
const nextEntries = await readActivityLogEntries(resolvedActivityLogPath, {
|
|
1613
|
+
limit: MAX_LOG_ENTRIES
|
|
1614
|
+
});
|
|
1615
|
+
const nextSignature = createLogStateSignature(nextEntries);
|
|
1616
|
+
if (nextSignature === logStateSignature) return false;
|
|
1617
|
+
replaceLogs(nextEntries);
|
|
1618
|
+
if (broadcast) {
|
|
1619
|
+
emitLogState(config);
|
|
1620
|
+
}
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function scheduleActivityLogRefresh(reason = "change") {
|
|
1625
|
+
if (activityLogWatchTimer) clearTimeout(activityLogWatchTimer);
|
|
1626
|
+
activityLogWatchTimer = setTimeout(() => {
|
|
1627
|
+
activityLogWatchTimer = null;
|
|
1628
|
+
if (closing) return;
|
|
1629
|
+
void syncLogsFromFile({ broadcast: true }).catch(() => {});
|
|
1630
|
+
}, reason === "clear" ? 0 : 80);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function startActivityLogWatcher() {
|
|
1634
|
+
const activityLogDir = path.dirname(resolvedActivityLogPath);
|
|
1635
|
+
const activityLogFile = path.basename(resolvedActivityLogPath);
|
|
1636
|
+
try {
|
|
1637
|
+
activityLogWatcher = fsWatch(activityLogDir, (_eventType, filename) => {
|
|
1638
|
+
if (closing) return;
|
|
1639
|
+
if (filename && String(filename) !== activityLogFile) return;
|
|
1640
|
+
scheduleActivityLogRefresh("watch");
|
|
1641
|
+
});
|
|
1642
|
+
} catch {
|
|
1643
|
+
activityLogWatcher = null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1459
1647
|
function addLog(level, message, detail = "") {
|
|
1460
|
-
|
|
1461
|
-
|
|
1648
|
+
if (!activityLogEnabled) return null;
|
|
1649
|
+
const entry = createActivityLogEntry({
|
|
1462
1650
|
level,
|
|
1463
1651
|
message,
|
|
1464
1652
|
detail,
|
|
1465
|
-
|
|
1466
|
-
|
|
1653
|
+
source: "web-console",
|
|
1654
|
+
category: "router",
|
|
1655
|
+
kind: "router-event"
|
|
1656
|
+
});
|
|
1467
1657
|
logs.unshift(entry);
|
|
1468
1658
|
logs.splice(MAX_LOG_ENTRIES);
|
|
1659
|
+
logStateSignature = createLogStateSignature(logs);
|
|
1469
1660
|
pushEvent("log", entry);
|
|
1661
|
+
void appendActivityLogEntry(resolvedActivityLogPath, entry).catch(() => {});
|
|
1470
1662
|
return entry;
|
|
1471
1663
|
}
|
|
1472
1664
|
|
|
1665
|
+
try {
|
|
1666
|
+
const initialConfigState = await readConfigState(configPath);
|
|
1667
|
+
resolveActivityLogSnapshot(initialConfigState.normalizedConfig);
|
|
1668
|
+
await syncLogsFromFile({ config: initialConfigState.normalizedConfig });
|
|
1669
|
+
} catch {
|
|
1670
|
+
activityLogEnabled = true;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1473
1673
|
const devReloadScript = devMode ? String.raw`<script>
|
|
1474
1674
|
(() => {
|
|
1475
1675
|
const source = new EventSource("/__dev/events");
|
|
@@ -1602,7 +1802,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1602
1802
|
};
|
|
1603
1803
|
}
|
|
1604
1804
|
|
|
1605
|
-
async function stopExternalRuntime(runtime, { reason = "Stopped another
|
|
1805
|
+
async function stopExternalRuntime(runtime, { reason = "Stopped another LLM Router instance." } = {}) {
|
|
1606
1806
|
if (!runtime || Number(runtime.pid) === Number(process.pid)) return false;
|
|
1607
1807
|
|
|
1608
1808
|
const runtimeUrl = `http://${formatHostForUrl(runtime.host, runtime.port)}`;
|
|
@@ -1615,7 +1815,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1615
1815
|
|
|
1616
1816
|
const stopped = await stopProcessByPidFn(runtime.pid);
|
|
1617
1817
|
if (!stopped?.ok) {
|
|
1618
|
-
const error = new Error(stopped?.reason || `Failed stopping
|
|
1818
|
+
const error = new Error(stopped?.reason || `Failed stopping LLM Router pid ${runtime.pid}.`);
|
|
1619
1819
|
error.statusCode = 409;
|
|
1620
1820
|
throw error;
|
|
1621
1821
|
}
|
|
@@ -1625,7 +1825,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1625
1825
|
return true;
|
|
1626
1826
|
}
|
|
1627
1827
|
|
|
1628
|
-
async function stopUntrackedStartupRuntime({ reason = "Stopped startup-managed
|
|
1828
|
+
async function stopUntrackedStartupRuntime({ reason = "Stopped startup-managed LLM Router." } = {}) {
|
|
1629
1829
|
const startup = await startupStatusFn().catch(() => null);
|
|
1630
1830
|
if (!startup?.running) return false;
|
|
1631
1831
|
await stopStartupFn();
|
|
@@ -1662,7 +1862,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1662
1862
|
}
|
|
1663
1863
|
|
|
1664
1864
|
if (!runtime) {
|
|
1665
|
-
const startError = new Error(`Startup-managed
|
|
1865
|
+
const startError = new Error(`Startup-managed LLM Router did not become ready on http://${settings.host}:${settings.port}.`);
|
|
1666
1866
|
startError.statusCode = 500;
|
|
1667
1867
|
throw startError;
|
|
1668
1868
|
}
|
|
@@ -1712,11 +1912,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1712
1912
|
const externalRuntime = await readExternalRuntime(configLocalServer);
|
|
1713
1913
|
if (externalRuntime) {
|
|
1714
1914
|
await stopExternalRuntime(externalRuntime, {
|
|
1715
|
-
reason: `Stopped existing
|
|
1915
|
+
reason: `Stopped an existing LLM Router instance so the web console can manage ${configLocalServer.host}:${configLocalServer.port} during ${reason}.`
|
|
1716
1916
|
});
|
|
1717
1917
|
} else {
|
|
1718
1918
|
await stopUntrackedStartupRuntime({
|
|
1719
|
-
reason: `Stopped startup-managed
|
|
1919
|
+
reason: `Stopped the startup-managed LLM Router instance so the web console can manage ${configLocalServer.host}:${configLocalServer.port} during ${reason}.`
|
|
1720
1920
|
});
|
|
1721
1921
|
}
|
|
1722
1922
|
|
|
@@ -1777,12 +1977,31 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1777
1977
|
};
|
|
1778
1978
|
}
|
|
1779
1979
|
|
|
1980
|
+
async function persistActivityLogConfig(enabled) {
|
|
1981
|
+
const currentConfigState = await readConfigState(configPath);
|
|
1982
|
+
if (currentConfigState.parseError) {
|
|
1983
|
+
const error = new Error(`Config JSON must parse before saving activity log settings: ${currentConfigState.parseError}`);
|
|
1984
|
+
error.statusCode = 400;
|
|
1985
|
+
throw error;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const nextConfig = applyActivityLogSettings(
|
|
1989
|
+
currentConfigState.normalizedConfig || buildDefaultConfigObject(),
|
|
1990
|
+
{ enabled: enabled === true }
|
|
1991
|
+
);
|
|
1992
|
+
ignoreConfigWatchUntil = Date.now() + 800;
|
|
1993
|
+
const savedConfig = await writeConfigFile(nextConfig, configPath, { migrateToVersion: CONFIG_VERSION });
|
|
1994
|
+
resolveActivityLogSnapshot(savedConfig);
|
|
1995
|
+
return savedConfig;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1780
1998
|
async function writeAndBroadcastConfig(parsed, { source = "" } = {}) {
|
|
1781
1999
|
const previousConfigState = await readConfigState(configPath);
|
|
1782
2000
|
const previousConfig = previousConfigState.normalizedConfig || buildDefaultConfigObject();
|
|
1783
2001
|
const previousLocalServer = getConfigLocalServer(previousConfigState);
|
|
1784
2002
|
ignoreConfigWatchUntil = Date.now() + 800;
|
|
1785
2003
|
const savedConfig = await writeConfigFile(parsed, configPath, { migrateToVersion: CONFIG_VERSION });
|
|
2004
|
+
resolveActivityLogSnapshot(savedConfig);
|
|
1786
2005
|
const nextLocalServer = readLocalServerSettings(savedConfig, previousLocalServer);
|
|
1787
2006
|
|
|
1788
2007
|
if (source !== "autosave") {
|
|
@@ -1870,6 +2089,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1870
2089
|
async function buildSnapshot() {
|
|
1871
2090
|
const configState = await readConfigState(configPath);
|
|
1872
2091
|
const configLocalServer = getConfigLocalServer(configState);
|
|
2092
|
+
const activityLog = resolveActivityLogSnapshot(configState.normalizedConfig);
|
|
1873
2093
|
const startup = await startupStatusFn().catch((error) => ({
|
|
1874
2094
|
manager: "unknown",
|
|
1875
2095
|
serviceId: "llm-router",
|
|
@@ -1891,6 +2111,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1891
2111
|
const ampClientGlobal = await readAmpGlobalRoutingState(configLocalServer);
|
|
1892
2112
|
const codexCliGlobal = await readCodexCliGlobalRoutingState(configLocalServer, configState.normalizedConfig);
|
|
1893
2113
|
const claudeCodeGlobal = await readClaudeCodeGlobalRoutingState(configLocalServer, configState.normalizedConfig);
|
|
2114
|
+
const webSearch = await readWebSearchState(configState.normalizedConfig).catch(() => null);
|
|
1894
2115
|
|
|
1895
2116
|
return {
|
|
1896
2117
|
web: {
|
|
@@ -1914,6 +2135,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1914
2135
|
ampClient: {
|
|
1915
2136
|
global: ampClientGlobal
|
|
1916
2137
|
},
|
|
2138
|
+
webSearch,
|
|
2139
|
+
ampWebSearch: webSearch,
|
|
1917
2140
|
codingTools: {
|
|
1918
2141
|
codexCli: codexCliGlobal,
|
|
1919
2142
|
claudeCode: claudeCodeGlobal
|
|
@@ -1923,6 +2146,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1923
2146
|
},
|
|
1924
2147
|
editors: detectAvailableEditorsFn(),
|
|
1925
2148
|
externalRuntime,
|
|
2149
|
+
activityLog,
|
|
1926
2150
|
logs
|
|
1927
2151
|
};
|
|
1928
2152
|
}
|
|
@@ -1939,14 +2163,23 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1939
2163
|
configWatchTimer = null;
|
|
1940
2164
|
if (closing) return;
|
|
1941
2165
|
if (Date.now() < ignoreConfigWatchUntil) return;
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2166
|
+
void (async () => {
|
|
2167
|
+
let latestConfigState = null;
|
|
2168
|
+
try {
|
|
2169
|
+
latestConfigState = await readConfigState(configPath);
|
|
2170
|
+
resolveActivityLogSnapshot(latestConfigState.normalizedConfig);
|
|
2171
|
+
} catch {
|
|
2172
|
+
latestConfigState = null;
|
|
2173
|
+
}
|
|
2174
|
+
addLog("info", `Config file changed on disk (${reason}).`);
|
|
2175
|
+
try {
|
|
2176
|
+
await reconcileManagedRouterWithConfig({ reason: `config-watch:${reason}` });
|
|
2177
|
+
} catch (reconcileError) {
|
|
1945
2178
|
addLog("warn", "Managed router auto-start skipped.", reconcileError instanceof Error ? reconcileError.message : String(reconcileError));
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2179
|
+
} finally {
|
|
2180
|
+
await broadcastState().catch(() => {});
|
|
2181
|
+
}
|
|
2182
|
+
})();
|
|
1950
2183
|
}, 150);
|
|
1951
2184
|
}
|
|
1952
2185
|
|
|
@@ -1995,7 +2228,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1995
2228
|
|
|
1996
2229
|
const stopped = await stopProcessByPidFn(activeRuntime.pid);
|
|
1997
2230
|
if (!stopped?.ok) {
|
|
1998
|
-
const stopError = new Error(stopped?.reason || `Failed stopping
|
|
2231
|
+
const stopError = new Error(stopped?.reason || `Failed stopping LLM Router pid ${activeRuntime.pid}.`);
|
|
1999
2232
|
stopError.statusCode = 409;
|
|
2000
2233
|
throw stopError;
|
|
2001
2234
|
}
|
|
@@ -2070,11 +2303,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2070
2303
|
const externalRuntime = await readExternalRuntime(nextOptions);
|
|
2071
2304
|
if (externalRuntime) {
|
|
2072
2305
|
await stopExternalRuntime(externalRuntime, {
|
|
2073
|
-
reason: "Stopped another
|
|
2306
|
+
reason: "Stopped another LLM Router instance before starting the managed router."
|
|
2074
2307
|
});
|
|
2075
2308
|
} else {
|
|
2076
2309
|
await stopUntrackedStartupRuntime({
|
|
2077
|
-
reason: "Stopped startup-managed
|
|
2310
|
+
reason: "Stopped the startup-managed LLM Router instance before starting the managed router."
|
|
2078
2311
|
});
|
|
2079
2312
|
}
|
|
2080
2313
|
|
|
@@ -2124,7 +2357,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2124
2357
|
requireAuth: nextOptions.requireAuth
|
|
2125
2358
|
});
|
|
2126
2359
|
if (!started?.ok) {
|
|
2127
|
-
const startError = new Error(started?.errorMessage || `Failed to start
|
|
2360
|
+
const startError = new Error(started?.errorMessage || `Failed to start LLM Router on http://${nextOptions.host}:${nextOptions.port}.`);
|
|
2128
2361
|
startError.statusCode = 500;
|
|
2129
2362
|
throw startError;
|
|
2130
2363
|
}
|
|
@@ -2178,6 +2411,14 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2178
2411
|
configWatcher.close();
|
|
2179
2412
|
configWatcher = null;
|
|
2180
2413
|
}
|
|
2414
|
+
if (activityLogWatchTimer) {
|
|
2415
|
+
clearTimeout(activityLogWatchTimer);
|
|
2416
|
+
activityLogWatchTimer = null;
|
|
2417
|
+
}
|
|
2418
|
+
if (activityLogWatcher) {
|
|
2419
|
+
activityLogWatcher.close();
|
|
2420
|
+
activityLogWatcher = null;
|
|
2421
|
+
}
|
|
2181
2422
|
|
|
2182
2423
|
for (const client of eventClients) {
|
|
2183
2424
|
client.end();
|
|
@@ -2261,6 +2502,33 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2261
2502
|
return;
|
|
2262
2503
|
}
|
|
2263
2504
|
|
|
2505
|
+
if (method === "POST" && requestUrl.pathname === "/api/activity-log/settings") {
|
|
2506
|
+
const body = await readJsonBody(req);
|
|
2507
|
+
const enabled = body?.enabled === false ? false : true;
|
|
2508
|
+
const savedConfig = await persistActivityLogConfig(enabled);
|
|
2509
|
+
await syncLogsFromFile({ config: savedConfig });
|
|
2510
|
+
if (enabled) {
|
|
2511
|
+
addLog("info", "Activity logging enabled.");
|
|
2512
|
+
}
|
|
2513
|
+
const snapshot = await broadcastState();
|
|
2514
|
+
sendJson(res, 200, {
|
|
2515
|
+
...snapshot,
|
|
2516
|
+
message: enabled ? "Activity log enabled." : "Activity log disabled."
|
|
2517
|
+
});
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
if (method === "POST" && requestUrl.pathname === "/api/activity-log/clear") {
|
|
2522
|
+
await clearActivityLogFile(resolvedActivityLogPath);
|
|
2523
|
+
replaceLogs([]);
|
|
2524
|
+
emitLogState();
|
|
2525
|
+
sendJson(res, 200, {
|
|
2526
|
+
...(await buildSnapshot()),
|
|
2527
|
+
message: "Activity log cleared."
|
|
2528
|
+
});
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2264
2532
|
if (method === "POST" && requestUrl.pathname === "/api/config/validate") {
|
|
2265
2533
|
const body = await readJsonBody(req);
|
|
2266
2534
|
const rawText = body?.rawText !== undefined
|
|
@@ -2322,6 +2590,48 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2322
2590
|
return;
|
|
2323
2591
|
}
|
|
2324
2592
|
|
|
2593
|
+
if (method === "POST" && requestUrl.pathname === "/api/config/test-web-search-provider") {
|
|
2594
|
+
const body = await readJsonBody(req);
|
|
2595
|
+
const providerId = String(body?.providerId || "").trim();
|
|
2596
|
+
const modelId = String(body?.modelId || "").trim();
|
|
2597
|
+
const rawText = body?.rawText !== undefined
|
|
2598
|
+
? String(body.rawText || "")
|
|
2599
|
+
: `${JSON.stringify(body?.config || {}, null, 2)}\n`;
|
|
2600
|
+
|
|
2601
|
+
if (!providerId || !modelId) {
|
|
2602
|
+
sendJson(res, 400, { error: "Provider id and model id are required before testing hosted web search." });
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
let normalizedConfig = null;
|
|
2607
|
+
try {
|
|
2608
|
+
const parsed = body?.config && typeof body.config === "object" && !Array.isArray(body.config)
|
|
2609
|
+
? body.config
|
|
2610
|
+
: (rawText.trim() ? JSON.parse(rawText) : {});
|
|
2611
|
+
normalizedConfig = normalizeRuntimeConfig(parsed, { migrateToVersion: CONFIG_VERSION });
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
sendJson(res, 400, { error: `Current config draft is invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
addLog("info", "Testing hosted web search route.", `${providerId}/${modelId}`);
|
|
2618
|
+
try {
|
|
2619
|
+
const result = await testHostedWebSearchProviderFn({
|
|
2620
|
+
runtimeConfig: normalizedConfig,
|
|
2621
|
+
providerId,
|
|
2622
|
+
modelId
|
|
2623
|
+
});
|
|
2624
|
+
addLog("success", "Hosted web search route is ready.", `${providerId}/${modelId}`);
|
|
2625
|
+
sendJson(res, 200, { result });
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
addLog("warn", "Hosted web search route test failed.", error instanceof Error ? error.message : String(error));
|
|
2628
|
+
sendJson(res, Number(error?.statusCode) || 502, {
|
|
2629
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2325
2635
|
if (method === "POST" && requestUrl.pathname === "/api/config/test-provider-stream") {
|
|
2326
2636
|
const body = await readJsonBody(req);
|
|
2327
2637
|
const endpoints = Array.isArray(body?.endpoints) ? body.endpoints.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
|
|
@@ -2400,6 +2710,34 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2400
2710
|
return;
|
|
2401
2711
|
}
|
|
2402
2712
|
|
|
2713
|
+
if (method === "POST" && requestUrl.pathname === "/api/config/litellm-context-lookup") {
|
|
2714
|
+
const body = await readJsonBody(req);
|
|
2715
|
+
const models = Array.isArray(body?.models) ? body.models.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
|
|
2716
|
+
if (models.length === 0) {
|
|
2717
|
+
sendJson(res, 400, { error: "At least one model id is required before looking up context windows." });
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
try {
|
|
2722
|
+
const result = await lookupLiteLlmContextWindowFn({
|
|
2723
|
+
models,
|
|
2724
|
+
limit: body?.limit
|
|
2725
|
+
});
|
|
2726
|
+
sendJson(res, 200, {
|
|
2727
|
+
result,
|
|
2728
|
+
source: {
|
|
2729
|
+
provider: "litellm",
|
|
2730
|
+
url: LITELLM_CONTEXT_CATALOG_URL
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
sendJson(res, Number(error?.statusCode) || 502, {
|
|
2735
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2403
2741
|
if (method === "POST" && requestUrl.pathname === "/api/config/save") {
|
|
2404
2742
|
const body = await readJsonBody(req);
|
|
2405
2743
|
let parsed;
|
|
@@ -2586,7 +2924,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2586
2924
|
const snapshot = await broadcastState();
|
|
2587
2925
|
sendJson(res, 200, {
|
|
2588
2926
|
...snapshot,
|
|
2589
|
-
message: "AMP now routes via LLM
|
|
2927
|
+
message: "AMP now routes via LLM Router.",
|
|
2590
2928
|
amp: {
|
|
2591
2929
|
patchResult,
|
|
2592
2930
|
bootstrapDefaultRoute: bootstrap.bootstrapRouteRef || "",
|
|
@@ -2660,7 +2998,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2660
2998
|
const snapshot = await broadcastState();
|
|
2661
2999
|
sendJson(res, 200, {
|
|
2662
3000
|
...snapshot,
|
|
2663
|
-
message: "Codex CLI now routes via LLM
|
|
3001
|
+
message: "Codex CLI now routes via LLM Router.",
|
|
2664
3002
|
codingTools: {
|
|
2665
3003
|
...(snapshot.codingTools || {}),
|
|
2666
3004
|
codexCli: {
|
|
@@ -2689,7 +3027,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2689
3027
|
return;
|
|
2690
3028
|
}
|
|
2691
3029
|
if (!routingState.routedViaRouter) {
|
|
2692
|
-
sendJson(res, 400, { error: "Connect Codex CLI to LLM
|
|
3030
|
+
sendJson(res, 400, { error: "Connect Codex CLI to LLM Router before updating model bindings." });
|
|
2693
3031
|
return;
|
|
2694
3032
|
}
|
|
2695
3033
|
|
|
@@ -2702,7 +3040,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2702
3040
|
captureBackup: false,
|
|
2703
3041
|
env: codexCliEnv
|
|
2704
3042
|
});
|
|
2705
|
-
addLog("success", "Codex CLI model binding updated.", patchResult.bindings.defaultModel
|
|
3043
|
+
addLog("success", "Codex CLI model binding updated.", formatCodexBindingLabel(patchResult.bindings.defaultModel));
|
|
2706
3044
|
const snapshot = await broadcastState();
|
|
2707
3045
|
sendJson(res, 200, {
|
|
2708
3046
|
...snapshot,
|
|
@@ -2773,7 +3111,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2773
3111
|
const snapshot = await broadcastState();
|
|
2774
3112
|
sendJson(res, 200, {
|
|
2775
3113
|
...snapshot,
|
|
2776
|
-
message: "Claude Code now routes via LLM
|
|
3114
|
+
message: "Claude Code now routes via LLM Router.",
|
|
2777
3115
|
codingTools: {
|
|
2778
3116
|
...(snapshot.codingTools || {}),
|
|
2779
3117
|
claudeCode: {
|
|
@@ -2802,7 +3140,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2802
3140
|
return;
|
|
2803
3141
|
}
|
|
2804
3142
|
if (!routingState.routedViaRouter) {
|
|
2805
|
-
sendJson(res, 400, { error: "Connect Claude Code to LLM
|
|
3143
|
+
sendJson(res, 400, { error: "Connect Claude Code to LLM Router before updating model bindings." });
|
|
2806
3144
|
return;
|
|
2807
3145
|
}
|
|
2808
3146
|
|
|
@@ -2831,6 +3169,49 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2831
3169
|
return;
|
|
2832
3170
|
}
|
|
2833
3171
|
|
|
3172
|
+
if (method === "POST" && requestUrl.pathname === "/api/file/open") {
|
|
3173
|
+
const body = await readJsonBody(req);
|
|
3174
|
+
const editorId = String(body?.editorId || "default").trim() || "default";
|
|
3175
|
+
const rawFilePath = String(body?.filePath || "").trim();
|
|
3176
|
+
const ensureMode = String(body?.ensureMode || "none").trim() || "none";
|
|
3177
|
+
if (!rawFilePath) {
|
|
3178
|
+
sendJson(res, 400, { error: "A file path is required." });
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
if (!["none", "text", "jsonObject"].includes(ensureMode)) {
|
|
3182
|
+
sendJson(res, 400, { error: "Unsupported file ensure mode." });
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const filePath = path.resolve(rawFilePath);
|
|
3187
|
+
if (ensureMode === "jsonObject") {
|
|
3188
|
+
await ensureJsonObjectFileExists(filePath, {});
|
|
3189
|
+
} else if (ensureMode === "text") {
|
|
3190
|
+
await ensureTextFileExists(filePath, "");
|
|
3191
|
+
} else {
|
|
3192
|
+
try {
|
|
3193
|
+
await fs.access(filePath);
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
3196
|
+
const missingFileError = new Error(`File does not exist yet: ${filePath}`);
|
|
3197
|
+
missingFileError.statusCode = 404;
|
|
3198
|
+
throw missingFileError;
|
|
3199
|
+
}
|
|
3200
|
+
throw error;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
await openFileInEditorFn(editorId, filePath);
|
|
3205
|
+
addLog("info", `Opened file in ${editorId}.`, filePath);
|
|
3206
|
+
sendJson(res, 200, {
|
|
3207
|
+
ok: true,
|
|
3208
|
+
editorId,
|
|
3209
|
+
filePath,
|
|
3210
|
+
ensureMode
|
|
3211
|
+
});
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
2834
3215
|
if (method === "POST" && requestUrl.pathname === "/api/amp/config/open") {
|
|
2835
3216
|
const body = await readJsonBody(req);
|
|
2836
3217
|
const editorId = String(body?.editorId || "default").trim() || "default";
|
|
@@ -2952,7 +3333,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2952
3333
|
const externalRuntime = await readExternalRuntime();
|
|
2953
3334
|
if (externalRuntime) {
|
|
2954
3335
|
await stopExternalRuntime(externalRuntime, {
|
|
2955
|
-
reason: "Stopped another
|
|
3336
|
+
reason: "Stopped another LLM Router instance before enabling startup."
|
|
2956
3337
|
});
|
|
2957
3338
|
}
|
|
2958
3339
|
|
|
@@ -3134,6 +3515,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3134
3515
|
addLog("info", `Web console listening on http://${formatHostForUrl(host, actualWebPort)}`);
|
|
3135
3516
|
if (devMode) addLog("info", "Development mode enabled for web assets.");
|
|
3136
3517
|
startConfigWatcher();
|
|
3518
|
+
startActivityLogWatcher();
|
|
3137
3519
|
|
|
3138
3520
|
try {
|
|
3139
3521
|
await reconcileManagedRouterWithConfig({ reason: "web-console-startup" });
|