@juspay/neurolink 11.18.1 → 11.18.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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +358 -358
- package/dist/cli/commands/proxy.js +15 -2
- package/dist/cli/proxy-clients/snapshot.js +6 -0
- package/dist/proxy/geminiFormat.d.ts +25 -4
- package/dist/proxy/geminiFormat.js +55 -15
- package/dist/proxy/proxyTranslationEngine.js +1 -1
- package/dist/server/routes/geminiProxyRoutes.js +6 -1
- package/dist/types/proxy.d.ts +9 -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
|
@@ -895,6 +895,11 @@ function printProxyBanner(url, strategy) {
|
|
|
895
895
|
logger.always(chalk.bold("Endpoints:"));
|
|
896
896
|
logger.always(` ${chalk.blue("POST")} /v1/messages — Claude proxy (Anthropic format)`);
|
|
897
897
|
logger.always(` ${chalk.blue("POST")} /v1/chat/completions — OpenAI-compatible proxy`);
|
|
898
|
+
// The banner listed two of the four inbound doors, so the Codex and Gemini
|
|
899
|
+
// CLIs looked unsupported to anyone reading start-up output rather than the
|
|
900
|
+
// docs. Every door the proxy actually answers on belongs here.
|
|
901
|
+
logger.always(` ${chalk.blue("POST")} /backend-api/codex/… — Codex proxy (Responses format)`);
|
|
902
|
+
logger.always(` ${chalk.blue("POST")} /v1beta/models/… — Gemini proxy (generateContent)`);
|
|
898
903
|
logger.always(` ${chalk.green("GET")} /health — Health check`);
|
|
899
904
|
logger.always(` ${chalk.green("GET")} /status — Detailed status`);
|
|
900
905
|
logger.always("");
|
|
@@ -1149,9 +1154,17 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1149
1154
|
throw error;
|
|
1150
1155
|
}
|
|
1151
1156
|
};
|
|
1152
|
-
// Cover
|
|
1153
|
-
//
|
|
1157
|
+
// Cover every inbound door so drain/reject, lifecycle logging, and
|
|
1158
|
+
// concurrency accounting apply to all of them.
|
|
1159
|
+
//
|
|
1160
|
+
// `/v1beta/*` is listed separately on purpose: Hono matches wildcards a path
|
|
1161
|
+
// segment at a time, so `/v1/*` does NOT cover `/v1beta/models/...` — the
|
|
1162
|
+
// segment is `v1beta`, not `v1`. When the Gemini door landed it inherited
|
|
1163
|
+
// neither tracker, which meant its requests were absent from the request
|
|
1164
|
+
// log, from per-CLI attribution, and from the in-flight count the graceful
|
|
1165
|
+
// drain waits on. An update could therefore cut a live Gemini stream.
|
|
1154
1166
|
app.use("/v1/*", trackingHandler);
|
|
1167
|
+
app.use("/v1beta/*", trackingHandler);
|
|
1155
1168
|
app.use("/backend-api/*", trackingHandler);
|
|
1156
1169
|
}
|
|
1157
1170
|
export async function createProxyStartApp(params) {
|
|
@@ -169,6 +169,12 @@ export async function writeFileAtomic(filePath, contents, mode) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
try {
|
|
172
|
+
// The temp file is a sibling of the destination, so a missing parent fails
|
|
173
|
+
// the write rather than the rename — the config is untouched, but the
|
|
174
|
+
// caller sees an ENOENT naming a path it never asked to write. Creating
|
|
175
|
+
// the directory first makes a first-run write behave like the plain
|
|
176
|
+
// writeFileSync it replaced.
|
|
177
|
+
fs.mkdirSync(dirname(filePath), { recursive: true });
|
|
172
178
|
fs.writeFileSync(tempPath, contents, { mode: effectiveMode });
|
|
173
179
|
stage = "chmod";
|
|
174
180
|
fs.chmodSync(tempPath, effectiveMode);
|
|
@@ -22,17 +22,38 @@ import type { ParsedGeminiRequest, StreamSerializerAdapter } from "../types/inde
|
|
|
22
22
|
/**
|
|
23
23
|
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
24
24
|
*
|
|
25
|
-
* The final user turn becomes `prompt
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
25
|
+
* The final user turn becomes `prompt`, with Google's `model` role mapped to
|
|
26
|
+
* `assistant` so downstream providers see a role they understand.
|
|
27
|
+
*
|
|
28
|
+
* `conversationMessages` carries EVERY turn, the final one included. That
|
|
29
|
+
* looks redundant next to `prompt`, and it is the contract the shared engine
|
|
30
|
+
* expects: `buildTranslationOptions` does `conversationMessages.slice(0, -1)`
|
|
31
|
+
* to derive history, because the final turn is already being sent as `prompt`.
|
|
32
|
+
* `claudeFormat` and `openaiFormat` both push unconditionally for that reason.
|
|
33
|
+
* Excluding the last turn here — the intuitive reading of "history" — made the
|
|
34
|
+
* engine's slice eat one real turn instead, so every multi-turn Gemini request
|
|
35
|
+
* silently lost its most recent message.
|
|
28
36
|
*/
|
|
29
37
|
export declare function parseGeminiRequest(model: string, body: Record<string, unknown>, stream: boolean): ParsedGeminiRequest;
|
|
30
38
|
/** Build a complete `generateContent` response body. */
|
|
39
|
+
/**
|
|
40
|
+
* Render a tool call as text.
|
|
41
|
+
*
|
|
42
|
+
* The proxy does not forward tool calls in Google's `functionCall` part shape:
|
|
43
|
+
* the CLI drives tools locally, so a `functionCall` it never asked for would be
|
|
44
|
+
* an unresolvable pending call. Text is what it can act on. Both the streaming
|
|
45
|
+
* serializer and the non-streaming builder go through here so the two paths
|
|
46
|
+
* cannot drift.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderGeminiToolUse(name: string, input: unknown): string;
|
|
31
49
|
export declare function buildGeminiResponse(text: string, finishReason: string, usage: {
|
|
32
50
|
input: number;
|
|
33
51
|
output: number;
|
|
34
52
|
total: number;
|
|
35
|
-
}, modelVersion: string
|
|
53
|
+
}, modelVersion: string, toolCalls?: ReadonlyArray<{
|
|
54
|
+
toolName: string;
|
|
55
|
+
args: Record<string, unknown>;
|
|
56
|
+
}>): Record<string, unknown>;
|
|
36
57
|
/** Google's error envelope, which the CLI parses to classify failures. */
|
|
37
58
|
export declare function buildGeminiErrorResponse(status: number, message: string, statusText?: string): Response;
|
|
38
59
|
/**
|
|
@@ -40,9 +40,17 @@ function partsToImages(parts) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Parse a `generateContent` body into the shape the translation engine takes.
|
|
42
42
|
*
|
|
43
|
-
* The final user turn becomes `prompt
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
43
|
+
* The final user turn becomes `prompt`, with Google's `model` role mapped to
|
|
44
|
+
* `assistant` so downstream providers see a role they understand.
|
|
45
|
+
*
|
|
46
|
+
* `conversationMessages` carries EVERY turn, the final one included. That
|
|
47
|
+
* looks redundant next to `prompt`, and it is the contract the shared engine
|
|
48
|
+
* expects: `buildTranslationOptions` does `conversationMessages.slice(0, -1)`
|
|
49
|
+
* to derive history, because the final turn is already being sent as `prompt`.
|
|
50
|
+
* `claudeFormat` and `openaiFormat` both push unconditionally for that reason.
|
|
51
|
+
* Excluding the last turn here — the intuitive reading of "history" — made the
|
|
52
|
+
* engine's slice eat one real turn instead, so every multi-turn Gemini request
|
|
53
|
+
* silently lost its most recent message.
|
|
46
54
|
*/
|
|
47
55
|
export function parseGeminiRequest(model, body, stream) {
|
|
48
56
|
const contents = Array.isArray(body.contents)
|
|
@@ -58,9 +66,9 @@ export function parseGeminiRequest(model, body, stream) {
|
|
|
58
66
|
content: partsToText(c?.parts),
|
|
59
67
|
images: partsToImages(c?.parts),
|
|
60
68
|
}));
|
|
61
|
-
// The last user turn is the prompt
|
|
62
|
-
//
|
|
63
|
-
//
|
|
69
|
+
// The last user turn is the prompt. A request whose final turn is a model
|
|
70
|
+
// turn (the CLI does this when continuing) leaves an empty prompt rather
|
|
71
|
+
// than replaying the assistant's own words as input.
|
|
64
72
|
let prompt = "";
|
|
65
73
|
let images = [];
|
|
66
74
|
const conversationMessages = [];
|
|
@@ -70,12 +78,24 @@ export function parseGeminiRequest(model, body, stream) {
|
|
|
70
78
|
prompt = turns[i].content;
|
|
71
79
|
images = turns[i].images;
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// Unconditional — see the slice-contract note on this function.
|
|
82
|
+
conversationMessages.push({
|
|
83
|
+
role: turns[i].role,
|
|
84
|
+
content: turns[i].content,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// The engine's `slice(0, -1)` drops the LAST entry on the assumption that it
|
|
88
|
+
// is the turn already being sent as `prompt`. That holds only when the
|
|
89
|
+
// request ends with a user turn. The Gemini CLI also continues from a model
|
|
90
|
+
// turn, and there the last entry is a real assistant reply — so the slice ate
|
|
91
|
+
// it, which is the same lost-turn bug one case further along.
|
|
92
|
+
//
|
|
93
|
+
// A terminal placeholder restores the invariant: the slice removes this
|
|
94
|
+
// instead of the model turn. It is never sent anywhere — `prompt` is
|
|
95
|
+
// independently "" in exactly this case, so the placeholder only exists to be
|
|
96
|
+
// consumed by the slice.
|
|
97
|
+
if (turns.length > 0 && turns[turns.length - 1].role !== "user") {
|
|
98
|
+
conversationMessages.push({ role: "user", content: "" });
|
|
79
99
|
}
|
|
80
100
|
const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
81
101
|
const stops = generationConfig.stopSequences;
|
|
@@ -115,11 +135,31 @@ function usageMetadata(usage) {
|
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
137
|
/** Build a complete `generateContent` response body. */
|
|
118
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Render a tool call as text.
|
|
140
|
+
*
|
|
141
|
+
* The proxy does not forward tool calls in Google's `functionCall` part shape:
|
|
142
|
+
* the CLI drives tools locally, so a `functionCall` it never asked for would be
|
|
143
|
+
* an unresolvable pending call. Text is what it can act on. Both the streaming
|
|
144
|
+
* serializer and the non-streaming builder go through here so the two paths
|
|
145
|
+
* cannot drift.
|
|
146
|
+
*/
|
|
147
|
+
export function renderGeminiToolUse(name, input) {
|
|
148
|
+
return `\n[tool: ${name} ${JSON.stringify(input)}]\n`;
|
|
149
|
+
}
|
|
150
|
+
export function buildGeminiResponse(text, finishReason, usage, modelVersion, toolCalls) {
|
|
151
|
+
// A translated result can legitimately carry tool calls and no text — the
|
|
152
|
+
// engine's hasTranslatedOutput() accepts that. Rendering only `text` there
|
|
153
|
+
// handed the client parts[0].text === "" with finishReason STOP, which reads
|
|
154
|
+
// as "the model answered nothing" rather than "the model wants a tool".
|
|
155
|
+
const rendered = (toolCalls ?? [])
|
|
156
|
+
.map((call) => renderGeminiToolUse(call.toolName, call.args))
|
|
157
|
+
.join("");
|
|
158
|
+
const body = `${text}${rendered}`;
|
|
119
159
|
return {
|
|
120
160
|
candidates: [
|
|
121
161
|
{
|
|
122
|
-
content: { role: MODEL_ROLE, parts: [{ text }] },
|
|
162
|
+
content: { role: MODEL_ROLE, parts: [{ text: body }] },
|
|
123
163
|
finishReason: toGeminiFinishReason(finishReason),
|
|
124
164
|
index: 0,
|
|
125
165
|
},
|
|
@@ -175,7 +215,7 @@ export class GeminiStreamSerializer {
|
|
|
175
215
|
* that is never coming; rendering it as text keeps the turn terminating.
|
|
176
216
|
*/
|
|
177
217
|
pushToolUse(_id, name, input) {
|
|
178
|
-
return this.pushDelta(
|
|
218
|
+
return this.pushDelta(renderGeminiToolUse(name, input));
|
|
179
219
|
}
|
|
180
220
|
finish(finishReason, usage) {
|
|
181
221
|
return [
|
|
@@ -598,7 +598,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
598
598
|
return serializeClaudeResponse(internal, requestModel);
|
|
599
599
|
}
|
|
600
600
|
if (format === "gemini") {
|
|
601
|
-
return buildGeminiResponse(internal.content, internal.finishReason ?? defaultFinishReason(format), resolvedUsage, internal.model ?? requestModel);
|
|
601
|
+
return buildGeminiResponse(internal.content, internal.finishReason ?? defaultFinishReason(format), resolvedUsage, internal.model ?? requestModel, internal.toolCalls);
|
|
602
602
|
}
|
|
603
603
|
return serializeOpenAIResponse(internal, requestModel);
|
|
604
604
|
}
|
|
@@ -184,7 +184,12 @@ export function createGeminiProxyRoutes(modelRouter, basePath = "", _loopbackPor
|
|
|
184
184
|
// --- Dispatch via shared translation engine ---
|
|
185
185
|
try {
|
|
186
186
|
if (stream) {
|
|
187
|
-
|
|
187
|
+
// Awaited, not returned bare: `handleTranslatedStreamRequest` is
|
|
188
|
+
// async, so a rejection raised before the Response exists would
|
|
189
|
+
// escape this try/catch and land in `app.onError`, which answers
|
|
190
|
+
// in Anthropic's error shape. A Gemini client parsing that finds
|
|
191
|
+
// no `error.message` and reports an empty failure.
|
|
192
|
+
return await handleTranslatedStreamRequest({
|
|
188
193
|
ctx,
|
|
189
194
|
format: "gemini",
|
|
190
195
|
requestModel: modelId,
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -2773,15 +2773,6 @@ export type OpenAIErrorResponse = {
|
|
|
2773
2773
|
};
|
|
2774
2774
|
};
|
|
2775
2775
|
/** Parsed OpenAI request — intermediate form for NeuroLink pipeline. */
|
|
2776
|
-
/**
|
|
2777
|
-
* A Gemini `generateContent` request, reduced to what translation needs.
|
|
2778
|
-
*
|
|
2779
|
-
* Google's shape differs from both others in three ways that matter here:
|
|
2780
|
-
* roles are `user`/`model` rather than `user`/`assistant`, the system prompt
|
|
2781
|
-
* lives in a sibling `systemInstruction` rather than in the turn list, and
|
|
2782
|
-
* generation settings are nested under `generationConfig` instead of sitting
|
|
2783
|
-
* at the top level.
|
|
2784
|
-
*/
|
|
2785
2776
|
/** One part of a Gemini `contents[].parts[]` entry. */
|
|
2786
2777
|
export type ProxyGeminiPart = {
|
|
2787
2778
|
text?: string;
|
|
@@ -2794,6 +2785,15 @@ export type ProxyGeminiContent = {
|
|
|
2794
2785
|
role?: string;
|
|
2795
2786
|
parts?: ProxyGeminiPart[];
|
|
2796
2787
|
};
|
|
2788
|
+
/**
|
|
2789
|
+
* A Gemini `generateContent` request, reduced to what translation needs.
|
|
2790
|
+
*
|
|
2791
|
+
* Google's shape differs from both others in three ways that matter here:
|
|
2792
|
+
* roles are `user`/`model` rather than `user`/`assistant`, the system prompt
|
|
2793
|
+
* lives in a sibling `systemInstruction` rather than in the turn list, and
|
|
2794
|
+
* generation settings are nested under `generationConfig` instead of sitting
|
|
2795
|
+
* at the top level.
|
|
2796
|
+
*/
|
|
2797
2797
|
export type ParsedGeminiRequest = {
|
|
2798
2798
|
model: string;
|
|
2799
2799
|
maxTokens?: number;
|
|
@@ -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.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": {
|