@juspay/neurolink 11.18.2 → 11.18.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.
@@ -16,8 +16,9 @@
16
16
  * and original -> sanitized when writing `functionResponse` parts. Neither the
17
17
  * engine nor any other adapter needs to know sanitization happened.
18
18
  */
19
+ import { guardToolExecutor } from "./toolExecutionGuards.js";
19
20
  import { resolveLiveTool } from "../tools/toolDiscovery.js";
20
- import { collectStreamChunksIncremental, guardToolExecutor, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
21
+ import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
21
22
  export function createGeminiLoopAdapter(config) {
22
23
  /**
23
24
  * Sanitized wire name -> the name the caller registered. Rebuilt per step
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Guards applied around a single tool execution inside a native provider loop.
3
+ *
4
+ * Lives here rather than beside any one provider because it is used by all of
5
+ * them: both Vertex+Claude loops call it directly, and the Gemini paths reach
6
+ * it through `buildDedupedEngineTools`. It previously sat in
7
+ * `providers/googleNativeGemini3/utils.ts`, which meant the Anthropic loops
8
+ * imported a tool-execution primitive out of a Gemini module — accurate about
9
+ * where it was written, misleading about what depends on it.
10
+ *
11
+ * Nothing in here is provider-specific: it takes an executor and a guards
12
+ * object and returns a wrapped executor.
13
+ */
14
+ import type { Tool, ToolExecutionGuards } from "../types/index.js";
15
+ /**
16
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
+ * discovered tools into the live record between steps; without this refresh
19
+ * they stay invisible to the rest of the turn and every call dies as
20
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
+ * `toolsConfig` by reference — and returns true when anything was added.
22
+ */
23
+ /**
24
+ * Everything a native Gemini loop wraps around a tool call that the shared
25
+ * engine does not do itself.
26
+ *
27
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
28
+ * abort is observed the moment it fires rather than after the tool settles,
29
+ * and the timeout still bounds a tool that neither settles nor honours its
30
+ * signal. The progress pings bracket the await because the stall watchdog is
31
+ * a whole-turn interval comparing wall-clock against the last progress mark —
32
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
33
+ *
34
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
35
+ * one declared up front; keeping this inline made the discovered tool the one
36
+ * executor in the system that ran raw.
37
+ */
38
+ export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: ToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Guards applied around a single tool execution inside a native provider loop.
3
+ *
4
+ * Lives here rather than beside any one provider because it is used by all of
5
+ * them: both Vertex+Claude loops call it directly, and the Gemini paths reach
6
+ * it through `buildDedupedEngineTools`. It previously sat in
7
+ * `providers/googleNativeGemini3/utils.ts`, which meant the Anthropic loops
8
+ * imported a tool-execution primitive out of a Gemini module — accurate about
9
+ * where it was written, misleading about what depends on it.
10
+ *
11
+ * Nothing in here is provider-specific: it takes an executor and a guards
12
+ * object and returns a wrapped executor.
13
+ */
14
+ import { raceWithAbort, withTimeout } from "../utils/async/index.js";
15
+ /**
16
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
+ * discovered tools into the live record between steps; without this refresh
19
+ * they stay invisible to the rest of the turn and every call dies as
20
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
+ * `toolsConfig` by reference — and returns true when anything was added.
22
+ */
23
+ /**
24
+ * Everything a native Gemini loop wraps around a tool call that the shared
25
+ * engine does not do itself.
26
+ *
27
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
28
+ * abort is observed the moment it fires rather than after the tool settles,
29
+ * and the timeout still bounds a tool that neither settles nor honours its
30
+ * signal. The progress pings bracket the await because the stall watchdog is
31
+ * a whole-turn interval comparing wall-clock against the last progress mark —
32
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
33
+ *
34
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
35
+ * one declared up front; keeping this inline made the discovered tool the one
36
+ * executor in the system that ran raw.
37
+ */
38
+ export function guardToolExecutor(name, execute, guards) {
39
+ return async (args, opts) => {
40
+ const invoke = () => Promise.resolve(execute(args, opts));
41
+ // The span wraps the CALL, not the guard: a timeout or an abort is a fact
42
+ // about this tool invocation and belongs inside its observation.
43
+ const wrapInSpan = guards.withToolSpan;
44
+ const call = wrapInSpan ? () => wrapInSpan(name, invoke) : invoke;
45
+ guards.onProgress?.();
46
+ try {
47
+ const raced = guards.abortSignal
48
+ ? raceWithAbort(call(), guards.abortSignal)
49
+ : call();
50
+ return await (guards.toolTimeoutMs === undefined
51
+ ? raced
52
+ : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
53
+ }
54
+ finally {
55
+ // In `finally`, not after a successful await: a tool that times out or
56
+ // throws has still consumed real time, and skipping the mark there would
57
+ // leave the watchdog measuring from before the call.
58
+ guards.onProgress?.();
59
+ }
60
+ };
61
+ }
@@ -9,7 +9,7 @@
9
9
  * providers so they can share a single implementation.
10
10
  */
11
11
  import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
12
- import type { Tool, GeminiToolExecutionGuards } from "../../types/index.js";
12
+ import type { Tool, ToolExecutionGuards } from "../../types/index.js";
13
13
  /**
14
14
  * A per-turn tool execute map that deduplicates identical tool calls.
15
15
  *
@@ -34,30 +34,6 @@ export declare class DedupExecuteMap extends Map<string, Tool["execute"]> {
34
34
  * This handles both Zod schemas and plain JSON Schema objects for tool parameters.
35
35
  */
36
36
  export declare function buildNativeToolDeclarations(tools: Record<string, Tool>, reservedNames?: ReadonlySet<string>): NativeToolDeclarationsResult;
37
- /**
38
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
39
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
40
- * discovered tools into the live record between steps; without this refresh
41
- * they stay invisible to the rest of the turn and every call dies as
42
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
43
- * `toolsConfig` by reference — and returns true when anything was added.
44
- */
45
- /**
46
- * Everything a native Gemini loop wraps around a tool call that the shared
47
- * engine does not do itself.
48
- *
49
- * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
50
- * abort is observed the moment it fires rather than after the tool settles,
51
- * and the timeout still bounds a tool that neither settles nor honours its
52
- * signal. The progress pings bracket the await because the stall watchdog is
53
- * a whole-turn interval comparing wall-clock against the last progress mark —
54
- * without them a legitimately slow tool reads as a stalled turn and is killed.
55
- *
56
- * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
57
- * one declared up front; keeping this inline made the discovered tool the one
58
- * executor in the system that ran raw.
59
- */
60
- export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: GeminiToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
61
37
  /**
62
38
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
63
39
  * DedupExecuteMap.
@@ -75,7 +51,7 @@ export declare function guardToolExecutor(name: string, execute: NonNullable<Too
75
51
  * identical in every test that calls a tool once, and silently reintroduces
76
52
  * duplicate side effects the moment the model repeats itself.
77
53
  */
78
- export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: GeminiToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
54
+ export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: ToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
79
55
  export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): string[];
80
56
  /**
81
57
  * Build the native @google/genai config object shared by stream and generate.
@@ -14,11 +14,11 @@ import { extname } from "node:path";
14
14
  import { DEFAULT_CONTEXT_GUARD_RATIO, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../../core/constants.js";
15
15
  import { needsAudioTranscode, toProviderCompatibleAudio, } from "../../adapters/audioFormatSupport.js";
16
16
  import { logger } from "../../utils/logger.js";
17
+ import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
17
18
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
18
19
  import { convertZodToJsonSchema, ensureNestedSchemaTypes, inlineJsonSchema, isZodSchema, } from "../../utils/schemaConversion.js";
19
20
  import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
20
21
  import { resolveLiveTool } from "../../tools/toolDiscovery.js";
21
- import { raceWithAbort, withTimeout } from "../../utils/async/index.js";
22
22
  // ── Functions ──
23
23
  /** Stable, key-order-independent serialization of tool args for the dedup key. */
24
24
  function stableStringifyForDedup(value) {
@@ -324,53 +324,6 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
324
324
  originalNameMap,
325
325
  };
326
326
  }
327
- /**
328
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
329
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
330
- * discovered tools into the live record between steps; without this refresh
331
- * they stay invisible to the rest of the turn and every call dies as
332
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
333
- * `toolsConfig` by reference — and returns true when anything was added.
334
- */
335
- /**
336
- * Everything a native Gemini loop wraps around a tool call that the shared
337
- * engine does not do itself.
338
- *
339
- * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
340
- * abort is observed the moment it fires rather than after the tool settles,
341
- * and the timeout still bounds a tool that neither settles nor honours its
342
- * signal. The progress pings bracket the await because the stall watchdog is
343
- * a whole-turn interval comparing wall-clock against the last progress mark —
344
- * without them a legitimately slow tool reads as a stalled turn and is killed.
345
- *
346
- * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
347
- * one declared up front; keeping this inline made the discovered tool the one
348
- * executor in the system that ran raw.
349
- */
350
- export function guardToolExecutor(name, execute, guards) {
351
- return async (args, opts) => {
352
- const invoke = () => Promise.resolve(execute(args, opts));
353
- // The span wraps the CALL, not the guard: a timeout or an abort is a fact
354
- // about this tool invocation and belongs inside its observation.
355
- const wrapInSpan = guards.withToolSpan;
356
- const call = wrapInSpan ? () => wrapInSpan(name, invoke) : invoke;
357
- guards.onProgress?.();
358
- try {
359
- const raced = guards.abortSignal
360
- ? raceWithAbort(call(), guards.abortSignal)
361
- : call();
362
- return await (guards.toolTimeoutMs === undefined
363
- ? raced
364
- : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
365
- }
366
- finally {
367
- // In `finally`, not after a successful await: a tool that times out or
368
- // throws has still consumed real time, and skipping the mark there would
369
- // leave the watchdog measuring from before the call.
370
- guards.onProgress?.();
371
- }
372
- };
373
- }
374
327
  /**
375
328
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
376
329
  * DedupExecuteMap.
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable max-lines-per-function */
2
2
  // Native SDK imports - no more @ai-sdk/google-vertex dependency
3
3
  import fs from "fs";
4
+ import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
4
5
  import path from "path";
5
6
  import { ErrorCategory, ErrorSeverity, } from "../../constants/enums.js";
6
7
  import { BaseProvider } from "../../core/baseProvider.js";
@@ -33,7 +34,6 @@ import { appendStepText, buildAbortedTurnMessage, buildContextCapMessage, buildD
33
34
  import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
34
35
  import { runAgenticLoop } from "../../core/loopEngine.js";
35
36
  import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
36
- import { guardToolExecutor } from "../googleNativeGemini3/utils.js";
37
37
  import { extractMcpToolErrorMessage } from "../../utils/mcpErrorText.js";
38
38
  import { createStreamChannel } from "../../core/streamChannel.js";
39
39
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
@@ -289,16 +289,22 @@ export type AnthropicLoopAdapterConfig<TMessage = Anthropic.Messages.MessagePara
289
289
  abortSignal?: AbortSignal;
290
290
  };
291
291
  /**
292
- * The three things a native Gemini loop wraps around every tool call that the
293
- * shared engine does not do itself.
292
+ * The things a native loop wraps around every tool call that the shared engine
293
+ * does not do itself.
294
294
  *
295
- * All optional, and the whole object is optional, because the two Gemini
296
- * providers differ here: Vertex bounds tool execution and runs a stall
297
- * watchdog, AI Studio does neither. Passing nothing leaves an executor exactly
298
- * as the caller supplied it, so this cannot quietly give AI Studio behaviour
299
- * its hand-rolled loops never had.
295
+ * NOT Gemini-specific, despite where this started. Both Vertex+Claude loops
296
+ * apply these guards directly via `guardToolExecutor`, and the Gemini adapter
297
+ * applies them to hydrated tools, so the former `GeminiToolExecutionGuards`
298
+ * name described the first caller rather than the contract and pointed the
299
+ * next reader at the wrong provider family.
300
+ *
301
+ * All optional, and the whole object is optional, because callers differ:
302
+ * Vertex bounds tool execution and runs a stall watchdog, AI Studio does
303
+ * neither. Passing nothing leaves an executor exactly as the caller supplied
304
+ * it, so this cannot quietly give a provider behaviour its hand-rolled loops
305
+ * never had.
300
306
  */
301
- export type GeminiToolExecutionGuards = {
307
+ export type ToolExecutionGuards = {
302
308
  /** Upper bound on a single execute(); omit for no bound. */
303
309
  toolTimeoutMs?: number;
304
310
  /**
@@ -328,6 +334,13 @@ export type GeminiToolExecutionGuards = {
328
334
  */
329
335
  withToolSpan?: <T>(name: string, run: () => Promise<T>) => Promise<T>;
330
336
  };
337
+ /**
338
+ * @deprecated Renamed to `ToolExecutionGuards` — the guards were never
339
+ * Gemini-specific. Kept because this name is re-exported from the package root
340
+ * via the types barrel, so removing it outright would break any consumer that
341
+ * imports it (CLAUDE.md rule 5). Safe to drop at the next major.
342
+ */
343
+ export type GeminiToolExecutionGuards = ToolExecutionGuards;
331
344
  /** What one Gemini step produced, carried to `buildToolResultMessages`. */
332
345
  export type GeminiStepRaw = {
333
346
  rawResponseParts: unknown[];
@@ -394,7 +407,7 @@ export type GeminiLoopAdapterCoreConfig = {
394
407
  * the opposite of what discovery is for — the tool the model just found is
395
408
  * the one most likely to be called repeatedly with the same arguments.
396
409
  */
397
- toolGuards?: GeminiToolExecutionGuards;
410
+ toolGuards?: ToolExecutionGuards;
398
411
  /**
399
412
  * Name of the terminal structured-output tool when one is in play. A call
400
413
  * to it ends the turn: its arguments ARE the answer, so it is reported as
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { open, readFile, realpath } from "fs/promises";
7
7
  import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
8
- import { getGlobalDispatcher, interceptors, request } from "undici";
8
+ import { request } from "undici";
9
+ import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
9
10
  // Lazy-loaded processor singletons — avoids loading heavy media deps
10
11
  // (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
11
12
  async function getVideoProcessor() {
@@ -1519,7 +1520,7 @@ export class FileDetector {
1519
1520
  if (getCachedUrlContentType(url, Date.now()) === undefined) {
1520
1521
  try {
1521
1522
  const head = await request(url, {
1522
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
1523
+ dispatcher: redirectFollowingDispatcher(5),
1523
1524
  method: "HEAD",
1524
1525
  headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
1525
1526
  bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
@@ -1550,7 +1551,7 @@ export class FileDetector {
1550
1551
  return withRetry(async () => {
1551
1552
  try {
1552
1553
  const response = await request(url, {
1553
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
1554
+ dispatcher: redirectFollowingDispatcher(5),
1554
1555
  method: "GET",
1555
1556
  headersTimeout: timeout,
1556
1557
  bodyTimeout: timeout,
@@ -2283,7 +2284,7 @@ class MimeTypeStrategy {
2283
2284
  // dump() can't hang detection, per the project's async-timeout guideline.
2284
2285
  contentType = await withTimeout((async () => {
2285
2286
  const response = await request(input, {
2286
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
2287
+ dispatcher: redirectFollowingDispatcher(5),
2287
2288
  method: "HEAD",
2288
2289
  headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
2289
2290
  bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, statSync } from "fs";
2
2
  import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
3
- import { getGlobalDispatcher, interceptors, request } from "undici";
3
+ import { request } from "undici";
4
+ import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
4
5
  import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
5
6
  import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
6
7
  import { getAvailableInputTokens } from "../constants/contextWindows.js";
@@ -1660,7 +1661,7 @@ async function downloadImageFromUrl(url) {
1660
1661
  await urlDownloadRateLimiter.acquire();
1661
1662
  try {
1662
1663
  const response = await request(url, {
1663
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
1664
+ dispatcher: redirectFollowingDispatcher(5),
1664
1665
  method: "GET",
1665
1666
  headersTimeout: 10000, // 10 second timeout for headers
1666
1667
  bodyTimeout: 30000, // 30 second timeout for body,
@@ -0,0 +1,28 @@
1
+ import type { Dispatcher } from "undici";
2
+ /**
3
+ * A dispatcher that follows redirects, when composing one is safe here.
4
+ *
5
+ * `getGlobalDispatcher()` returns Node's **built-in** undici dispatcher, whose
6
+ * major tracks the runtime rather than this package's dependency. The composed
7
+ * result is then passed to the **npm** undici's `request()`, and the two majors
8
+ * do not share a handler contract:
9
+ *
10
+ * node 24 built-in 7.24.4 + npm 7.28.0 request() succeeds
11
+ * node 22 built-in 6.28.0 + npm 7.28.0 throws "invalid onError method"
12
+ *
13
+ * Node 22 is this package's declared minimum, so the broken combination is not
14
+ * exotic — it is the floor. The throw happens at request time rather than at
15
+ * compose(), which is why it surfaces as an opaque runtime error instead of
16
+ * something recognisably about versions.
17
+ *
18
+ * When the majors disagree, return the global dispatcher uncomposed. That drops
19
+ * redirect-following from the pre-flight HEAD only. Callers already treat any
20
+ * non-2xx HEAD — a redirect included — as untrustworthy and fall through to the
21
+ * streaming size guard on the GET, so the size protection is unchanged and the
22
+ * cost is one extra round trip.
23
+ *
24
+ * Composing onto the global dispatcher rather than a fresh `Agent` is
25
+ * deliberate: it preserves whatever the host application configured globally,
26
+ * such as a corporate ProxyAgent.
27
+ */
28
+ export declare function redirectFollowingDispatcher(maxRedirections: number): Dispatcher;
@@ -0,0 +1,43 @@
1
+ import { getGlobalDispatcher, interceptors } from "undici";
2
+ /**
3
+ * The major of the `undici` this package depends on.
4
+ *
5
+ * `dependencies.undici` is `>=7.24.0 <8.0.0`, and `pnpm.overrides` maps
6
+ * `undici@>=8.0.0` back into that same range, so major 7 is a declared
7
+ * invariant rather than an observation. Update both together if it ever moves.
8
+ */
9
+ const NPM_UNDICI_MAJOR = 7;
10
+ /**
11
+ * A dispatcher that follows redirects, when composing one is safe here.
12
+ *
13
+ * `getGlobalDispatcher()` returns Node's **built-in** undici dispatcher, whose
14
+ * major tracks the runtime rather than this package's dependency. The composed
15
+ * result is then passed to the **npm** undici's `request()`, and the two majors
16
+ * do not share a handler contract:
17
+ *
18
+ * node 24 built-in 7.24.4 + npm 7.28.0 request() succeeds
19
+ * node 22 built-in 6.28.0 + npm 7.28.0 throws "invalid onError method"
20
+ *
21
+ * Node 22 is this package's declared minimum, so the broken combination is not
22
+ * exotic — it is the floor. The throw happens at request time rather than at
23
+ * compose(), which is why it surfaces as an opaque runtime error instead of
24
+ * something recognisably about versions.
25
+ *
26
+ * When the majors disagree, return the global dispatcher uncomposed. That drops
27
+ * redirect-following from the pre-flight HEAD only. Callers already treat any
28
+ * non-2xx HEAD — a redirect included — as untrustworthy and fall through to the
29
+ * streaming size guard on the GET, so the size protection is unchanged and the
30
+ * cost is one extra round trip.
31
+ *
32
+ * Composing onto the global dispatcher rather than a fresh `Agent` is
33
+ * deliberate: it preserves whatever the host application configured globally,
34
+ * such as a corporate ProxyAgent.
35
+ */
36
+ export function redirectFollowingDispatcher(maxRedirections) {
37
+ const globalDispatcher = getGlobalDispatcher();
38
+ const builtinMajor = Number.parseInt(process.versions.undici?.split(".")[0] ?? "", 10);
39
+ if (!Number.isFinite(builtinMajor) || builtinMajor !== NPM_UNDICI_MAJOR) {
40
+ return globalDispatcher;
41
+ }
42
+ return globalDispatcher.compose(interceptors.redirect({ maxRedirections }));
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.18.2",
3
+ "version": "11.18.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": {