@juspay/neurolink 11.26.0 → 11.26.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.
@@ -11,6 +11,8 @@ import { API_KEY_FORMATS } from "../utils/providerConfig.js";
11
11
  export const PROVIDER_DESCRIPTORS = [
12
12
  {
13
13
  name: AIProviderName.BEDROCK,
14
+ defaultHealthSweepPriority: 5,
15
+ autoSelectPreference: 7,
14
16
  aliases: ["aws"],
15
17
  credentialsKey: "bedrock",
16
18
  envVars: {
@@ -32,6 +34,8 @@ export const PROVIDER_DESCRIPTORS = [
32
34
  },
33
35
  {
34
36
  name: AIProviderName.OPENAI,
37
+ defaultHealthSweepPriority: 4,
38
+ autoSelectPreference: 3,
35
39
  aliases: ["gpt", "chatgpt"],
36
40
  credentialsKey: "openai",
37
41
  envVars: { apiKey: "OPENAI_API_KEY", baseURL: "OPENAI_BASE_URL" },
@@ -75,6 +79,8 @@ export const PROVIDER_DESCRIPTORS = [
75
79
  },
76
80
  {
77
81
  name: AIProviderName.VERTEX,
82
+ defaultHealthSweepPriority: 1,
83
+ autoSelectPreference: 5,
78
84
  aliases: ["googleVertex"],
79
85
  credentialsKey: "vertex",
80
86
  envVars: {
@@ -115,6 +121,8 @@ export const PROVIDER_DESCRIPTORS = [
115
121
  },
116
122
  {
117
123
  name: AIProviderName.ANTHROPIC,
124
+ defaultHealthSweepPriority: 3,
125
+ autoSelectPreference: 4,
118
126
  aliases: ["claude"],
119
127
  credentialsKey: "anthropic",
120
128
  envVars: {
@@ -137,6 +145,8 @@ export const PROVIDER_DESCRIPTORS = [
137
145
  },
138
146
  {
139
147
  name: AIProviderName.AZURE,
148
+ defaultHealthSweepPriority: 6,
149
+ autoSelectPreference: 8,
140
150
  aliases: ["azureOpenai"],
141
151
  credentialsKey: "azure",
142
152
  envVars: {
@@ -160,6 +170,8 @@ export const PROVIDER_DESCRIPTORS = [
160
170
  },
161
171
  {
162
172
  name: AIProviderName.GOOGLE_AI,
173
+ defaultHealthSweepPriority: 2,
174
+ autoSelectPreference: 6,
163
175
  aliases: ["googleAiStudio", "google", "gemini", "google-ai-studio"],
164
176
  credentialsKey: "googleAiStudio",
165
177
  envVars: {
@@ -202,6 +214,8 @@ export const PROVIDER_DESCRIPTORS = [
202
214
  },
203
215
  {
204
216
  name: AIProviderName.OLLAMA,
217
+ defaultHealthSweepPriority: 8,
218
+ autoSelectPreference: 2,
205
219
  aliases: ["local"],
206
220
  credentialsKey: "ollama",
207
221
  envVars: {
@@ -234,6 +248,8 @@ export const PROVIDER_DESCRIPTORS = [
234
248
  },
235
249
  {
236
250
  name: AIProviderName.LITELLM,
251
+ defaultHealthSweepPriority: 7,
252
+ autoSelectPreference: 1,
237
253
  aliases: [],
238
254
  credentialsKey: "litellm",
239
255
  envVars: {
@@ -5,6 +5,22 @@
5
5
  */
6
6
  import { MODEL_REGISTRY, MODEL_ALIASES, USE_CASE_RECOMMENDATIONS, getAllModels, getModelById, getModelsByProvider, getAvailableProviders, calculateCost, formatModelForDisplay, } from "./modelRegistry.js";
7
7
  import { isNonNullObject } from "../utils/typeUtils.js";
8
+ const MIN_FUZZY_QUERY_LENGTH = 4;
9
+ function escapeRegExp(value) {
10
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ }
12
+ /**
13
+ * True when `needle` appears in `haystack` at a real token boundary (hyphen,
14
+ * underscore, dot, slash, whitespace, or string start/end) — not merely as
15
+ * an arbitrary substring. Model ids use hyphens/dots ("gpt-4.1-mini");
16
+ * MODEL_REGISTRY .name fields use spaces ("GPT-4 Omni"), hence including
17
+ * \s in the boundary class.
18
+ */
19
+ function includesAtWordBoundary(haystack, needle) {
20
+ const boundary = "(?:^|[-_./\\s])";
21
+ const pattern = new RegExp(`${boundary}${escapeRegExp(needle)}${boundary.replace("^|", "$|")}`);
22
+ return pattern.test(haystack);
23
+ }
8
24
  /**
9
25
  * Model resolver class with advanced search and recommendation functionality
10
26
  */
@@ -23,25 +39,31 @@ export class ModelResolver {
23
39
  const resolvedId = MODEL_ALIASES[normalizedQuery];
24
40
  return MODEL_REGISTRY[resolvedId] || null;
25
41
  }
42
+ // Underspecified queries produce ambiguous, iteration-order-dependent
43
+ // matches (see Task 13 of the model metadata consolidation plan) — skip
44
+ // fuzzy matching entirely below this length.
45
+ if (normalizedQuery.length < MIN_FUZZY_QUERY_LENGTH) {
46
+ return null;
47
+ }
26
48
  // Fuzzy matching
27
49
  const allModels = getAllModels();
28
50
  // Try partial matching on ID
29
- const idMatch = allModels.find((model) => model.id.toLowerCase().includes(normalizedQuery) ||
30
- normalizedQuery.includes(model.id.toLowerCase()));
51
+ const idMatch = allModels.find((model) => includesAtWordBoundary(model.id.toLowerCase(), normalizedQuery) ||
52
+ includesAtWordBoundary(normalizedQuery, model.id.toLowerCase()));
31
53
  if (idMatch) {
32
54
  return idMatch;
33
55
  }
34
56
  // Try partial matching on name
35
- const nameMatch = allModels.find((model) => model.name.toLowerCase().includes(normalizedQuery) ||
36
- normalizedQuery.includes(model.name.toLowerCase()));
57
+ const nameMatch = allModels.find((model) => includesAtWordBoundary(model.name.toLowerCase(), normalizedQuery) ||
58
+ includesAtWordBoundary(normalizedQuery, model.name.toLowerCase()));
37
59
  if (nameMatch) {
38
60
  return nameMatch;
39
61
  }
40
62
  // Try provider-specific matching
41
63
  const providerMatch = allModels.find((model) => {
42
64
  const providerQuery = `${model.provider}-${normalizedQuery}`;
43
- return (model.id.toLowerCase().includes(providerQuery) ||
44
- model.name.toLowerCase().includes(normalizedQuery));
65
+ return (includesAtWordBoundary(model.id.toLowerCase(), providerQuery) ||
66
+ includesAtWordBoundary(model.name.toLowerCase(), normalizedQuery));
45
67
  });
46
68
  if (providerMatch) {
47
69
  return providerMatch;
@@ -160,7 +160,13 @@ export class ClassifierRouter {
160
160
  const mode = DIFFICULTY_RANK_MODE[difficulty];
161
161
  const originalIndex = new Map(members.map((m, i) => [m, i]));
162
162
  const num = (v) => (typeof v === "number" ? v : NEUTRAL);
163
- return [...members].sort((a, b) => {
163
+ const isFullyUnmeasured = (m) => {
164
+ const meta = this.metaFor(m);
165
+ return meta.cost === undefined && meta.quality === undefined;
166
+ };
167
+ const measured = members.filter((m) => !isFullyUnmeasured(m));
168
+ const unmeasured = members.filter(isFullyUnmeasured);
169
+ const sortMeasured = (pool) => [...pool].sort((a, b) => {
164
170
  const ma = this.metaFor(a);
165
171
  const mb = this.metaFor(b);
166
172
  let delta;
@@ -185,6 +191,11 @@ export class ClassifierRouter {
185
191
  // Stable: preserve declared pool order on a tie.
186
192
  return (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0);
187
193
  });
194
+ // The unmeasured bucket ranks after every measured member, but WITHIN the
195
+ // bucket the same comparator still applies: with cost/quality both
196
+ // NEUTRAL it falls through to the weight tie-break, preserving the
197
+ // documented weight contract that a plain append would silently drop.
198
+ return [...sortMeasured(measured), ...sortMeasured(unmeasured)];
188
199
  }
189
200
  /** Tool narrowing: per-difficulty directive, then classifier hints. */
190
201
  selectTools(decision) {
@@ -223,7 +234,10 @@ export class ClassifierRouter {
223
234
  if (needsEnrichment && member.model) {
224
235
  try {
225
236
  const info = ModelResolver.resolveModel(member.model);
226
- if (info) {
237
+ if (!info) {
238
+ this.deps.logger?.debug?.(`[ClassifierRouter] metaFor: no registry match for ${member.provider}/${member.model}`);
239
+ }
240
+ else {
227
241
  if (cost === undefined) {
228
242
  cost = info.pricing.inputCostPer1K + info.pricing.outputCostPer1K;
229
243
  }
@@ -251,8 +265,8 @@ export class ClassifierRouter {
251
265
  }
252
266
  }
253
267
  }
254
- catch {
255
- // Enrichment is best-effort; ignore registry lookup failures.
268
+ catch (err) {
269
+ this.deps.logger?.warn?.(`[ClassifierRouter] metaFor: registry lookup threw for ${member.provider}/${member.model}`, { error: err instanceof Error ? err.message : String(err) });
256
270
  }
257
271
  }
258
272
  const meta = { cost, quality, capabilities };
@@ -1885,6 +1885,24 @@ export type ProviderDescriptor = {
1885
1885
  localRuntime: boolean;
1886
1886
  /** How ProviderHealthChecker should verify this provider is reachable. */
1887
1887
  healthCheck: "env-only" | "models-probe" | "live-generate";
1888
+ /**
1889
+ * Membership + order in the default health sweep
1890
+ * (`ProviderHealthChecker.checkAllProvidersHealth` with no explicit
1891
+ * list). Lower number = checked and reported first; the sweep's array
1892
+ * order is behaviour for its first-healthy fallback consumers. Absent =
1893
+ * not part of the default sweep. Replaces the hand-maintained 8-provider
1894
+ * array that lived in providerHealth.ts.
1895
+ */
1896
+ defaultHealthSweepPriority?: number;
1897
+ /**
1898
+ * Preference rank for `getBestHealthyProvider`'s default auto-selection
1899
+ * (lower = tried first). Deliberately a SEPARATE ordering from the sweep:
1900
+ * auto-select prefers local/cheap runtimes (litellm, ollama) before cloud
1901
+ * providers, while the sweep reports the majors first. Absent = not in
1902
+ * the default preference list. Replaces the second hand-maintained array
1903
+ * that lived inline as getBestHealthyProvider's default parameter.
1904
+ */
1905
+ autoSelectPreference?: number;
1888
1906
  setupUrl?: string;
1889
1907
  timeouts?: {
1890
1908
  generateMs?: number;
@@ -9,6 +9,7 @@ import { basename } from "path";
9
9
  import { createProxyFetch } from "../proxy/proxyFetch.js";
10
10
  import { DEFAULT_OLLAMA_MODEL } from "../providers/ollama/constants.js";
11
11
  import { ProviderFactory } from "../factories/providerFactory.js";
12
+ import { PROVIDER_DESCRIPTORS } from "../factories/providerDescriptors.js";
12
13
  export class ProviderHealthChecker {
13
14
  static healthCache = new Map();
14
15
  static DEFAULT_TIMEOUT = 5000; // 5 seconds
@@ -1382,16 +1383,13 @@ export class ProviderHealthChecker {
1382
1383
  * Prioritizes healthy providers over configured but unhealthy ones
1383
1384
  * Uses fast, cached health checks to avoid blocking initialization
1384
1385
  */
1385
- static async getBestHealthyProvider(preferredProviders = [
1386
- "litellm",
1387
- "ollama",
1388
- "openai",
1389
- "anthropic",
1390
- "vertex",
1391
- "google-ai",
1392
- "bedrock",
1393
- "azure",
1394
- ]) {
1386
+ static async getBestHealthyProvider(
1387
+ // Auto-select preference comes from the descriptors too — a SEPARATE
1388
+ // ordering from the sweep (local/cheap runtimes first), carried by
1389
+ // autoSelectPreference rather than defaultHealthSweepPriority.
1390
+ preferredProviders = PROVIDER_DESCRIPTORS.filter((d) => d.autoSelectPreference !== undefined)
1391
+ .sort((a, b) => (a.autoSelectPreference ?? 0) - (b.autoSelectPreference ?? 0))
1392
+ .map((d) => d.name)) {
1395
1393
  const healthStatuses = await this.checkAllProvidersHealth({
1396
1394
  includeConnectivityTest: false, // Quick config check only
1397
1395
  cacheResults: true,
@@ -1424,16 +1422,13 @@ export class ProviderHealthChecker {
1424
1422
  * Get health status for all registered providers
1425
1423
  */
1426
1424
  static async checkAllProvidersHealth(options = {}) {
1427
- const providers = [
1428
- AIProviderName.VERTEX,
1429
- AIProviderName.GOOGLE_AI,
1430
- AIProviderName.ANTHROPIC,
1431
- AIProviderName.OPENAI,
1432
- AIProviderName.BEDROCK,
1433
- AIProviderName.AZURE,
1434
- AIProviderName.LITELLM,
1435
- AIProviderName.OLLAMA,
1436
- ];
1425
+ // Sweep membership and ORDER come from the descriptors. Order is
1426
+ // behaviour: auto-select takes the first healthy provider, so the
1427
+ // priority field, not the descriptor array's layout, decides preference.
1428
+ const providers = PROVIDER_DESCRIPTORS.filter((d) => d.defaultHealthSweepPriority !== undefined)
1429
+ .sort((a, b) => (a.defaultHealthSweepPriority ?? 0) -
1430
+ (b.defaultHealthSweepPriority ?? 0))
1431
+ .map((d) => d.name);
1437
1432
  const healthChecks = providers.map((provider) => this.checkProviderHealth(provider, options));
1438
1433
  const results = await Promise.allSettled(healthChecks);
1439
1434
  return results.map((result, index) => {
@@ -13,17 +13,6 @@ import type { RetryOptions } from "../types/index.js";
13
13
  * @returns Calculated delay in milliseconds
14
14
  */
15
15
  export declare function calculateBackoffDelay(attempt: number, initialDelay?: number, multiplier?: number, maxDelay?: number, addJitter?: boolean): number;
16
- /**
17
- * Error types that are typically retryable
18
- */
19
- export declare class NetworkError extends Error {
20
- readonly cause?: Error | undefined;
21
- constructor(message: string, cause?: Error | undefined);
22
- }
23
- export declare class TemporaryError extends Error {
24
- readonly cause?: Error | undefined;
25
- constructor(message: string, cause?: Error | undefined);
26
- }
27
16
  /**
28
17
  * Default retry configuration
29
18
  */
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { logger } from "./logger.js";
6
6
  import { SYSTEM_LIMITS } from "../core/constants.js";
7
+ import { NetworkError } from "../types/index.js";
7
8
  /**
8
9
  * Calculate exponential backoff delay with jitter
9
10
  * @param attempt - Current attempt number (1-based)
@@ -24,25 +25,6 @@ export function calculateBackoffDelay(attempt, initialDelay = SYSTEM_LIMITS.DEFA
24
25
  : 0;
25
26
  return cappedDelay + jitter;
26
27
  }
27
- /**
28
- * Error types that are typically retryable
29
- */
30
- export class NetworkError extends Error {
31
- cause;
32
- constructor(message, cause) {
33
- super(message);
34
- this.cause = cause;
35
- this.name = "NetworkError";
36
- }
37
- }
38
- export class TemporaryError extends Error {
39
- cause;
40
- constructor(message, cause) {
41
- super(message);
42
- this.cause = cause;
43
- this.name = "TemporaryError";
44
- }
45
- }
46
28
  /**
47
29
  * Default retry configuration
48
30
  */
@@ -52,8 +34,11 @@ export const DEFAULT_RETRY_CONFIG = {
52
34
  maxDelay: SYSTEM_LIMITS.DEFAULT_MAX_DELAY,
53
35
  backoffMultiplier: SYSTEM_LIMITS.DEFAULT_BACKOFF_MULTIPLIER,
54
36
  retryCondition: (error) => {
55
- // Retry on network errors, timeouts, and specific HTTP errors
56
- if (error instanceof NetworkError || error instanceof TemporaryError) {
37
+ // Retry on network errors, timeouts, and specific HTTP errors. The
38
+ // instanceof now matches the canonical types/errors.js NetworkError —
39
+ // the local shadow class this file used to declare matched nothing the
40
+ // rest of the SDK ever threw.
41
+ if (error instanceof NetworkError) {
57
42
  return true;
58
43
  }
59
44
  // Retry on timeout errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.26.0",
3
+ "version": "11.26.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -142,7 +142,7 @@
142
142
  "// CI tier — fast, no live AI calls, safe for every commit (test:unit; also see the separate provider-safety-net CI job, which runs build + test:providers-mocked + test:provider-structure + test:error-classifier-contract on every PR)": "",
143
143
  "test:tool-routing": "pnpm exec tsx test/continuous-test-suite-tool-routing.ts",
144
144
  "test:tool-routing-semantic": "pnpm exec tsx test/continuous-test-suite-tool-routing-semantic.ts",
145
- "test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
145
+ "test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
146
146
  "// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit; test:matrix, a different suite covering the full provider capability matrix, runs nightly via .github/workflows/live-matrix.yml — test:providers itself is still only wired into test:live, not any GitHub Actions workflow)": "",
147
147
  "test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
148
148
  "// CI tier — product output (image/video/TTS/PPT) — costs $$ per run (not wired into any GitHub Actions workflow as of this comment; run manually or add to live-matrix.yml if nightly coverage is needed)": "",
@@ -207,6 +207,7 @@
207
207
  "test:archive:security": "pnpm exec tsx test/continuous-test-suite-archive-security.ts",
208
208
  "test:office:security": "pnpm exec tsx test/continuous-test-suite-office-security.ts",
209
209
  "test:model-pool": "pnpm exec tsx test/continuous-test-suite-model-pool.ts",
210
+ "test:classifier-router": "pnpm exec tsx test/continuous-test-suite-classifier-router.ts",
210
211
  "test:vector-chroma": "pnpm exec tsx test/continuous-test-suite-vector-chroma.ts",
211
212
  "test:vector-pgvector": "pnpm exec tsx test/continuous-test-suite-vector-pgvector.ts",
212
213
  "test:vector-pinecone": "pnpm exec tsx test/continuous-test-suite-vector-pinecone.ts",