@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.
- package/CHANGELOG.md +1 -5
- package/dist/browser/neurolink.min.js +357 -357
- package/dist/core/geminiLoopAdapter.js +2 -1
- package/dist/core/toolExecutionGuards.d.ts +38 -0
- package/dist/core/toolExecutionGuards.js +61 -0
- package/dist/providers/googleNativeGemini3/utils.d.ts +2 -26
- package/dist/providers/googleNativeGemini3/utils.js +1 -48
- package/dist/providers/googleVertex/client.js +1 -1
- package/dist/types/loopEngine.d.ts +22 -9
- package/dist/utils/fileDetector.js +5 -4
- package/dist/utils/messageBuilder.js +3 -2
- package/dist/utils/redirectDispatcher.d.ts +28 -0
- package/dist/utils/redirectDispatcher.js +43 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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,
|
|
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?:
|
|
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
|
|
293
|
-
*
|
|
292
|
+
* The things a native loop wraps around every tool call that the shared engine
|
|
293
|
+
* does not do itself.
|
|
294
294
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
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
|
|
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?:
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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:
|
|
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.
|
|
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": {
|