@juspay/neurolink 10.8.1 → 10.8.3

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/dist/neurolink.js CHANGED
@@ -100,6 +100,7 @@ import { runWorkflow } from "./workflow/core/workflowRunner.js";
100
100
  import { ModelPool, classifyProviderError } from "./routing/index.js";
101
101
  import { ClassifierRouter } from "./routing/classifierRouter.js";
102
102
  import { looksLikeModelAccessDenied as sharedLooksLikeModelAccessDenied, isNonRetryableProviderError as sharedIsNonRetryableProviderError, } from "./utils/providerErrorClassification.js";
103
+ import { getErrorStatusCode } from "./utils/providerRetry.js";
103
104
  import { detectAndRedactPII } from "./utils/piiDetector.js";
104
105
  import { validateResponse } from "./utils/responseValidator.js";
105
106
  /**
@@ -3261,7 +3262,7 @@ Current user's request: ${currentInput}`;
3261
3262
  * Curator P2-3: wraps a generate/stream call with the fallback
3262
3263
  * orchestration (`providerFallback` callback + `modelChain` walker).
3263
3264
  *
3264
- * On a model-access-denied error from the inner call:
3265
+ * On a qualifying error from the inner call:
3265
3266
  * 1. Resolve the effective callback (per-call > instance > synthesised
3266
3267
  * from modelChain) and the effective chain (per-call > instance).
3267
3268
  * 2. Walk attempts: invoke callback (or pop next chain entry) → emit
@@ -3269,6 +3270,12 @@ Current user's request: ${currentInput}`;
3269
3270
  * model}.
3270
3271
  * 3. Stop on first success, on a callback returning null, or after
3271
3272
  * exhausting the chain (throw the most recent error).
3273
+ *
3274
+ * What qualifies depends on how fallback was configured: an EXPLICIT
3275
+ * `providerFallback` callback is consulted for any error except client
3276
+ * aborts (the callback owns the decision — it receives the error
3277
+ * unmodified and can return null to bubble), while modelChain-only
3278
+ * configs keep the narrow model-access-denied gate.
3272
3279
  */
3273
3280
  async runWithFallbackOrchestration(optionsOrPrompt, kind, inner) {
3274
3281
  const initialAttempt = await this.attemptInner(inner, optionsOrPrompt);
@@ -3276,10 +3283,8 @@ Current user's request: ${currentInput}`;
3276
3283
  return initialAttempt.ok;
3277
3284
  }
3278
3285
  let lastError = initialAttempt.error;
3279
- if (!looksLikeModelAccessDenied(lastError)) {
3280
- throw lastError;
3281
- }
3282
- // Build the chain orchestration.
3286
+ // Resolve the fallback configuration BEFORE gating so the gate can
3287
+ // distinguish an explicit callback from a modelChain-only setup.
3283
3288
  const requestedProvider = (typeof optionsOrPrompt === "object"
3284
3289
  ? optionsOrPrompt.provider
3285
3290
  : undefined);
@@ -3293,6 +3298,30 @@ Current user's request: ${currentInput}`;
3293
3298
  const perCallChain = callOpts.modelChain;
3294
3299
  const effectiveCallback = perCallCallback ?? this.fallbackConfig.providerFallback;
3295
3300
  const effectiveChain = perCallChain ?? this.fallbackConfig.modelChain;
3301
+ // Explicit callback (per-call or instance providerFallback): the callback
3302
+ // owns the decision for any error except genuine caller cancels — it can
3303
+ // return null to bubble. modelChain-only keeps the narrow
3304
+ // model-access-denied gate so chain walkers don't retry errors the chain
3305
+ // can't fix.
3306
+ //
3307
+ // Error shape alone cannot identify a caller cancel: NeuroLink's own
3308
+ // watchdog/timeout controllers abort the same composed signal, and SDKs
3309
+ // normalize that into the identical AbortError shapes a user cancel
3310
+ // produces (e.g. Anthropic's APIUserAbortError). Ground truth is the
3311
+ // caller-supplied abortSignal itself — only when IT has fired is an
3312
+ // abort-shaped error a real cancel; otherwise the abort was internally
3313
+ // initiated (turn/stall watchdog, per-step timeout) and the callback
3314
+ // must still be consulted so provider hangs can fall back. Read live
3315
+ // (not captured) so the post-retry re-gate sees a cancel that arrives
3316
+ // mid-fallback; cloneOptionsForCallIsolation keeps abortSignal
3317
+ // by-reference, so the retried options observe the same signal.
3318
+ const callerAborted = () => callOpts.abortSignal?.aborted === true;
3319
+ const shouldOrchestrateFallback = (err) => effectiveCallback
3320
+ ? !(isAbortError(err) && callerAborted())
3321
+ : looksLikeModelAccessDenied(err);
3322
+ if (!shouldOrchestrateFallback(lastError)) {
3323
+ throw lastError;
3324
+ }
3296
3325
  if (!effectiveCallback && !effectiveChain) {
3297
3326
  throw lastError;
3298
3327
  }
@@ -3364,7 +3393,7 @@ Current user's request: ${currentInput}`;
3364
3393
  }
3365
3394
  lastError = retryAttempt.error;
3366
3395
  attemptedRequestedModel = next.model ?? attemptedRequestedModel;
3367
- if (!looksLikeModelAccessDenied(lastError)) {
3396
+ if (!shouldOrchestrateFallback(lastError)) {
3368
3397
  throw lastError;
3369
3398
  }
3370
3399
  }
@@ -5223,7 +5252,12 @@ Current user's request: ${currentInput}`;
5223
5252
  error.isRetryable ===
5224
5253
  false) ||
5225
5254
  (error instanceof Error &&
5226
- error.statusCode === 400);
5255
+ error.statusCode === 400) ||
5256
+ // A 429 already went through the provider layer's bounded,
5257
+ // Retry-After-aware retry (withProviderRetry) — re-running the
5258
+ // whole MCP generation against a rate-limited key is redundant
5259
+ // and only delays fallback orchestration.
5260
+ getErrorStatusCode(error) === 429;
5227
5261
  if (isNonRetryable) {
5228
5262
  logger.debug(`[${functionTag}] Non-retryable error detected, skipping remaining retries`);
5229
5263
  break;
@@ -608,6 +608,12 @@ export class AnthropicProvider extends BaseProvider {
608
608
  // Note: No headers passed - fetch wrapper sets oauth-2025-04-20 beta header
609
609
  fetch: oauthFetch,
610
610
  timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
611
+ // The SDK's built-in retry honors Retry-After hints without any
612
+ // upper bound (a 429 with retry-after: 8549 sleeps 2.4h per retry,
613
+ // invisible to fallback orchestration). Retries are the
614
+ // orchestrator's job; transient network blips are still retried by
615
+ // the fetch wrapper.
616
+ maxRetries: 0,
611
617
  });
612
618
  logger.debug("[AnthropicProvider] Anthropic SDK client created with OAuth fetch wrapper");
613
619
  logger.debug("Anthropic Provider initialized with OAuth", {
@@ -654,6 +660,9 @@ export class AnthropicProvider extends BaseProvider {
654
660
  ...(normalizedBaseURL && { baseURL: normalizedBaseURL }),
655
661
  fetch: createProxyFetch(),
656
662
  timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
663
+ // See the OAuth-path client above: unbounded Retry-After sleeps in
664
+ // the SDK's retry loop must never stall fallback orchestration.
665
+ maxRetries: 0,
657
666
  });
658
667
  logger.debug("Anthropic Provider initialized with API key", {
659
668
  modelName: this.modelName,
@@ -724,6 +733,12 @@ export class AnthropicProvider extends BaseProvider {
724
733
  headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
725
734
  }
726
735
  }
736
+ if (usingProxy) {
737
+ // WAFs in front of ANTHROPIC_BASE_URL proxies commonly block the bare
738
+ // SDK UA ("Anthropic/JS x.y.z"); send the claude-cli UA the OAuth path
739
+ // already uses. Direct-to-Anthropic traffic keeps the honest SDK UA.
740
+ headers["User-Agent"] = CLAUDE_CLI_USER_AGENT;
741
+ }
727
742
  // Add subscription-specific headers if applicable
728
743
  if (this.subscriptionTier !== "api") {
729
744
  headers["x-subscription-tier"] = this.subscriptionTier;
@@ -532,6 +532,10 @@ const createVertexAnthropicSettings = async (region, timeoutMs) => {
532
532
  projectId: project,
533
533
  region: location,
534
534
  ...(timeoutMs !== undefined && { timeout: timeoutMs }),
535
+ // The SDK's built-in retry honors Retry-After hints without any upper
536
+ // bound (a 429 with retry-after: 8549 sleeps 2.4h per retry, invisible
537
+ // to fallback orchestration). Retries are the orchestrator's job.
538
+ maxRetries: 0,
535
539
  };
536
540
  };
537
541
  // Helper function to determine if a model is an Anthropic model
@@ -612,6 +612,15 @@ export const buildAPIError = async (url, body, res) => {
612
612
  `OpenAI-compatible request failed with status ${res.status}`;
613
613
  const err = new Error(msg);
614
614
  err.statusCode = res.status;
615
+ // Response headers carry rate-limit hints (Retry-After, X-RateLimit-*)
616
+ // that withProviderRetry needs to honor short waits and surface long ones.
617
+ // Allowlisted to just those — attaching every header would let unrelated
618
+ // (potentially sensitive) values like set-cookie or internal routing
619
+ // headers ride along when the error is logged.
620
+ err.responseHeaders = Object.fromEntries([...res.headers.entries()].filter(([key]) => {
621
+ const lower = key.toLowerCase();
622
+ return lower === "retry-after" || lower.startsWith("x-ratelimit-");
623
+ }));
615
624
  err.url = url;
616
625
  // Redacted summary only — never attach raw prompts, tool definitions, or
617
626
  // tool arguments to the thrown error. Anything serialized by upstream
@@ -27,10 +27,17 @@ export type NeuroLinkConfig = {
27
27
  [key: string]: unknown;
28
28
  };
29
29
  /**
30
- * Curator P2-3: callback signature for centralized fallback policy. Invoked
31
- * when a generate/stream call fails with what looks like a model-access-denied
32
- * error. Return `{ provider, model }` (either / both optional) to drive a
33
- * retry; return `null` to bubble the original error untouched.
30
+ * Curator P2-3: callback signature for centralized fallback policy. When an
31
+ * explicit callback is configured (per-call or instance), it is invoked for
32
+ * ANY error thrown by a generate/stream call except genuine caller cancels
33
+ * network errors, 5xx, timeouts, auth failures included. A caller cancel is
34
+ * identified by the caller-supplied `abortSignal` having fired, not by error
35
+ * shape: abort-shaped errors from NeuroLink's own turn/stall watchdogs and
36
+ * per-step timeouts DO invoke the callback, so provider hangs can fall back.
37
+ * The callback receives the error unmodified so hosts can classify it
38
+ * themselves (status codes, `isNonRetryableProviderError`, …). Return
39
+ * `{ provider, model }` (either / both optional) to drive a retry; return
40
+ * `null` to bubble the original error untouched.
34
41
  */
35
42
  export type ProviderFallbackCallback = (error: unknown) => Promise<{
36
43
  provider?: string;
@@ -65,16 +72,23 @@ export type NeurolinkConstructorConfig = {
65
72
  */
66
73
  credentials?: NeurolinkCredentials;
67
74
  /**
68
- * Curator P2-3: callback invoked on model-access-denied. Lets a host (e.g.
69
- * Curator) centrally drive fallback policy. The callback receives the
70
- * original error and returns the next `{ provider, model }` to try, or
75
+ * Curator P2-3: callback invoked when a generate/stream call fails with
76
+ * any error except a genuine caller cancel i.e. the caller-supplied
77
+ * `abortSignal` fired (network errors, 5xx, timeouts, auth failures,
78
+ * model-access-denied, and internal watchdog aborts all invoke it). Lets
79
+ * a host (e.g. Curator) centrally drive fallback policy — "provider A
80
+ * primary, provider B on failure". The callback receives the original
81
+ * error unmodified and returns the next `{ provider, model }` to try, or
71
82
  * `null` to bubble the error.
72
83
  */
73
84
  providerFallback?: ProviderFallbackCallback;
74
85
  /**
75
- * Curator P2-3: ordered list of model names to try in sequence on
76
- * model-access-denied. Sugar over `providerFallback`. The current
77
- * provider is preserved across the chain; only the model name changes.
86
+ * Curator P2-3: ordered list of model names to try in sequence. Sugar
87
+ * over `providerFallback`, but with a narrower trigger: without an
88
+ * explicit callback the chain only advances on model-access-denied
89
+ * errors — other failures (network, 5xx, timeouts) bubble immediately.
90
+ * The current provider is preserved across the chain; only the model
91
+ * name changes.
78
92
  */
79
93
  modelChain?: string[];
80
94
  /**
@@ -577,6 +577,11 @@ export type GenerateOptions = {
577
577
  /**
578
578
  * Curator P2-3: per-call fallback callback. Overrides any
579
579
  * instance-level `providerFallback` set on `new NeuroLink({...})`.
580
+ * Invoked for any error except a genuine caller cancel — i.e. this
581
+ * call's `abortSignal` fired (network errors, 5xx, timeouts, auth
582
+ * failures, model-access-denied, and internal watchdog aborts all invoke
583
+ * it); receives the error unmodified. Return `{ provider, model }` to
584
+ * retry, `null` to bubble.
580
585
  */
581
586
  providerFallback?: (error: unknown) => Promise<{
582
587
  provider?: string;
@@ -584,7 +589,9 @@ export type GenerateOptions = {
584
589
  } | null>;
585
590
  /**
586
591
  * Curator P2-3: per-call ordered model chain. Overrides any
587
- * instance-level `modelChain`. Tried in order on model-access-denied.
592
+ * instance-level `modelChain`. Without an explicit `providerFallback`
593
+ * callback the chain only advances on model-access-denied errors —
594
+ * other failures (network, 5xx, timeouts) bubble immediately.
588
595
  */
589
596
  modelChain?: string[];
590
597
  /**
@@ -925,6 +925,8 @@ export type AnthropicVertexSettings = {
925
925
  region: string;
926
926
  /** SDK request timeout in milliseconds */
927
927
  timeout?: number;
928
+ /** SDK-internal retry budget (transport retries are the orchestrator's job) */
929
+ maxRetries?: number;
928
930
  };
929
931
  /**
930
932
  * OpenAI-compatible models endpoint response structure
@@ -487,6 +487,12 @@ export type StreamOptions = {
487
487
  /**
488
488
  * Curator P2-3: per-call fallback callback. Overrides any
489
489
  * instance-level `providerFallback` set on `new NeuroLink({...})`.
490
+ * Invoked for any error thrown while establishing the stream, except a
491
+ * genuine caller cancel — i.e. this call's `abortSignal` fired (network
492
+ * errors, 5xx, timeouts, auth failures, model-access-denied, and
493
+ * internal watchdog aborts all invoke it); receives the error
494
+ * unmodified. There is no mid-stream resume once chunks are flowing.
495
+ * Return `{ provider, model }` to retry, `null` to bubble.
490
496
  */
491
497
  providerFallback?: (error: unknown) => Promise<{
492
498
  provider?: string;
@@ -494,7 +500,9 @@ export type StreamOptions = {
494
500
  } | null>;
495
501
  /**
496
502
  * Curator P2-3: per-call ordered model chain. Overrides any
497
- * instance-level `modelChain`. Tried in order on model-access-denied.
503
+ * instance-level `modelChain`. Without an explicit `providerFallback`
504
+ * callback the chain only advances on model-access-denied errors —
505
+ * other failures (network, 5xx, timeouts) bubble immediately.
498
506
  */
499
507
  modelChain?: string[];
500
508
  /**
@@ -20,8 +20,16 @@ export declare const MAX_PROVIDER_RETRIES = 2;
20
20
  export declare const BASE_RETRY_DELAY_MS = 1000;
21
21
  /** Minimum delay in ms when a retryable response provides no retry timing. */
22
22
  export declare const NO_HINT_FLOOR_MS = 10000;
23
- /** Maximum server-requested retry delay honored by provider retries. */
24
- export declare const MAX_RETRY_AFTER_MS = 120000;
23
+ /**
24
+ * Maximum server-requested retry delay honored by provider retries.
25
+ *
26
+ * A hint above this cap is not clamped-and-slept: the server declared an
27
+ * unavailability window we are not willing to wait out, so the error is
28
+ * surfaced immediately instead — fallback orchestration (providerFallback /
29
+ * modelChain) can route to another provider, and callers without fallback
30
+ * get a prompt rate-limit error rather than a silent multi-minute stall.
31
+ */
32
+ export declare const MAX_RETRY_AFTER_MS = 60000;
25
33
  /**
26
34
  * Check whether an error thrown by the AI SDK is retryable.
27
35
  *
@@ -29,6 +37,21 @@ export declare const MAX_RETRY_AFTER_MS = 120000;
29
37
  * uses a branded symbol marker, so `instanceof` doesn't work across package
30
38
  * boundaries). Falls back to duck-typing for non-APICallError cases.
31
39
  */
40
+ /**
41
+ * Duck-typed HTTP status extraction for errors of unknown provenance.
42
+ * NeuroLink's hand-rolled clients stamp `.statusCode`; official SDK errors
43
+ * (e.g. @anthropic-ai/sdk APIError) expose `.status`. Shared with
44
+ * baseProvider's handleProviderError metadata preservation.
45
+ */
46
+ export declare function duckTypedStatusCode(error: unknown): number | undefined;
47
+ /**
48
+ * Duck-typed Retry-After extraction for errors of unknown provenance.
49
+ * Reads a pre-parsed `.retryAfterMs` (stamped by metadata-preserving
50
+ * wrappers like handleProviderError), then `.headers` (Headers instance or
51
+ * record — e.g. @anthropic-ai/sdk APIError), then `.responseHeaders` (the
52
+ * hand-rolled OpenAI-compatible client's record). Shared with baseProvider.
53
+ */
54
+ export declare function extractRetryAfterMsFromError(error: unknown): number | undefined;
32
55
  export declare function isRetryableProviderError(error: unknown): boolean;
33
56
  /**
34
57
  * Extract the HTTP status code from an AI SDK error, if available.
@@ -23,8 +23,16 @@ export const MAX_PROVIDER_RETRIES = 2;
23
23
  export const BASE_RETRY_DELAY_MS = 1000;
24
24
  /** Minimum delay in ms when a retryable response provides no retry timing. */
25
25
  export const NO_HINT_FLOOR_MS = 10_000;
26
- /** Maximum server-requested retry delay honored by provider retries. */
27
- export const MAX_RETRY_AFTER_MS = 120_000;
26
+ /**
27
+ * Maximum server-requested retry delay honored by provider retries.
28
+ *
29
+ * A hint above this cap is not clamped-and-slept: the server declared an
30
+ * unavailability window we are not willing to wait out, so the error is
31
+ * surfaced immediately instead — fallback orchestration (providerFallback /
32
+ * modelChain) can route to another provider, and callers without fallback
33
+ * get a prompt rate-limit error rather than a silent multi-minute stall.
34
+ */
35
+ export const MAX_RETRY_AFTER_MS = 60_000;
28
36
  const sleepWithTimeout = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
29
37
  /**
30
38
  * Check whether an error thrown by the AI SDK is retryable.
@@ -33,6 +41,54 @@ const sleepWithTimeout = (delayMs) => new Promise((resolve) => setTimeout(resolv
33
41
  * uses a branded symbol marker, so `instanceof` doesn't work across package
34
42
  * boundaries). Falls back to duck-typing for non-APICallError cases.
35
43
  */
44
+ /**
45
+ * Duck-typed HTTP status extraction for errors of unknown provenance.
46
+ * NeuroLink's hand-rolled clients stamp `.statusCode`; official SDK errors
47
+ * (e.g. @anthropic-ai/sdk APIError) expose `.status`. Shared with
48
+ * baseProvider's handleProviderError metadata preservation.
49
+ */
50
+ export function duckTypedStatusCode(error) {
51
+ if (!error || typeof error !== "object") {
52
+ return undefined;
53
+ }
54
+ const err = error;
55
+ if (typeof err.statusCode === "number") {
56
+ return err.statusCode;
57
+ }
58
+ if (typeof err.status === "number") {
59
+ return err.status;
60
+ }
61
+ return undefined;
62
+ }
63
+ /**
64
+ * Duck-typed Retry-After extraction for errors of unknown provenance.
65
+ * Reads a pre-parsed `.retryAfterMs` (stamped by metadata-preserving
66
+ * wrappers like handleProviderError), then `.headers` (Headers instance or
67
+ * record — e.g. @anthropic-ai/sdk APIError), then `.responseHeaders` (the
68
+ * hand-rolled OpenAI-compatible client's record). Shared with baseProvider.
69
+ */
70
+ export function extractRetryAfterMsFromError(error) {
71
+ if (!error || typeof error !== "object") {
72
+ return undefined;
73
+ }
74
+ const err = error;
75
+ // Number.isFinite rejects NaN/Infinity: typeof NaN === "number", and a NaN
76
+ // hint would survive Math.max() into sleep(NaN) — an immediate retry
77
+ // instead of the no-hint floor.
78
+ if (typeof err.retryAfterMs === "number" &&
79
+ Number.isFinite(err.retryAfterMs)) {
80
+ return err.retryAfterMs;
81
+ }
82
+ for (const candidate of [err.headers, err.responseHeaders]) {
83
+ if (candidate && typeof candidate === "object") {
84
+ const parsed = parseRetryAfterMs(candidate);
85
+ if (parsed !== undefined) {
86
+ return parsed;
87
+ }
88
+ }
89
+ }
90
+ return undefined;
91
+ }
36
92
  export function isRetryableProviderError(error) {
37
93
  // Preferred path: use the AI SDK's own branded type check + isRetryable flag
38
94
  if (APICallError.isInstance(error)) {
@@ -42,8 +98,8 @@ export function isRetryableProviderError(error) {
42
98
  return error.retriable;
43
99
  }
44
100
  // Fallback: duck-type for status codes on errors that aren't APICallError
45
- if (error && typeof error === "object" && "statusCode" in error) {
46
- const statusCode = error.statusCode;
101
+ const statusCode = duckTypedStatusCode(error);
102
+ if (statusCode !== undefined) {
47
103
  return statusCode === 429 || statusCode >= 500;
48
104
  }
49
105
  return false;
@@ -55,10 +111,7 @@ export function getErrorStatusCode(error) {
55
111
  if (APICallError.isInstance(error)) {
56
112
  return error.statusCode;
57
113
  }
58
- if (error && typeof error === "object" && "statusCode" in error) {
59
- return error.statusCode;
60
- }
61
- return undefined;
114
+ return duckTypedStatusCode(error);
62
115
  }
63
116
  function getRetryAfterMs(error) {
64
117
  if (APICallError.isInstance(error) && error.responseHeaders) {
@@ -67,10 +120,12 @@ function getRetryAfterMs(error) {
67
120
  return parsedDelay;
68
121
  }
69
122
  }
70
- if (error instanceof NeuroLinkError && error.retryAfterMs !== undefined) {
123
+ if (error instanceof NeuroLinkError &&
124
+ error.retryAfterMs !== undefined &&
125
+ Number.isFinite(error.retryAfterMs)) {
71
126
  return error.retryAfterMs;
72
127
  }
73
- return undefined;
128
+ return extractRetryAfterMsFromError(error);
74
129
  }
75
130
  /**
76
131
  * Execute a provider call with instrumented retry logic.
@@ -110,10 +165,29 @@ export async function withProviderRetry(operation, span, label, sleep = sleepWit
110
165
  throw error;
111
166
  }
112
167
  const retryAfterMs = getRetryAfterMs(error);
113
- const boundedRetryAfterMs = retryAfterMs === undefined
114
- ? undefined
115
- : Math.min(MAX_RETRY_AFTER_MS, Math.max(0, retryAfterMs));
116
- const delay = boundedRetryAfterMs ??
168
+ if (retryAfterMs !== undefined && retryAfterMs > MAX_RETRY_AFTER_MS) {
169
+ span?.setAttribute("gen_ai.provider.total_attempts", attempt + 1);
170
+ span?.addEvent("gen_ai.provider.retry_suppressed", {
171
+ "retry.after_ms": retryAfterMs,
172
+ ...(statusCode !== undefined && { "retry.status_code": statusCode }),
173
+ });
174
+ // Best-effort stamp so upper retry layers (the MCP generation loop's
175
+ // isRetryable check) also surface instead of re-running a
176
+ // rate-limited provider.
177
+ try {
178
+ Object.assign(error, {
179
+ isRetryable: false,
180
+ retryAfterMs,
181
+ });
182
+ }
183
+ catch {
184
+ /* frozen error — the throw below still surfaces it */
185
+ }
186
+ logger.warn(`[providerRetry] ${label} not retrying — server requested a ` +
187
+ `${Math.round(retryAfterMs / 1000)}s wait (cap ${MAX_RETRY_AFTER_MS / 1000}s); surfacing for fallback`, { attempt: attempt + 1, retryAfterMs, statusCode });
188
+ throw error;
189
+ }
190
+ const delay = (retryAfterMs === undefined ? undefined : Math.max(0, retryAfterMs)) ??
117
191
  Math.max(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), NO_HINT_FLOOR_MS);
118
192
  // Record retry event on the OTel span
119
193
  span?.addEvent("gen_ai.provider.retry", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.8.1",
3
+ "version": "10.8.3",
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": {
@@ -94,6 +94,8 @@
94
94
  "test:mcp:limits": "npx tsx test/continuous-test-suite-mcp-output-limits.ts",
95
95
  "test:mcp:infra": "npx tsx test/continuous-test-suite-mcp-infra.ts",
96
96
  "test:providers-mocked": "npx tsx test/continuous-test-suite-providers-mocked.ts",
97
+ "test:provider-fallback": "npx tsx test/continuous-test-suite-provider-fallback.ts",
98
+ "test:provider-fallback-latency": "npx tsx test/continuous-test-suite-provider-fallback-latency.ts",
97
99
  "test:rag": "npx tsx test/continuous-test-suite-rag.ts",
98
100
  "test:vector-pinecone": "npx tsx test/continuous-test-suite-vector-pinecone.ts",
99
101
  "test:vector-pgvector": "npx tsx test/continuous-test-suite-vector-pgvector.ts",