@khanglvm/llm-router 2.3.7 → 2.4.1
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 +17 -0
- package/README.md +12 -0
- package/package.json +2 -1
- package/src/node/instance-state.js +6 -2
- package/src/node/port-reclaim.js +4 -1
- package/src/node/web-console-client.js +26 -26
- package/src/node/web-console-server.js +175 -3
- package/src/node/web-console-styles.generated.js +1 -1
- 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
|
@@ -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,
|
|
@@ -5,6 +5,7 @@ import { buildCandidateKey } from "../state-store.js";
|
|
|
5
5
|
import { consumeCandidateRateLimits, resolveWindowRange } from "../rate-limits.js";
|
|
6
6
|
import {
|
|
7
7
|
buildProviderHeaders,
|
|
8
|
+
normalizeClaudeCodeWebSearchProvider,
|
|
8
9
|
resolveProviderFormat,
|
|
9
10
|
resolveProviderUrl,
|
|
10
11
|
resolveRouteReference
|
|
@@ -311,16 +312,29 @@ function hasSearchToolType(type) {
|
|
|
311
312
|
if (!normalized) return false;
|
|
312
313
|
return normalized === SEARCH_TOOL_NAME
|
|
313
314
|
|| normalized.startsWith("web_search_preview")
|
|
314
|
-
|| normalized
|
|
315
|
+
|| normalized.startsWith("web_search_");
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
function hasSearchToolName(name) {
|
|
318
|
-
const normalized = String(name || "").trim().toLowerCase();
|
|
319
|
-
return normalized === SEARCH_TOOL_NAME
|
|
319
|
+
const normalized = String(name || "").trim().toLowerCase().replace(/\s+/g, "_");
|
|
320
|
+
return normalized === SEARCH_TOOL_NAME
|
|
321
|
+
|| normalized === "web_search_preview"
|
|
322
|
+
|| normalized === "websearch";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hasReadWebPageToolType(type) {
|
|
326
|
+
const normalized = String(type || "").trim().toLowerCase();
|
|
327
|
+
if (!normalized) return false;
|
|
328
|
+
return normalized === READ_WEB_PAGE_TOOL_NAME
|
|
329
|
+
|| normalized === "web_fetch"
|
|
330
|
+
|| normalized.startsWith("web_fetch_");
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
function hasReadWebPageToolName(name) {
|
|
323
|
-
|
|
334
|
+
const normalized = String(name || "").trim().toLowerCase().replace(/\s+/g, "_");
|
|
335
|
+
return normalized === READ_WEB_PAGE_TOOL_NAME
|
|
336
|
+
|| normalized === "web_fetch"
|
|
337
|
+
|| normalized === "webfetch";
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
function hasInterceptableTool(tool) {
|
|
@@ -338,7 +352,7 @@ function hasInterceptableToolName(name) {
|
|
|
338
352
|
|
|
339
353
|
function getToolName(tool) {
|
|
340
354
|
if (!tool || typeof tool !== "object") return "";
|
|
341
|
-
if (hasReadWebPageToolName(tool.name) || hasReadWebPageToolName(tool.function?.name)) {
|
|
355
|
+
if (hasReadWebPageToolType(tool.type) || hasReadWebPageToolName(tool.name) || hasReadWebPageToolName(tool.function?.name)) {
|
|
342
356
|
return READ_WEB_PAGE_TOOL_NAME;
|
|
343
357
|
}
|
|
344
358
|
if (hasSearchToolType(tool.type) || hasSearchToolName(tool.name) || hasSearchToolName(tool.function?.name)) {
|
|
@@ -405,6 +419,28 @@ function normalizeSearchProviderId(value) {
|
|
|
405
419
|
return AMP_WEB_SEARCH_PROVIDER_META.has(normalized) ? normalized : "";
|
|
406
420
|
}
|
|
407
421
|
|
|
422
|
+
function normalizeSearchProviderSelection(value) {
|
|
423
|
+
return normalizeClaudeCodeWebSearchProvider(value) || "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function hasNativeClaudeWebSearchTool(tool) {
|
|
427
|
+
const normalizedType = String(tool?.type || "").trim().toLowerCase();
|
|
428
|
+
return normalizedType === "web_search"
|
|
429
|
+
|| (normalizedType.startsWith("web_search_") && !normalizedType.startsWith("web_search_preview"));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function hasNativeClaudeWebFetchTool(tool) {
|
|
433
|
+
const normalizedType = String(tool?.type || "").trim().toLowerCase();
|
|
434
|
+
return normalizedType === "web_fetch" || normalizedType.startsWith("web_fetch_");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function shouldUseClaudeCodeWebSearchSelection(runtimeConfig = {}, originalBody = {}) {
|
|
438
|
+
const selectedProvider = normalizeSearchProviderSelection(runtimeConfig?.claudeCode?.webSearchProvider);
|
|
439
|
+
if (!selectedProvider) return false;
|
|
440
|
+
const tools = Array.isArray(originalBody?.tools) ? originalBody.tools : [];
|
|
441
|
+
return tools.some((tool) => hasNativeClaudeWebSearchTool(tool) || hasNativeClaudeWebFetchTool(tool));
|
|
442
|
+
}
|
|
443
|
+
|
|
408
444
|
function normalizeSearchRoutingStrategy(value) {
|
|
409
445
|
const normalized = String(value || "").trim().toLowerCase();
|
|
410
446
|
if (!normalized) return "ordered";
|
|
@@ -561,7 +597,7 @@ function normalizeConfiguredSearchProviders(rawProviders, raw = {}, env = {}) {
|
|
|
561
597
|
];
|
|
562
598
|
}
|
|
563
599
|
|
|
564
|
-
export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}) {
|
|
600
|
+
export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}, options = {}) {
|
|
565
601
|
const amp = runtimeConfig?.amp && typeof runtimeConfig.amp === "object" ? runtimeConfig.amp : {};
|
|
566
602
|
const raw = runtimeConfig?.webSearch && typeof runtimeConfig.webSearch === "object" && !Array.isArray(runtimeConfig.webSearch)
|
|
567
603
|
? runtimeConfig.webSearch
|
|
@@ -574,11 +610,17 @@ export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}) {
|
|
|
574
610
|
{ min: MIN_SEARCH_COUNT, max: MAX_SEARCH_COUNT }
|
|
575
611
|
);
|
|
576
612
|
const providers = normalizeConfiguredSearchProviders(raw.providers, raw, env);
|
|
613
|
+
const selectedProviderId = shouldUseClaudeCodeWebSearchSelection(runtimeConfig, options?.originalBody)
|
|
614
|
+
? normalizeSearchProviderSelection(runtimeConfig?.claudeCode?.webSearchProvider)
|
|
615
|
+
: "";
|
|
616
|
+
const selectedProviders = selectedProviderId
|
|
617
|
+
? providers.filter((provider) => normalizeSearchProviderSelection(provider?.id) === selectedProviderId)
|
|
618
|
+
: [];
|
|
577
619
|
|
|
578
620
|
return {
|
|
579
621
|
strategy: normalizeSearchRoutingStrategy(raw.strategy ?? env.AMP_WEB_SEARCH_STRATEGY),
|
|
580
622
|
count,
|
|
581
|
-
providers
|
|
623
|
+
providers: selectedProviders.length > 0 ? selectedProviders : providers
|
|
582
624
|
};
|
|
583
625
|
}
|
|
584
626
|
|
|
@@ -895,8 +937,8 @@ function buildSearchProviderStatus(provider, evaluation, runtimeConfig = {}) {
|
|
|
895
937
|
};
|
|
896
938
|
}
|
|
897
939
|
|
|
898
|
-
export async function buildAmpWebSearchSnapshot(runtimeConfig = {}, { env = {}, stateStore = null, now = Date.now() } = {}) {
|
|
899
|
-
const settings = resolveAmpWebSearchConfig(runtimeConfig, env);
|
|
940
|
+
export async function buildAmpWebSearchSnapshot(runtimeConfig = {}, { env = {}, stateStore = null, now = Date.now(), originalBody = null } = {}) {
|
|
941
|
+
const settings = resolveAmpWebSearchConfig(runtimeConfig, env, { originalBody });
|
|
900
942
|
const effectiveStateStore = resolveSearchStateStore(stateStore);
|
|
901
943
|
const providers = [];
|
|
902
944
|
|
|
@@ -1198,7 +1240,8 @@ export async function executeAmpWebSearch(query, runtimeConfig = {}, env = {}, o
|
|
|
1198
1240
|
const snapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1199
1241
|
env,
|
|
1200
1242
|
stateStore: options.stateStore,
|
|
1201
|
-
now: options.now
|
|
1243
|
+
now: options.now,
|
|
1244
|
+
originalBody: options.originalBody
|
|
1202
1245
|
});
|
|
1203
1246
|
const stateStore = resolveSearchStateStore(options.stateStore);
|
|
1204
1247
|
const configuredProviders = snapshot.providers.filter((provider) => provider.ready);
|
|
@@ -1227,7 +1270,8 @@ export async function executeAmpWebSearch(query, runtimeConfig = {}, env = {}, o
|
|
|
1227
1270
|
const refreshedSnapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1228
1271
|
env,
|
|
1229
1272
|
stateStore,
|
|
1230
|
-
now: options.now
|
|
1273
|
+
now: options.now,
|
|
1274
|
+
originalBody: options.originalBody
|
|
1231
1275
|
});
|
|
1232
1276
|
const refreshedProvider = refreshedSnapshot.providers.find((entry) => entry.id === providerStatus.id) || providerStatus;
|
|
1233
1277
|
return {
|
|
@@ -1271,12 +1315,29 @@ export function shouldInterceptAmpWebSearch({ clientType, originalBody, runtimeC
|
|
|
1271
1315
|
if (requestedToolNames.length === 0) {
|
|
1272
1316
|
return false;
|
|
1273
1317
|
}
|
|
1318
|
+
const requestsWebSearch = requestedToolNames.includes(SEARCH_TOOL_NAME);
|
|
1319
|
+
const requestsReadWebPage = requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME);
|
|
1274
1320
|
const readyProviders = resolveAmpWebSearchConfig(runtimeConfig, env).providers.filter((provider) => {
|
|
1275
1321
|
if (!isSearchProviderConfigured(provider)) return false;
|
|
1276
1322
|
if (!isHostedSearchProvider(provider)) return true;
|
|
1277
1323
|
const resolvedRoute = getResolvedHostedSearchRoute(runtimeConfig, provider);
|
|
1278
1324
|
return Boolean(resolvedRoute && supportsResolvedHostedSearchRoute(resolvedRoute.provider, resolvedRoute.model));
|
|
1279
1325
|
});
|
|
1326
|
+
if (
|
|
1327
|
+
clientType === "amp"
|
|
1328
|
+
&& requestsWebSearch
|
|
1329
|
+
&& !requestsReadWebPage
|
|
1330
|
+
&& readyProviders.length === 0
|
|
1331
|
+
&& runtimeConfig?.amp?.proxyWebSearchToUpstream === true
|
|
1332
|
+
&& String(runtimeConfig?.amp?.upstreamUrl || "").trim()
|
|
1333
|
+
&& String(runtimeConfig?.amp?.upstreamApiKey || "").trim()
|
|
1334
|
+
) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
const hasNativeClaudeWebTool = tools.some((tool) => hasNativeClaudeWebSearchTool(tool) || hasNativeClaudeWebFetchTool(tool));
|
|
1338
|
+
if (hasNativeClaudeWebTool) {
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1280
1341
|
if (readyProviders.length === 0) {
|
|
1281
1342
|
return clientType === "amp" && requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME);
|
|
1282
1343
|
}
|
|
@@ -2324,6 +2385,7 @@ export async function maybeInterceptAmpWebSearch({
|
|
|
2324
2385
|
runtimeConfig,
|
|
2325
2386
|
env,
|
|
2326
2387
|
stateStore,
|
|
2388
|
+
originalBody,
|
|
2327
2389
|
executeProviderRequest
|
|
2328
2390
|
} = {}) {
|
|
2329
2391
|
if (!(response instanceof Response)) {
|
|
@@ -2362,7 +2424,8 @@ export async function maybeInterceptAmpWebSearch({
|
|
|
2362
2424
|
runtimeConfig,
|
|
2363
2425
|
env,
|
|
2364
2426
|
{
|
|
2365
|
-
stateStore
|
|
2427
|
+
stateStore,
|
|
2428
|
+
originalBody
|
|
2366
2429
|
}
|
|
2367
2430
|
));
|
|
2368
2431
|
}
|
|
@@ -920,6 +920,7 @@ export async function makeProviderCall({
|
|
|
920
920
|
runtimeConfig,
|
|
921
921
|
env,
|
|
922
922
|
stateStore,
|
|
923
|
+
originalBody: body,
|
|
923
924
|
executeProviderRequest: async (followUpBody) => {
|
|
924
925
|
const followUpResult = await executeSubscriptionRequest(followUpBody);
|
|
925
926
|
return followUpResult?.response instanceof Response ? followUpResult.response : null;
|
|
@@ -1219,6 +1220,7 @@ export async function makeProviderCall({
|
|
|
1219
1220
|
runtimeConfig,
|
|
1220
1221
|
env,
|
|
1221
1222
|
stateStore,
|
|
1223
|
+
originalBody: body,
|
|
1222
1224
|
executeProviderRequest: async (followUpBody) => {
|
|
1223
1225
|
try {
|
|
1224
1226
|
return await executeHttpProviderRequest({
|