@juspay/neurolink 10.8.2 → 10.8.4

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.
@@ -29,12 +29,15 @@ export type NeuroLinkConfig = {
29
29
  /**
30
30
  * Curator P2-3: callback signature for centralized fallback policy. When an
31
31
  * explicit callback is configured (per-call or instance), it is invoked for
32
- * ANY error thrown by a generate/stream call except client aborts network
33
- * errors, 5xx, timeouts, auth failures included. The callback receives the
34
- * error unmodified so hosts can classify it themselves (status codes,
35
- * `isNonRetryableProviderError`, …). Return `{ provider, model }` (either /
36
- * both optional) to drive a retry; return `null` to bubble the original
37
- * error untouched.
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.
38
41
  */
39
42
  export type ProviderFallbackCallback = (error: unknown) => Promise<{
40
43
  provider?: string;
@@ -70,11 +73,13 @@ export type NeurolinkConstructorConfig = {
70
73
  credentials?: NeurolinkCredentials;
71
74
  /**
72
75
  * Curator P2-3: callback invoked when a generate/stream call fails with
73
- * any error except a client abort (network errors, 5xx, timeouts, auth
74
- * failures, model-access-denied, …). Lets a host (e.g. Curator) centrally
75
- * drive fallback policy "provider A primary, provider B on failure".
76
- * The callback receives the original error unmodified and returns the
77
- * next `{ provider, model }` to try, or `null` to bubble the error.
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
82
+ * `null` to bubble the error.
78
83
  */
79
84
  providerFallback?: ProviderFallbackCallback;
80
85
  /**
@@ -577,9 +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 client aborts (network errors, 5xx,
581
- * timeouts, auth failures, model-access-denied, …); receives the error
582
- * unmodified. Return `{ provider, model }` to retry, `null` to bubble.
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.
583
585
  */
584
586
  providerFallback?: (error: unknown) => Promise<{
585
587
  provider?: string;
@@ -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,11 +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
491
- * client aborts (network errors, 5xx, timeouts, auth failures,
492
- * model-access-denied, …); receives the error unmodified. There is no
493
- * mid-stream resume once chunks are flowing. Return `{ provider,
494
- * model }` to retry, `null` to bubble.
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.
495
496
  */
496
497
  providerFallback?: (error: unknown) => Promise<{
497
498
  provider?: string;
@@ -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.2",
3
+ "version": "10.8.4",
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": {
@@ -95,6 +95,7 @@
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
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",
98
99
  "test:rag": "npx tsx test/continuous-test-suite-rag.ts",
99
100
  "test:vector-pinecone": "npx tsx test/continuous-test-suite-vector-pinecone.ts",
100
101
  "test:vector-pgvector": "npx tsx test/continuous-test-suite-vector-pgvector.ts",