@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.
@@ -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);
@@ -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 === "web_search_20250305";
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 || normalized === "web_search_preview";
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
- return String(name || "").trim().toLowerCase() === READ_WEB_PAGE_TOOL_NAME;
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({