@khanglvm/llm-router 2.3.6 → 2.4.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 +19 -0
- package/README.md +19 -0
- package/package.json +1 -1
- package/src/cli/router-module.js +1 -1
- package/src/cli-entry.js +17 -2
- package/src/node/coding-tool-config.js +179 -25
- package/src/node/config-store.js +6 -1
- package/src/node/instance-state.js +10 -3
- package/src/node/local-server.js +40 -0
- package/src/node/port-reclaim.js +4 -1
- package/src/node/router-supervisor.js +543 -0
- package/src/node/start-command.js +392 -61
- package/src/node/upgrade-command.js +90 -62
- package/src/node/web-console-client.js +1 -1
- package/src/node/web-console-server.js +247 -30
- package/src/node/web-console-ui/web-search-utils.js +42 -0
- package/src/runtime/config.js +53 -0
- package/src/runtime/handler/amp-web-search.js +75 -12
- package/src/runtime/handler/provider-call.js +2 -0
- package/src/shared/coding-tool-bindings.js +28 -7
- package/src/shared/local-router-defaults.js +15 -2
- package/src/shared/timeout-signal.js +6 -7
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
areLocalServerSettingsEqual,
|
|
13
13
|
formatStartupDetail,
|
|
14
14
|
formatStartupLabel,
|
|
15
|
-
getFixedLocalRouterOrigin,
|
|
16
15
|
readLocalServerSettings
|
|
17
16
|
} from "./local-server-settings.js";
|
|
18
17
|
import { appendActivityLogEntry, clearActivityLogFile, createActivityLogEntry, readActivityLogEntries, resolveActivityLogPath } from "./activity-log.js";
|
|
@@ -69,6 +68,7 @@ import {
|
|
|
69
68
|
OLLAMA_KEEP_ALIVE_PATTERN,
|
|
70
69
|
OLLAMA_PROVIDER_TYPE,
|
|
71
70
|
configHasProvider,
|
|
71
|
+
normalizeClaudeCodeWebSearchProvider,
|
|
72
72
|
normalizeOllamaConfig,
|
|
73
73
|
normalizeRuntimeConfig,
|
|
74
74
|
resolveProviderApiKey,
|
|
@@ -444,8 +444,17 @@ function formatHostForUrl(host, port) {
|
|
|
444
444
|
return `[${value}]:${port}`;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
function buildManagedRouterOrigin(settings = {}) {
|
|
448
|
+
const port = Number.isInteger(Number(settings?.port)) ? Number(settings.port) : FIXED_LOCAL_ROUTER_PORT;
|
|
449
|
+
const configuredHost = normalizeRuntimeHost(settings?.host || FIXED_LOCAL_ROUTER_HOST);
|
|
450
|
+
const host = isWildcardRuntimeHost(configuredHost) || isLoopbackRuntimeHost(configuredHost)
|
|
451
|
+
? FIXED_LOCAL_ROUTER_HOST
|
|
452
|
+
: configuredHost;
|
|
453
|
+
return `http://${formatHostForUrl(host, port)}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
447
456
|
function buildAmpClientEndpointUrl(settings = {}) {
|
|
448
|
-
return
|
|
457
|
+
return buildManagedRouterOrigin(settings);
|
|
449
458
|
}
|
|
450
459
|
|
|
451
460
|
function buildCodexCliEndpointUrl(settings = {}) {
|
|
@@ -465,7 +474,7 @@ function buildFactoryDroidEndpointUrl(settings = {}) {
|
|
|
465
474
|
|
|
466
475
|
function buildRouterEndpoints({ host, port, running }) {
|
|
467
476
|
if (!running) return [];
|
|
468
|
-
const origin =
|
|
477
|
+
const origin = buildManagedRouterOrigin({ host, port });
|
|
469
478
|
return [
|
|
470
479
|
{ label: "Unified", url: `${origin}/route` },
|
|
471
480
|
{ label: "Anthropic", url: `${origin}/anthropic` },
|
|
@@ -805,8 +814,10 @@ function writeJsonLine(res, payload) {
|
|
|
805
814
|
|
|
806
815
|
function resolveRouterOptions(current, body) {
|
|
807
816
|
return {
|
|
808
|
-
host: FIXED_LOCAL_ROUTER_HOST,
|
|
809
|
-
port:
|
|
817
|
+
host: normalizeRuntimeHost(body?.host || current?.host || FIXED_LOCAL_ROUTER_HOST),
|
|
818
|
+
port: Number.isInteger(Number(body?.port))
|
|
819
|
+
? Number(body.port)
|
|
820
|
+
: (Number.isInteger(Number(current?.port)) ? Number(current.port) : FIXED_LOCAL_ROUTER_PORT),
|
|
810
821
|
watchConfig: body?.watchConfig === undefined ? current.watchConfig : body.watchConfig === true,
|
|
811
822
|
requireAuth: body?.requireAuth === undefined ? current.requireAuth : body.requireAuth === true,
|
|
812
823
|
watchBinary: body?.watchBinary === undefined ? current.watchBinary : body.watchBinary === true
|
|
@@ -815,8 +826,8 @@ function resolveRouterOptions(current, body) {
|
|
|
815
826
|
|
|
816
827
|
function getRouterStateSettings(routerState) {
|
|
817
828
|
return {
|
|
818
|
-
host: FIXED_LOCAL_ROUTER_HOST,
|
|
819
|
-
port: FIXED_LOCAL_ROUTER_PORT,
|
|
829
|
+
host: normalizeRuntimeHost(routerState?.host || FIXED_LOCAL_ROUTER_HOST),
|
|
830
|
+
port: Number.isInteger(Number(routerState?.port)) ? Number(routerState.port) : FIXED_LOCAL_ROUTER_PORT,
|
|
820
831
|
watchConfig: routerState?.watchConfig !== false,
|
|
821
832
|
watchBinary: routerState?.watchBinary !== false,
|
|
822
833
|
requireAuth: routerState?.requireAuth === true
|
|
@@ -840,6 +851,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
840
851
|
host = "127.0.0.1",
|
|
841
852
|
port = 8788,
|
|
842
853
|
configPath = getDefaultConfigPath(),
|
|
854
|
+
productionConfigPath = getDefaultConfigPath(),
|
|
843
855
|
activityLogPath = "",
|
|
844
856
|
routerHost = FIXED_LOCAL_ROUTER_HOST,
|
|
845
857
|
routerPort = FIXED_LOCAL_ROUTER_PORT,
|
|
@@ -881,7 +893,17 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
881
893
|
? deps.loadWebConsoleDevAssets
|
|
882
894
|
: loadWebConsoleDevAssets;
|
|
883
895
|
const resolvedRouterCliPath = String(cliPathForRouter || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim();
|
|
896
|
+
const resolvedConfigPath = path.resolve(String(configPath || getDefaultConfigPath()).trim() || getDefaultConfigPath());
|
|
897
|
+
const resolvedProductionConfigPath = path.resolve(String(productionConfigPath || getDefaultConfigPath()).trim() || getDefaultConfigPath());
|
|
884
898
|
const resolvedActivityLogPath = resolveActivityLogPath(configPath, activityLogPath);
|
|
899
|
+
const startupControlsEnabled = !devMode;
|
|
900
|
+
const defaultRouterSettings = {
|
|
901
|
+
host: normalizeRuntimeHost(routerHost || FIXED_LOCAL_ROUTER_HOST),
|
|
902
|
+
port: Number.isInteger(Number(routerPort)) ? Number(routerPort) : FIXED_LOCAL_ROUTER_PORT,
|
|
903
|
+
watchConfig: routerWatchConfig !== false,
|
|
904
|
+
watchBinary: routerWatchBinary !== false,
|
|
905
|
+
requireAuth: routerRequireAuth === true
|
|
906
|
+
};
|
|
885
907
|
|
|
886
908
|
async function readWebSearchState(config = null) {
|
|
887
909
|
if (!config || typeof config !== "object") return null;
|
|
@@ -1308,6 +1330,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1308
1330
|
async function readClaudeCodeGlobalRoutingState(settings = {}, config = null) {
|
|
1309
1331
|
const endpointUrl = buildAmpClientEndpointUrl(settings);
|
|
1310
1332
|
const apiKey = String(config?.masterKey || "").trim();
|
|
1333
|
+
const webSearchProvider = normalizeClaudeCodeWebSearchProvider(config?.claudeCode?.webSearchProvider);
|
|
1311
1334
|
try {
|
|
1312
1335
|
const state = await readClaudeCodeRoutingState({
|
|
1313
1336
|
endpointUrl,
|
|
@@ -1316,6 +1339,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1316
1339
|
});
|
|
1317
1340
|
return {
|
|
1318
1341
|
...state,
|
|
1342
|
+
webSearchProvider,
|
|
1319
1343
|
endpointUrl,
|
|
1320
1344
|
error: ""
|
|
1321
1345
|
};
|
|
@@ -1336,6 +1360,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1336
1360
|
subagentModel: "",
|
|
1337
1361
|
thinkingLevel: ""
|
|
1338
1362
|
},
|
|
1363
|
+
webSearchProvider,
|
|
1339
1364
|
endpointUrl,
|
|
1340
1365
|
error: error instanceof Error ? error.message : String(error)
|
|
1341
1366
|
};
|
|
@@ -1718,11 +1743,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1718
1743
|
let activityLogEnabled = true;
|
|
1719
1744
|
|
|
1720
1745
|
const routerState = {
|
|
1721
|
-
host:
|
|
1722
|
-
port:
|
|
1723
|
-
watchConfig:
|
|
1724
|
-
watchBinary:
|
|
1725
|
-
requireAuth:
|
|
1746
|
+
host: defaultRouterSettings.host,
|
|
1747
|
+
port: defaultRouterSettings.port,
|
|
1748
|
+
watchConfig: defaultRouterSettings.watchConfig,
|
|
1749
|
+
watchBinary: defaultRouterSettings.watchBinary,
|
|
1750
|
+
requireAuth: defaultRouterSettings.requireAuth,
|
|
1726
1751
|
lastError: ""
|
|
1727
1752
|
};
|
|
1728
1753
|
|
|
@@ -1987,6 +2012,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1987
2012
|
}
|
|
1988
2013
|
|
|
1989
2014
|
async function stopUntrackedStartupRuntime({ reason = "Stopped startup-managed LLM Router." } = {}) {
|
|
2015
|
+
if (!startupControlsEnabled) return false;
|
|
1990
2016
|
const startup = await startupStatusFn().catch(() => null);
|
|
1991
2017
|
if (!startup?.running) return false;
|
|
1992
2018
|
await stopStartupFn();
|
|
@@ -1996,6 +2022,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1996
2022
|
}
|
|
1997
2023
|
|
|
1998
2024
|
async function startStartupOwnedRouter(settings, { restart = false } = {}) {
|
|
2025
|
+
if (!startupControlsEnabled) {
|
|
2026
|
+
const error = new Error("Startup service controls are disabled in dev mode.");
|
|
2027
|
+
error.statusCode = 409;
|
|
2028
|
+
throw error;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1999
2031
|
await clearRuntimeStateFn();
|
|
2000
2032
|
const detail = await installStartupFn({
|
|
2001
2033
|
configPath,
|
|
@@ -2058,7 +2090,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2058
2090
|
};
|
|
2059
2091
|
}
|
|
2060
2092
|
|
|
2061
|
-
const startup = await startupStatusFn().catch(() => null);
|
|
2093
|
+
const startup = startupControlsEnabled ? await startupStatusFn().catch(() => null) : null;
|
|
2062
2094
|
const activeRuntime = await readManagedRuntime(configLocalServer);
|
|
2063
2095
|
if (activeRuntime) {
|
|
2064
2096
|
return {
|
|
@@ -2075,7 +2107,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2075
2107
|
await stopExternalRuntime(externalRuntime, {
|
|
2076
2108
|
reason: `Stopped an existing LLM Router instance so the web console can manage ${configLocalServer.host}:${configLocalServer.port} during ${reason}.`
|
|
2077
2109
|
});
|
|
2078
|
-
} else {
|
|
2110
|
+
} else if (startupControlsEnabled) {
|
|
2079
2111
|
await stopUntrackedStartupRuntime({
|
|
2080
2112
|
reason: `Stopped the startup-managed LLM Router instance so the web console can manage ${configLocalServer.host}:${configLocalServer.port} during ${reason}.`
|
|
2081
2113
|
});
|
|
@@ -2156,7 +2188,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2156
2188
|
return savedConfig;
|
|
2157
2189
|
}
|
|
2158
2190
|
|
|
2159
|
-
async function writeAndBroadcastConfig(parsed, {
|
|
2191
|
+
async function writeAndBroadcastConfig(parsed, {
|
|
2192
|
+
source = "",
|
|
2193
|
+
preserveMissingKeys = true,
|
|
2194
|
+
successMessage = ""
|
|
2195
|
+
} = {}) {
|
|
2160
2196
|
const previousConfigState = await readConfigState(configPath);
|
|
2161
2197
|
const previousConfig = previousConfigState.normalizedConfig || buildDefaultConfigObject();
|
|
2162
2198
|
const previousLocalServer = getConfigLocalServer(previousConfigState);
|
|
@@ -2165,7 +2201,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2165
2201
|
// This prevents partial writes (e.g. from scoped Ollama endpoints) from wiping
|
|
2166
2202
|
// unrelated config sections like masterKey, modelAliases, amp, metadata, etc.
|
|
2167
2203
|
const previousRaw = previousConfigState.rawConfig;
|
|
2168
|
-
if (previousRaw && typeof previousRaw === "object" && parsed && typeof parsed === "object") {
|
|
2204
|
+
if (preserveMissingKeys && previousRaw && typeof previousRaw === "object" && parsed && typeof parsed === "object") {
|
|
2169
2205
|
for (const key of Object.keys(previousRaw)) {
|
|
2170
2206
|
if (!(key in parsed)) {
|
|
2171
2207
|
parsed[key] = previousRaw[key];
|
|
@@ -2179,7 +2215,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2179
2215
|
const nextLocalServer = readLocalServerSettings(savedConfig, previousLocalServer);
|
|
2180
2216
|
|
|
2181
2217
|
if (source !== "autosave") {
|
|
2182
|
-
addLog("success", `Config saved to ${path.basename(configPath)}.`);
|
|
2218
|
+
addLog("success", successMessage || `Config saved to ${path.basename(configPath)}.`);
|
|
2183
2219
|
}
|
|
2184
2220
|
|
|
2185
2221
|
const managedRuntime = await readManagedRuntime(previousLocalServer);
|
|
@@ -2244,6 +2280,54 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2244
2280
|
};
|
|
2245
2281
|
}
|
|
2246
2282
|
|
|
2283
|
+
async function syncConfigFromProduction() {
|
|
2284
|
+
if (!devMode) {
|
|
2285
|
+
const error = new Error("Production config sync is only available in dev mode.");
|
|
2286
|
+
error.statusCode = 409;
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (resolvedConfigPath === resolvedProductionConfigPath) {
|
|
2291
|
+
const error = new Error("Current config already points to the production config file.");
|
|
2292
|
+
error.statusCode = 409;
|
|
2293
|
+
throw error;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const currentConfigState = await readConfigState(configPath);
|
|
2297
|
+
const currentLocalServer = getConfigLocalServer(currentConfigState);
|
|
2298
|
+
const productionConfigState = await readConfigState(resolvedProductionConfigPath);
|
|
2299
|
+
|
|
2300
|
+
if (!productionConfigState.summary?.exists) {
|
|
2301
|
+
const error = new Error(`Production config file was not found at ${resolvedProductionConfigPath}.`);
|
|
2302
|
+
error.statusCode = 404;
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if (productionConfigState.parseError) {
|
|
2307
|
+
const error = new Error(`Production config JSON must parse before syncing: ${productionConfigState.parseError}`);
|
|
2308
|
+
error.statusCode = 400;
|
|
2309
|
+
throw error;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const clonedConfig = applyLocalServerSettings(
|
|
2313
|
+
productionConfigState.normalizedConfig || buildDefaultConfigObject(),
|
|
2314
|
+
currentLocalServer
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
const result = await writeAndBroadcastConfig(clonedConfig, {
|
|
2318
|
+
source: "production-sync",
|
|
2319
|
+
preserveMissingKeys: false,
|
|
2320
|
+
successMessage: `Synced ${path.basename(resolvedConfigPath)} from ${path.basename(resolvedProductionConfigPath)}.`
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
return {
|
|
2324
|
+
...result,
|
|
2325
|
+
message: `Synced dev config from ${resolvedProductionConfigPath}.`,
|
|
2326
|
+
syncedFrom: resolvedProductionConfigPath,
|
|
2327
|
+
syncedTo: resolvedConfigPath
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2247
2331
|
let routerRestartPromise = null;
|
|
2248
2332
|
|
|
2249
2333
|
async function restartManagedRouterWithSettings(settings, {
|
|
@@ -2270,13 +2354,21 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2270
2354
|
const configState = await readConfigState(configPath);
|
|
2271
2355
|
const configLocalServer = getConfigLocalServer(configState);
|
|
2272
2356
|
const activityLog = resolveActivityLogSnapshot(configState.normalizedConfig);
|
|
2273
|
-
const startup =
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2357
|
+
const startup = startupControlsEnabled
|
|
2358
|
+
? await startupStatusFn().catch((error) => ({
|
|
2359
|
+
manager: "unknown",
|
|
2360
|
+
serviceId: "llm-router",
|
|
2361
|
+
installed: false,
|
|
2362
|
+
running: false,
|
|
2363
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2364
|
+
}))
|
|
2365
|
+
: {
|
|
2366
|
+
manager: "disabled",
|
|
2367
|
+
serviceId: "llm-router",
|
|
2368
|
+
installed: false,
|
|
2369
|
+
running: false,
|
|
2370
|
+
detail: "Startup service controls are disabled in dev mode."
|
|
2371
|
+
};
|
|
2280
2372
|
const managedRuntime = await readManagedRuntime(configLocalServer);
|
|
2281
2373
|
const externalRuntime = managedRuntime ? null : await readExternalRuntime(configLocalServer);
|
|
2282
2374
|
const portProbe = probeRouterPort(configLocalServer);
|
|
@@ -2301,6 +2393,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2301
2393
|
: { ok: false };
|
|
2302
2394
|
|
|
2303
2395
|
return {
|
|
2396
|
+
environment: {
|
|
2397
|
+
devMode,
|
|
2398
|
+
configPath: resolvedConfigPath,
|
|
2399
|
+
productionConfigPath: resolvedProductionConfigPath,
|
|
2400
|
+
canSyncProductionConfig: devMode && resolvedConfigPath !== resolvedProductionConfigPath
|
|
2401
|
+
},
|
|
2304
2402
|
web: {
|
|
2305
2403
|
host,
|
|
2306
2404
|
port: actualWebPort,
|
|
@@ -2315,9 +2413,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2315
2413
|
router: routerSnapshot,
|
|
2316
2414
|
startup: {
|
|
2317
2415
|
...startup,
|
|
2318
|
-
label: formatStartupLabel(startup),
|
|
2319
|
-
friendlyDetail:
|
|
2320
|
-
|
|
2416
|
+
label: startupControlsEnabled ? formatStartupLabel(startup) : "Startup disabled in dev mode",
|
|
2417
|
+
friendlyDetail: startupControlsEnabled
|
|
2418
|
+
? formatStartupDetail(startup)
|
|
2419
|
+
: "Development web console will not install, stop, or replace startup-managed routers.",
|
|
2420
|
+
defaults: configLocalServer,
|
|
2421
|
+
available: startupControlsEnabled
|
|
2321
2422
|
},
|
|
2322
2423
|
ampClient: {
|
|
2323
2424
|
global: ampClientGlobal
|
|
@@ -2473,8 +2574,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2473
2574
|
nextOptions = persisted.savedSettings;
|
|
2474
2575
|
}
|
|
2475
2576
|
|
|
2476
|
-
const startup = await startupStatusFn().catch(() => null);
|
|
2477
|
-
const preferStartupOwnership = Boolean(startup?.installed);
|
|
2577
|
+
const startup = startupControlsEnabled ? await startupStatusFn().catch(() => null) : null;
|
|
2578
|
+
const preferStartupOwnership = startupControlsEnabled && Boolean(startup?.installed);
|
|
2478
2579
|
const runningRuntime = await readManagedRuntime(nextOptions);
|
|
2479
2580
|
const webConsoleConflict = getWebConsoleConflictMessage(nextOptions);
|
|
2480
2581
|
|
|
@@ -2501,7 +2602,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2501
2602
|
await stopExternalRuntime(externalRuntime, {
|
|
2502
2603
|
reason: "Stopped another LLM Router instance before starting the managed router."
|
|
2503
2604
|
});
|
|
2504
|
-
} else {
|
|
2605
|
+
} else if (startupControlsEnabled) {
|
|
2505
2606
|
await stopUntrackedStartupRuntime({
|
|
2506
2607
|
reason: "Stopped the startup-managed LLM Router instance before starting the managed router."
|
|
2507
2608
|
});
|
|
@@ -2791,6 +2892,17 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2791
2892
|
return;
|
|
2792
2893
|
}
|
|
2793
2894
|
|
|
2895
|
+
if (method === "POST" && requestUrl.pathname === "/api/config/sync-production") {
|
|
2896
|
+
const synced = await syncConfigFromProduction();
|
|
2897
|
+
sendJson(res, 200, {
|
|
2898
|
+
...synced.snapshot,
|
|
2899
|
+
message: synced.message,
|
|
2900
|
+
syncedFrom: synced.syncedFrom,
|
|
2901
|
+
syncedTo: synced.syncedTo
|
|
2902
|
+
});
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2794
2906
|
if (method === "POST" && requestUrl.pathname === "/api/config/test-provider") {
|
|
2795
2907
|
const body = await readJsonBody(req);
|
|
2796
2908
|
const endpoints = Array.isArray(body?.endpoints) ? body.endpoints.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
|
|
@@ -3418,6 +3530,102 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3418
3530
|
return;
|
|
3419
3531
|
}
|
|
3420
3532
|
|
|
3533
|
+
if (method === "POST" && requestUrl.pathname === "/api/claude-code/search-provider") {
|
|
3534
|
+
const body = await readJsonBody(req);
|
|
3535
|
+
const currentConfigState = await readConfigState(configPath);
|
|
3536
|
+
|
|
3537
|
+
let parsed;
|
|
3538
|
+
try {
|
|
3539
|
+
if (body?.config && typeof body.config === "object" && !Array.isArray(body.config)) {
|
|
3540
|
+
parsed = body.config;
|
|
3541
|
+
} else if (typeof body?.rawText === "string") {
|
|
3542
|
+
const rawText = String(body.rawText || "");
|
|
3543
|
+
parsed = rawText.trim() ? JSON.parse(rawText) : {};
|
|
3544
|
+
} else {
|
|
3545
|
+
parsed = currentConfigState.rawConfig && typeof currentConfigState.rawConfig === "object" && !Array.isArray(currentConfigState.rawConfig)
|
|
3546
|
+
? { ...currentConfigState.rawConfig }
|
|
3547
|
+
: {};
|
|
3548
|
+
}
|
|
3549
|
+
} catch (error) {
|
|
3550
|
+
sendJson(res, 400, {
|
|
3551
|
+
error: `Config JSON parse failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3552
|
+
});
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const nextConfig = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3557
|
+
const selectedProvider = normalizeClaudeCodeWebSearchProvider(
|
|
3558
|
+
body?.webSearchProvider
|
|
3559
|
+
?? body?.searchProvider
|
|
3560
|
+
?? body?.providerId
|
|
3561
|
+
);
|
|
3562
|
+
|
|
3563
|
+
if (selectedProvider) {
|
|
3564
|
+
let normalizedConfig;
|
|
3565
|
+
try {
|
|
3566
|
+
normalizedConfig = normalizeRuntimeConfig(nextConfig, { migrateToVersion: CONFIG_VERSION });
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
sendJson(res, 400, {
|
|
3569
|
+
error: `Config normalization failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3570
|
+
});
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const configuredProviders = Array.isArray(normalizedConfig?.webSearch?.providers) ? normalizedConfig.webSearch.providers : [];
|
|
3574
|
+
const selectedExists = configuredProviders.some((provider) => normalizeClaudeCodeWebSearchProvider(provider?.id) === selectedProvider);
|
|
3575
|
+
if (!selectedExists) {
|
|
3576
|
+
sendJson(res, 400, {
|
|
3577
|
+
error: `Claude Code web search provider '${selectedProvider}' must reference a configured webSearch provider.`
|
|
3578
|
+
});
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
if (selectedProvider) {
|
|
3584
|
+
const currentClaudeCode = nextConfig?.claudeCode && typeof nextConfig.claudeCode === "object" && !Array.isArray(nextConfig.claudeCode)
|
|
3585
|
+
? nextConfig.claudeCode
|
|
3586
|
+
: (nextConfig?.["claude-code"] && typeof nextConfig["claude-code"] === "object" && !Array.isArray(nextConfig["claude-code"])
|
|
3587
|
+
? nextConfig["claude-code"]
|
|
3588
|
+
: null);
|
|
3589
|
+
const nextClaudeCode = currentClaudeCode
|
|
3590
|
+
? { ...currentClaudeCode }
|
|
3591
|
+
: {};
|
|
3592
|
+
nextClaudeCode.webSearchProvider = selectedProvider;
|
|
3593
|
+
nextConfig.claudeCode = nextClaudeCode;
|
|
3594
|
+
delete nextConfig["claude-code"];
|
|
3595
|
+
} else {
|
|
3596
|
+
const currentClaudeCode = nextConfig?.claudeCode && typeof nextConfig.claudeCode === "object" && !Array.isArray(nextConfig.claudeCode)
|
|
3597
|
+
? nextConfig.claudeCode
|
|
3598
|
+
: (nextConfig?.["claude-code"] && typeof nextConfig["claude-code"] === "object" && !Array.isArray(nextConfig["claude-code"])
|
|
3599
|
+
? nextConfig["claude-code"]
|
|
3600
|
+
: null);
|
|
3601
|
+
if (currentClaudeCode) {
|
|
3602
|
+
const nextClaudeCode = { ...currentClaudeCode };
|
|
3603
|
+
delete nextClaudeCode.webSearchProvider;
|
|
3604
|
+
delete nextClaudeCode["web-search-provider"];
|
|
3605
|
+
delete nextClaudeCode.searchProvider;
|
|
3606
|
+
delete nextClaudeCode["search-provider"];
|
|
3607
|
+
if (Object.keys(nextClaudeCode).length > 0) {
|
|
3608
|
+
nextConfig.claudeCode = nextClaudeCode;
|
|
3609
|
+
} else {
|
|
3610
|
+
nextConfig.claudeCode = {};
|
|
3611
|
+
}
|
|
3612
|
+
delete nextConfig["claude-code"];
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const { snapshot } = await writeAndBroadcastConfig(nextConfig, { source: "claude-code-search-provider" });
|
|
3617
|
+
addLog(
|
|
3618
|
+
"success",
|
|
3619
|
+
selectedProvider ? "Claude Code search capability updated." : "Claude Code search capability cleared.",
|
|
3620
|
+
selectedProvider || "Default router search order"
|
|
3621
|
+
);
|
|
3622
|
+
sendJson(res, 200, {
|
|
3623
|
+
...snapshot,
|
|
3624
|
+
message: selectedProvider ? "Claude Code search capability updated." : "Claude Code search capability cleared."
|
|
3625
|
+
});
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3421
3629
|
if (method === "POST" && requestUrl.pathname === "/api/claude-code/effort-level") {
|
|
3422
3630
|
const body = await readJsonBody(req);
|
|
3423
3631
|
const effortLevel = String(body?.effortLevel || body?.thinkingLevel || "").trim();
|
|
@@ -3733,6 +3941,10 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3733
3941
|
}
|
|
3734
3942
|
|
|
3735
3943
|
if (method === "POST" && requestUrl.pathname === "/api/startup/enable") {
|
|
3944
|
+
if (!startupControlsEnabled) {
|
|
3945
|
+
sendJson(res, 409, { error: "Startup service controls are unavailable in dev mode." });
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3736
3948
|
const body = await readJsonBody(req);
|
|
3737
3949
|
const configState = await readConfigState(configPath);
|
|
3738
3950
|
if (configState.parseError) {
|
|
@@ -3795,6 +4007,10 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3795
4007
|
}
|
|
3796
4008
|
|
|
3797
4009
|
if (method === "POST" && requestUrl.pathname === "/api/startup/disable") {
|
|
4010
|
+
if (!startupControlsEnabled) {
|
|
4011
|
+
sendJson(res, 409, { error: "Startup service controls are unavailable in dev mode." });
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
3798
4014
|
await readJsonBody(req);
|
|
3799
4015
|
const statusBefore = await startupStatusFn().catch(() => null);
|
|
3800
4016
|
if (!statusBefore?.installed) {
|
|
@@ -4245,6 +4461,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
4245
4461
|
|
|
4246
4462
|
addLog("info", `Web console listening on http://${formatHostForUrl(host, actualWebPort)}`);
|
|
4247
4463
|
if (devMode) addLog("info", "Development mode enabled for web assets.");
|
|
4464
|
+
if (!startupControlsEnabled) addLog("info", "Startup service controls disabled in dev mode.");
|
|
4248
4465
|
startConfigWatcher();
|
|
4249
4466
|
startActivityLogWatcher();
|
|
4250
4467
|
|
|
@@ -306,6 +306,48 @@ export function buildWebSearchProviderRows(config = {}, snapshot = null) {
|
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
export function buildClaudeCodeWebSearchProviderOptions(providerRows = [], selectedProviderId = "") {
|
|
310
|
+
const options = [];
|
|
311
|
+
const seen = new Set();
|
|
312
|
+
|
|
313
|
+
for (const row of Array.isArray(providerRows) ? providerRows : []) {
|
|
314
|
+
if (!row?.configured) continue;
|
|
315
|
+
const optionValue = String(row?.id || "").trim();
|
|
316
|
+
const optionKey = normalizeWebSearchProviderKey(optionValue);
|
|
317
|
+
if (!optionValue || !optionKey || seen.has(optionKey)) continue;
|
|
318
|
+
|
|
319
|
+
const label = row.kind === "hosted"
|
|
320
|
+
? [String(row?.label || "").trim(), String(row?.modelId || "").trim()].filter(Boolean).join(" · ")
|
|
321
|
+
: (String(row?.label || optionValue).trim() || optionValue);
|
|
322
|
+
const hint = row.kind === "hosted"
|
|
323
|
+
? (String(row?.routeId || optionValue).trim() || "Hosted web search route")
|
|
324
|
+
: (row.active === false ? "Configured but not ready" : "Configured provider");
|
|
325
|
+
|
|
326
|
+
options.push({
|
|
327
|
+
value: optionValue,
|
|
328
|
+
label,
|
|
329
|
+
hint,
|
|
330
|
+
groupKey: row.kind === "hosted" ? "hosted-web-search-routes" : "builtin-web-search-providers",
|
|
331
|
+
groupLabel: row.kind === "hosted" ? "Hosted search routes" : "Built-in search providers"
|
|
332
|
+
});
|
|
333
|
+
seen.add(optionKey);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const selectedValue = String(selectedProviderId || "").trim();
|
|
337
|
+
const selectedKey = normalizeWebSearchProviderKey(selectedValue);
|
|
338
|
+
if (selectedValue && selectedKey && !seen.has(selectedKey)) {
|
|
339
|
+
options.unshift({
|
|
340
|
+
value: selectedValue,
|
|
341
|
+
label: selectedValue,
|
|
342
|
+
hint: "Current config (not configured)",
|
|
343
|
+
groupKey: "current-claude-selection",
|
|
344
|
+
groupLabel: "Current selection"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return options;
|
|
349
|
+
}
|
|
350
|
+
|
|
309
351
|
export function updateWebSearchConfig(config = {}, updates = {}) {
|
|
310
352
|
const next = ensureAmpDraftConfigShape(config);
|
|
311
353
|
const webSearch = ensureWebSearchConfigShape(next);
|
package/src/runtime/config.js
CHANGED
|
@@ -559,6 +559,12 @@ function normalizeAmpWebSearchProviderId(value) {
|
|
|
559
559
|
return AMP_WEB_SEARCH_PROVIDER_IDS.includes(normalized) ? normalized : "";
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
export function normalizeClaudeCodeWebSearchProvider(value) {
|
|
563
|
+
const normalized = String(value || "").trim();
|
|
564
|
+
if (!normalized) return "";
|
|
565
|
+
return normalizeAmpWebSearchProviderId(normalized) || normalized;
|
|
566
|
+
}
|
|
567
|
+
|
|
562
568
|
function normalizeAmpWebSearchStrategy(value) {
|
|
563
569
|
const normalized = String(value || "").trim().toLowerCase();
|
|
564
570
|
if (!normalized) return "ordered";
|
|
@@ -750,6 +756,32 @@ function normalizeAmpWebSearchConfig(rawWebSearch) {
|
|
|
750
756
|
};
|
|
751
757
|
}
|
|
752
758
|
|
|
759
|
+
function normalizeClaudeCodeConfig(rawClaudeCode) {
|
|
760
|
+
if (!rawClaudeCode || typeof rawClaudeCode !== "object" || Array.isArray(rawClaudeCode)) {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const source = rawClaudeCode;
|
|
765
|
+
const hasWebSearchProvider = hasOwn(source, "webSearchProvider")
|
|
766
|
+
|| hasOwn(source, "web-search-provider")
|
|
767
|
+
|| hasOwn(source, "searchProvider")
|
|
768
|
+
|| hasOwn(source, "search-provider");
|
|
769
|
+
const webSearchProvider = normalizeClaudeCodeWebSearchProvider(
|
|
770
|
+
source.webSearchProvider
|
|
771
|
+
?? source["web-search-provider"]
|
|
772
|
+
?? source.searchProvider
|
|
773
|
+
?? source["search-provider"]
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
if (!hasWebSearchProvider) {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return webSearchProvider
|
|
781
|
+
? { webSearchProvider }
|
|
782
|
+
: {};
|
|
783
|
+
}
|
|
784
|
+
|
|
753
785
|
function supportsOpenAIHostedWebSearchRoute(provider, model) {
|
|
754
786
|
const providerFormats = dedupeStrings([...(provider?.formats || []), provider?.format]);
|
|
755
787
|
if (!providerFormats.includes(FORMATS.OPENAI)) return false;
|
|
@@ -1759,11 +1791,13 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1759
1791
|
? rawAmp
|
|
1760
1792
|
: {};
|
|
1761
1793
|
const rawWebSearch = raw.webSearch ?? raw["web-search"];
|
|
1794
|
+
const rawClaudeCode = raw.claudeCode ?? raw["claude-code"];
|
|
1762
1795
|
const amp = normalizeAmpConfig(rawAmp);
|
|
1763
1796
|
const webSearch = normalizeAmpWebSearchConfig(rawWebSearch)
|
|
1764
1797
|
?? (amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1765
1798
|
? amp.webSearch
|
|
1766
1799
|
: undefined);
|
|
1800
|
+
const claudeCode = normalizeClaudeCodeConfig(rawClaudeCode);
|
|
1767
1801
|
const normalizedAmp = webSearch && amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1768
1802
|
? Object.fromEntries(
|
|
1769
1803
|
Object.entries(amp).filter(([key]) => key !== "webSearch")
|
|
@@ -1780,6 +1814,7 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1780
1814
|
modelAliases,
|
|
1781
1815
|
amp: normalizedAmp,
|
|
1782
1816
|
...(webSearch ? { webSearch } : {}),
|
|
1817
|
+
...(claudeCode && Object.keys(claudeCode).length > 0 ? { claudeCode } : {}),
|
|
1783
1818
|
ollama,
|
|
1784
1819
|
metadata: sanitizeRuntimeMetadata(raw.metadata)
|
|
1785
1820
|
};
|
|
@@ -2085,6 +2120,21 @@ function validateAmpConfig(config, routingIndex, errors) {
|
|
|
2085
2120
|
}
|
|
2086
2121
|
}
|
|
2087
2122
|
}
|
|
2123
|
+
|
|
2124
|
+
const claudeCode = config?.claudeCode;
|
|
2125
|
+
if (claudeCode && typeof claudeCode === "object" && !Array.isArray(claudeCode)) {
|
|
2126
|
+
const selectedWebSearchProvider = normalizeClaudeCodeWebSearchProvider(claudeCode.webSearchProvider);
|
|
2127
|
+
if (selectedWebSearchProvider) {
|
|
2128
|
+
const configuredProviders = Array.isArray(webSearch?.providers) ? webSearch.providers : [];
|
|
2129
|
+
const selectedExists = configuredProviders.some((provider) => {
|
|
2130
|
+
const providerId = normalizeClaudeCodeWebSearchProvider(provider?.id);
|
|
2131
|
+
return providerId && providerId === selectedWebSearchProvider;
|
|
2132
|
+
});
|
|
2133
|
+
if (!selectedExists) {
|
|
2134
|
+
errors.push(`claudeCode.webSearchProvider '${selectedWebSearchProvider}' must reference a configured webSearch provider.`);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2088
2138
|
}
|
|
2089
2139
|
|
|
2090
2140
|
export function validateRuntimeConfig(config, { requireMasterKey = false, requireProvider = false } = {}) {
|
|
@@ -2325,6 +2375,9 @@ export function sanitizeConfigForDisplay(config) {
|
|
|
2325
2375
|
...config,
|
|
2326
2376
|
masterKey: config.masterKey ? maskSecret(config.masterKey) : undefined,
|
|
2327
2377
|
...(sanitizedWebSearch ? { webSearch: sanitizedWebSearch } : {}),
|
|
2378
|
+
...(config.claudeCode && typeof config.claudeCode === "object" && !Array.isArray(config.claudeCode)
|
|
2379
|
+
? { claudeCode: { ...config.claudeCode } }
|
|
2380
|
+
: {}),
|
|
2328
2381
|
amp: sanitizedAmp,
|
|
2329
2382
|
providers: (config.providers || []).map((provider) => ({
|
|
2330
2383
|
...provider,
|