@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.
@@ -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({
@@ -83,10 +83,31 @@ function sanitizeFactoryDroidRouterModelIdPart(value) {
83
83
  function formatFactoryDroidDisplayNameBase(value) {
84
84
  const normalized = String(value || "").trim();
85
85
  if (!normalized) return "";
86
- if (/^gpt(?=[-\s.]|$)/i.test(normalized)) return `GPT${normalized.slice(3)}`;
87
- if (/^glm(?=[-\s.]|$)/i.test(normalized)) return `GLM${normalized.slice(3)}`;
88
- if (/^claude(?=[-\s.]|$)/i.test(normalized)) return `Claude${normalized.slice(6)}`;
89
- return normalized;
86
+ let next = normalized;
87
+ if (/^gpt(?=[-\s.]|$)/i.test(next)) next = `GPT${next.slice(3)}`;
88
+ else if (/^glm(?=[-\s.]|$)/i.test(next)) next = `GLM${next.slice(3)}`;
89
+ else if (/^claude(?=[-\s.]|$)/i.test(next)) next = `Claude${next.slice(6)}`;
90
+
91
+ return next
92
+ .replace(/(\d)-(\d)(?=(?:-|$))/g, "$1.$2")
93
+ .replace(/[_-]+/g, " ")
94
+ .replace(/\s+/g, " ")
95
+ .trim();
96
+ }
97
+
98
+ function formatFactoryDroidProviderLabel(value) {
99
+ const normalized = String(value || "").trim();
100
+ if (!normalized) return "Provider";
101
+ if (normalized.toLowerCase() === "openrouter") return "OpenRouter";
102
+ if (normalized.toLowerCase() === "deepseek") return "DeepSeek";
103
+ if (/^[A-Za-z]{2,5}$/.test(normalized)) return normalized.toUpperCase();
104
+ return normalized
105
+ .replace(/[_-]+/g, " ")
106
+ .replace(/\s+/g, " ")
107
+ .split(" ")
108
+ .filter(Boolean)
109
+ .map((part) => part[0].toUpperCase() + part.slice(1))
110
+ .join(" ");
90
111
  }
91
112
 
92
113
  export function isFactoryDroidRouterModelId(value) {
@@ -181,20 +202,20 @@ export function buildFactoryDroidRouterModelId(modelRef, { kind = "" } = {}) {
181
202
  return buildFactoryDroidRouterModelId(normalizedModelRef, { kind: "model" });
182
203
  }
183
204
 
184
- export function buildFactoryDroidRouterDisplayName(modelRef, { kind = "" } = {}) {
205
+ export function buildFactoryDroidRouterDisplayName(modelRef, { kind = "", providerName = "" } = {}) {
185
206
  const normalizedModelRef = String(modelRef || "").trim();
186
207
  if (!normalizedModelRef) return "";
187
208
 
188
209
  const explicitKind = String(kind || "").trim().toLowerCase();
189
210
  const inferredKind = explicitKind || (normalizedModelRef.includes("/") ? "model" : "alias");
190
211
  if (inferredKind === "alias") {
191
- return `[LLM Alias] ${formatFactoryDroidDisplayNameBase(normalizedModelRef)}`;
212
+ return `${formatFactoryDroidDisplayNameBase(normalizedModelRef)} - LLM Router (Alias)`;
192
213
  }
193
214
 
194
215
  const modelName = normalizedModelRef.includes("/")
195
216
  ? normalizedModelRef.slice(normalizedModelRef.indexOf("/") + 1).trim()
196
217
  : normalizedModelRef;
197
- return `[LLM] ${formatFactoryDroidDisplayNameBase(modelName)}`;
218
+ return `${formatFactoryDroidDisplayNameBase(modelName)} - LLM Router (${formatFactoryDroidProviderLabel(providerName)})`;
198
219
  }
199
220
 
200
221
  export function normalizeFactoryDroidReasoningEffort(value) {
@@ -20,16 +20,29 @@ function isPlainObject(value) {
20
20
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
21
  }
22
22
 
23
+ function normalizeHost(value, fallback = LOCAL_ROUTER_HOST) {
24
+ const text = String(value || fallback).trim();
25
+ return text || fallback;
26
+ }
27
+
28
+ function normalizePort(value, fallback = LOCAL_ROUTER_PORT) {
29
+ const parsed = Number.parseInt(String(value ?? ""), 10);
30
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return fallback;
31
+ return parsed;
32
+ }
33
+
23
34
  export function buildLocalRouterSettings(source = {}, fallback = {}) {
24
35
  const base = {
36
+ host: normalizeHost(fallback?.host, LOCAL_ROUTER_HOST),
37
+ port: normalizePort(fallback?.port, LOCAL_ROUTER_PORT),
25
38
  watchConfig: toBoolean(fallback?.watchConfig, true),
26
39
  watchBinary: toBoolean(fallback?.watchBinary, true),
27
40
  requireAuth: toBoolean(fallback?.requireAuth, false)
28
41
  };
29
42
 
30
43
  return {
31
- host: LOCAL_ROUTER_HOST,
32
- port: LOCAL_ROUTER_PORT,
44
+ host: normalizeHost(source?.host, base.host),
45
+ port: normalizePort(source?.port, base.port),
33
46
  watchConfig: toBoolean(source?.watchConfig, base.watchConfig),
34
47
  watchBinary: toBoolean(source?.watchBinary, base.watchBinary),
35
48
  requireAuth: toBoolean(source?.requireAuth, base.requireAuth)
@@ -3,14 +3,13 @@ export function buildTimeoutSignal(timeoutMs) {
3
3
  return { signal: undefined, cleanup: () => {} };
4
4
  }
5
5
 
6
- if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
7
- return {
8
- signal: AbortSignal.timeout(timeoutMs),
9
- cleanup: () => {}
10
- };
11
- }
12
-
13
6
  if (typeof AbortController === "undefined") {
7
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
8
+ return {
9
+ signal: AbortSignal.timeout(timeoutMs),
10
+ cleanup: () => {}
11
+ };
12
+ }
14
13
  return { signal: undefined, cleanup: () => {} };
15
14
  }
16
15