@juspay/neurolink 11.26.1 → 11.26.2

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.
@@ -1733,6 +1733,22 @@ export class BaseProvider {
1733
1733
  ? error
1734
1734
  : new DOMException("The operation was aborted", "AbortError");
1735
1735
  }
1736
+ // Already formatted by this method — hand it straight back. Formatting is
1737
+ // NOT idempotent: formatProviderError prepends the provider tag every time,
1738
+ // so a second pass produces
1739
+ // "[vertex] Google Vertex AI error: [vertex] Google Vertex AI error: ..."
1740
+ // and, when a rule matched on statusCode the first time, can also DEGRADE
1741
+ // the classification (a specific "quota exhausted" ProviderError re-matching
1742
+ // the bare 429 rule as a generic RateLimitError) because the block below
1743
+ // copies statusCode onto its own output.
1744
+ //
1745
+ // A single Vertex failure reaches here FIVE times for one logical error;
1746
+ // this makes calls 2..5 cheap pass-throughs. `instanceof Error` rather than
1747
+ // a cast: nothing but an Error is ever stamped, and an unstamped value
1748
+ // simply falls through to formatting, which is the safe direction.
1749
+ if (error instanceof Error && isProviderErrorClassified(error)) {
1750
+ return error;
1751
+ }
1736
1752
  const formatted = this.formatProviderError(error);
1737
1753
  // Preserve transport retry metadata across formatting. Provider
1738
1754
  // formatters return fresh Error instances (RateLimitError, NetworkError,
@@ -15,6 +15,7 @@ import { getCapturedLimitSnapshot, getCapturedResponseHeaders, logClaudeLimitSna
15
15
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
16
16
  import { classifyProviderError } from "../../utils/errorClassifier.js";
17
17
  import { logger } from "../../utils/logger.js";
18
+ import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
18
19
  import { ANTHROPIC_ELISION_NOTE, planAnthropicLoopReclaim, previewAnthropicToolResultText, } from "../../context/anthropicLoopGuard.js";
19
20
  import { getAvailableInputTokens } from "../../constants/contextWindows.js";
20
21
  import { estimateTokens } from "../../utils/tokenEstimation.js";
@@ -1749,15 +1750,15 @@ export class AnthropicProvider extends BaseProvider {
1749
1750
  // formatted one the caller received, which is why the existing
1750
1751
  // `loopPromise.catch` guard below does not cover it.
1751
1752
  //
1752
- // Same shape googleAiStudio/client.ts and googleVertex/client.ts already
1753
- // use at every one of their pump sites; Anthropic was the only provider
1754
- // missing it.
1753
+ // Every detached-drain site in the codebase now goes through
1754
+ // drainDetachedPump(), which adopts the rejection and logs the reason at
1755
+ // debug instead of discarding it silently.
1755
1756
  let result;
1756
1757
  try {
1757
1758
  result = await resultPromise;
1758
1759
  }
1759
1760
  catch (error) {
1760
- await pump.catch(() => { });
1761
+ await drainDetachedPump(pump, "Anthropic");
1761
1762
  throw error;
1762
1763
  }
1763
1764
  await pump;
@@ -6,6 +6,7 @@ import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "
6
6
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
7
7
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
8
8
  import { logger } from "../../utils/logger.js";
9
+ import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
9
10
  import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
10
11
  import { runAgenticLoop } from "../../core/loopEngine.js";
11
12
  import { DEFAULT_TOOL_MAX_RETRIES } from "../../core/constants.js";
@@ -861,7 +862,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
861
862
  engineResult = await resultPromise;
862
863
  }
863
864
  catch (error) {
864
- await pump.catch(() => { });
865
+ await drainDetachedPump(pump, "GoogleAIStudio");
865
866
  logger.error("[GoogleAIStudio] Native SDK error", error);
866
867
  throw this.handleProviderError(error);
867
868
  }
@@ -1173,7 +1174,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
1173
1174
  engineResult = await resultPromise;
1174
1175
  }
1175
1176
  catch (error) {
1176
- await drain.catch(() => { });
1177
+ await drainDetachedPump(drain, "GoogleAIStudio");
1177
1178
  logger.error("[GoogleAIStudio] Native SDK generate error", error);
1178
1179
  throw this.handleProviderError(error);
1179
1180
  }
@@ -20,6 +20,7 @@ import { applyVertexAnthropicCacheBreakpoints } from "../../utils/anthropicCache
20
20
  import { FileDetector } from "../../utils/fileDetector.js";
21
21
  import { mergeMediaFileAliases, normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
22
22
  import { logger } from "../../utils/logger.js";
23
+ import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
23
24
  import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
24
25
  import { ANTHROPIC_ELISION_NOTE, planAnthropicLoopReclaim, previewAnthropicToolResultText, } from "../../context/anthropicLoopGuard.js";
25
26
  import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, toVertexAnthropicModelId, } from "../../utils/modelDetection.js";
@@ -1814,7 +1815,7 @@ export class GoogleVertexProvider extends BaseProvider {
1814
1815
  // rethrow the very error the branch below has already decided to absorb —
1815
1816
  // which is what turned both turn-clock cases into failures instead of
1816
1817
  // clean deadline exits.
1817
- await pump.catch(() => { });
1818
+ await drainDetachedPump(pump, "GoogleVertex");
1818
1819
  if (turnFailure !== undefined) {
1819
1820
  // A mid-drain abort surfaces as an AbortError. End gracefully into the
1820
1821
  // terminal block instead of re-throwing — a re-throw would route the
@@ -2598,7 +2599,7 @@ export class GoogleVertexProvider extends BaseProvider {
2598
2599
  engineResult = await resultPromise;
2599
2600
  }
2600
2601
  catch (error) {
2601
- await pump.catch(() => { });
2602
+ await drainDetachedPump(pump, "GoogleVertex");
2602
2603
  // A mid-drain abort surfaces as an AbortError. End gracefully into the
2603
2604
  // terminal block instead of re-throwing — a re-throw would route the
2604
2605
  // caller's abort into a second unbounded fallback stream().
@@ -3687,7 +3688,7 @@ export class GoogleVertexProvider extends BaseProvider {
3687
3688
  // Drained tolerantly and exactly once: when a turn ends by abort the
3688
3689
  // channel rejects too, and re-awaiting a settled rejection would rethrow
3689
3690
  // the error the branch below has already decided to absorb.
3690
- await pump.catch(() => { });
3691
+ await drainDetachedPump(pump, "GoogleVertex");
3691
3692
  if (turnFailure !== undefined) {
3692
3693
  if (internalAbort.signal.aborted || isAbortError(turnFailure)) {
3693
3694
  wasAborted = true;
@@ -4686,7 +4687,7 @@ export class GoogleVertexProvider extends BaseProvider {
4686
4687
  catch (error) {
4687
4688
  turnFailure = error;
4688
4689
  }
4689
- await pump.catch(() => { });
4690
+ await drainDetachedPump(pump, "GoogleVertex");
4690
4691
  if (turnFailure !== undefined) {
4691
4692
  if (internalAbort.signal.aborted || isAbortError(turnFailure)) {
4692
4693
  wasAborted = true;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Await a detached stream pump, swallowing its rejection but not its evidence.
3
+ *
4
+ * Several providers drain their engine's channel with a pump started outside
5
+ * the promise chain the caller awaits:
6
+ *
7
+ * const pump = (async () => { for await (const chunk of stream) { ... } })();
8
+ * const result = await resultPromise; // can throw
9
+ * await pump; // never reached if it does
10
+ *
11
+ * When the turn fails, `resultPromise` and the pump reject together — the
12
+ * engine calls `channel.error(err)` before closing — so the pump must be
13
+ * adopted on the error path too. Nothing adopting it is not a cosmetic leak:
14
+ * an unhandled rejection TERMINATES the process, so a caller who correctly
15
+ * try/catches the streaming error still dies. That was a real bug in the
16
+ * Anthropic path.
17
+ *
18
+ * The established remedy is `await pump.catch(() => {})`, which every site
19
+ * already uses. The gap this closes is the second half: `() => {}` throws the
20
+ * reason away, so the raw upstream error — the one carrying the provider's
21
+ * actual wire response — was invisible in traces at every one of the seven
22
+ * sites (six named `pump`, plus one named `drain` in googleAiStudio's
23
+ * non-streaming path, which a search for `pump` does not find). It is logged at DEBUG rather than WARN deliberately: on a failing turn
24
+ * this reason is almost always a duplicate of the error the caller is already
25
+ * being handed, and on an aborted turn it is the expected AbortError. It is
26
+ * diagnostic detail, not a new event worth alerting on.
27
+ *
28
+ * Behaviour is otherwise identical to `await pump.catch(() => {})`: it awaits,
29
+ * it never rethrows.
30
+ */
31
+ export declare function drainDetachedPump(pump: Promise<unknown>, providerLabel: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { logger } from "./logger.js";
2
+ /**
3
+ * Await a detached stream pump, swallowing its rejection but not its evidence.
4
+ *
5
+ * Several providers drain their engine's channel with a pump started outside
6
+ * the promise chain the caller awaits:
7
+ *
8
+ * const pump = (async () => { for await (const chunk of stream) { ... } })();
9
+ * const result = await resultPromise; // can throw
10
+ * await pump; // never reached if it does
11
+ *
12
+ * When the turn fails, `resultPromise` and the pump reject together — the
13
+ * engine calls `channel.error(err)` before closing — so the pump must be
14
+ * adopted on the error path too. Nothing adopting it is not a cosmetic leak:
15
+ * an unhandled rejection TERMINATES the process, so a caller who correctly
16
+ * try/catches the streaming error still dies. That was a real bug in the
17
+ * Anthropic path.
18
+ *
19
+ * The established remedy is `await pump.catch(() => {})`, which every site
20
+ * already uses. The gap this closes is the second half: `() => {}` throws the
21
+ * reason away, so the raw upstream error — the one carrying the provider's
22
+ * actual wire response — was invisible in traces at every one of the seven
23
+ * sites (six named `pump`, plus one named `drain` in googleAiStudio's
24
+ * non-streaming path, which a search for `pump` does not find). It is logged at DEBUG rather than WARN deliberately: on a failing turn
25
+ * this reason is almost always a duplicate of the error the caller is already
26
+ * being handed, and on an aborted turn it is the expected AbortError. It is
27
+ * diagnostic detail, not a new event worth alerting on.
28
+ *
29
+ * Behaviour is otherwise identical to `await pump.catch(() => {})`: it awaits,
30
+ * it never rethrows.
31
+ */
32
+ export async function drainDetachedPump(pump, providerLabel) {
33
+ try {
34
+ await pump;
35
+ }
36
+ catch (error) {
37
+ logger.debug(`[${providerLabel}] detached stream pump rejected; reason absorbed because the turn's own error is authoritative`, error);
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.26.1",
3
+ "version": "11.26.2",
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": {