@juspay/neurolink 12.35.1 → 12.35.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.
@@ -13,7 +13,6 @@ import { claudeOutputLogFields, observeClaudeJsonOutput, observeClaudeStreamOutp
13
13
  import { access, readFile } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
- import { Agent } from "undici";
17
16
  import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
18
17
  import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
19
18
  import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
@@ -184,6 +183,50 @@ function prepareAnthropicWire(ctx, bodyStr) {
184
183
  };
185
184
  }
186
185
  let anthropicUpstreamDispatcher;
186
+ let anthropicUpstreamDispatcherPromise;
187
+ /**
188
+ * Lazily construct (once) and cache the upstream Agent, reused across every
189
+ * request rather than rebuilt per call.
190
+ *
191
+ * `undici` is imported dynamically here — rather than as a static top-level
192
+ * import — so that requiring this module (and transitively, the package
193
+ * entry that re-exports `createClaudeProxyRoutes`) does not force-load undici
194
+ * until an upstream Anthropic request actually dispatches. The construction
195
+ * itself is guarded by a cached promise, not a bare `??=` on the Agent, so
196
+ * concurrent first callers await the same in-flight construction instead of
197
+ * each racing past the `undefined` check and building a duplicate Agent. A
198
+ * rejected construction (a bad undici install, an Agent constructor throwing)
199
+ * clears the cached promise rather than leaving it permanently rejected, so
200
+ * the next request gets a fresh attempt instead of every later request
201
+ * failing forever on a cached failure. See
202
+ * test/continuous-test-suite-import-cost.ts.
203
+ */
204
+ async function getAnthropicUpstreamDispatcher() {
205
+ if (anthropicUpstreamDispatcher) {
206
+ return anthropicUpstreamDispatcher;
207
+ }
208
+ anthropicUpstreamDispatcherPromise ??= (async () => {
209
+ try {
210
+ const { Agent: UndiciAgent } = await import("undici");
211
+ // Node's global fetch applies Undici's 300s default headers timeout
212
+ // before the route's 15-minute abort signal. Keep both transport
213
+ // deadlines aligned with the proxy contract.
214
+ const agent = new UndiciAgent({
215
+ headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
216
+ bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
217
+ });
218
+ anthropicUpstreamDispatcher = agent;
219
+ return agent;
220
+ }
221
+ catch (error) {
222
+ // Let a later request try again instead of every subsequent request
223
+ // failing forever against a cached rejection.
224
+ anthropicUpstreamDispatcherPromise = undefined;
225
+ throw error;
226
+ }
227
+ })();
228
+ return anthropicUpstreamDispatcherPromise;
229
+ }
187
230
  async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
188
231
  clearClaudeFallbackTerminalEvidence(ctx);
189
232
  const prepared = dispatch.preparedContext;
@@ -203,15 +246,8 @@ async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
203
246
  ctx.metadata.tokenBudget = lease.snapshot;
204
247
  throw init.signal?.reason ?? ctx.abortSignal?.reason;
205
248
  }
206
- // Node's global fetch applies Undici's 300s default headers timeout before
207
- // the route's 15-minute abort signal. Keep both transport deadlines aligned
208
- // with the proxy contract and instantiate lazily so importing routes has no
209
- // open transport handles.
210
- anthropicUpstreamDispatcher ??= new Agent({
211
- headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
212
- bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
213
- });
214
249
  try {
250
+ const upstreamDispatcher = await getAnthropicUpstreamDispatcher();
215
251
  const dispatchedBody = String(init.body);
216
252
  recordAttempt(dispatch.accountLabel, dispatch.accountType);
217
253
  dispatch.tracer?.logUpstreamRequestHeaders(init.headers);
@@ -230,7 +266,7 @@ async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
230
266
  const response = await fetch(url, {
231
267
  ...init,
232
268
  body: dispatchedBody,
233
- dispatcher: anthropicUpstreamDispatcher,
269
+ dispatcher: upstreamDispatcher,
234
270
  });
235
271
  return observeAnthropicBudgetResponse(response, lease, () => {
236
272
  ctx.metadata.tokenBudget = lease.snapshot;
@@ -6,7 +6,6 @@
6
6
  import { open, readFile, realpath } from "fs/promises";
7
7
  import { STATUS_CODES } from "node:http";
8
8
  import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
9
- import { request } from "undici";
10
9
  import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
11
10
  // Lazy-loaded processor singletons — avoids loading heavy media deps
12
11
  // (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
@@ -1609,8 +1608,13 @@ export class FileDetector {
1609
1608
  const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
1610
1609
  return withRetry(async () => {
1611
1610
  try {
1611
+ // Dynamic import keeps undici off the package's static import
1612
+ // graph — it is only needed once a URL-referenced attachment
1613
+ // actually has to be fetched. See
1614
+ // test/continuous-test-suite-import-cost.ts.
1615
+ const { request } = await import("undici");
1612
1616
  const response = await request(url, {
1613
- dispatcher: redirectFollowingDispatcher(5),
1617
+ dispatcher: await redirectFollowingDispatcher(5),
1614
1618
  method: "GET",
1615
1619
  headersTimeout: timeout,
1616
1620
  bodyTimeout: timeout,
@@ -2375,8 +2379,13 @@ class MimeTypeStrategy {
2375
2379
  // Wrap the whole HEAD (request + body drain) in withTimeout so a stalled
2376
2380
  // dump() can't hang detection, per the project's async-timeout guideline.
2377
2381
  contentType = await withTimeout((async () => {
2382
+ // Dynamic import keeps undici off the package's static import
2383
+ // graph — it is only needed once a URL-referenced attachment
2384
+ // actually has to be fetched. See
2385
+ // test/continuous-test-suite-import-cost.ts.
2386
+ const { request } = await import("undici");
2378
2387
  const response = await request(input, {
2379
- dispatcher: redirectFollowingDispatcher(5),
2388
+ dispatcher: await redirectFollowingDispatcher(5),
2380
2389
  method: "HEAD",
2381
2390
  headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
2382
2391
  bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "fs";
2
2
  import { open as openAsync, readFile as readFileAsync, stat as statAsync, } from "fs/promises";
3
3
  import pLimit from "p-limit";
4
- import { request } from "undici";
5
4
  import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
6
5
  import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
7
6
  import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
@@ -1780,8 +1779,12 @@ async function downloadImageFromUrl(url) {
1780
1779
  // Apply rate limiting only if cache missed
1781
1780
  await urlDownloadRateLimiter.acquire();
1782
1781
  try {
1782
+ // Dynamic import keeps undici off the package's static import graph —
1783
+ // it is only needed once a URL-referenced image actually has to be
1784
+ // fetched. See test/continuous-test-suite-import-cost.ts.
1785
+ const { request } = await import("undici");
1783
1786
  const response = await request(url, {
1784
- dispatcher: redirectFollowingDispatcher(5),
1787
+ dispatcher: await redirectFollowingDispatcher(5),
1785
1788
  method: "GET",
1786
1789
  headersTimeout: 10000, // 10 second timeout for headers
1787
1790
  bodyTimeout: 30000, // 30 second timeout for body,
@@ -34,6 +34,11 @@ import type { Dispatcher } from "undici";
34
34
  * requests. Losing a proxy on one Node version is recoverable; silently failing
35
35
  * every redirecting download is not.
36
36
  *
37
+ * `undici` itself is imported dynamically here (rather than at module scope)
38
+ * so that requiring this module — and transitively, messageBuilder.ts /
39
+ * fileDetector.ts — does not force-load undici until a URL attachment is
40
+ * actually fetched. See test/continuous-test-suite-import-cost.ts.
41
+ *
37
42
  * @param maxRedirections How many redirects to follow before giving up.
38
43
  */
39
- export declare function redirectFollowingDispatcher(maxRedirections: number): Dispatcher;
44
+ export declare function redirectFollowingDispatcher(maxRedirections: number): Promise<Dispatcher>;
@@ -1,4 +1,3 @@
1
- import { Agent, getGlobalDispatcher, interceptors } from "undici";
2
1
  /**
3
2
  * The major of the `undici` this package depends on.
4
3
  *
@@ -42,9 +41,15 @@ const NPM_UNDICI_MAJOR = 7;
42
41
  * requests. Losing a proxy on one Node version is recoverable; silently failing
43
42
  * every redirecting download is not.
44
43
  *
44
+ * `undici` itself is imported dynamically here (rather than at module scope)
45
+ * so that requiring this module — and transitively, messageBuilder.ts /
46
+ * fileDetector.ts — does not force-load undici until a URL attachment is
47
+ * actually fetched. See test/continuous-test-suite-import-cost.ts.
48
+ *
45
49
  * @param maxRedirections How many redirects to follow before giving up.
46
50
  */
47
- export function redirectFollowingDispatcher(maxRedirections) {
51
+ export async function redirectFollowingDispatcher(maxRedirections) {
52
+ const { Agent, getGlobalDispatcher, interceptors } = await import("undici");
48
53
  const builtinMajor = Number.parseInt(process.versions.undici?.split(".")[0] ?? "", 10);
49
54
  const base = Number.isFinite(builtinMajor) && builtinMajor === NPM_UNDICI_MAJOR
50
55
  ? getGlobalDispatcher()
@@ -16,7 +16,6 @@
16
16
  *
17
17
  * @module utils/safeFetch
18
18
  */
19
- import { Agent, fetch as undiciFetch } from "undici";
20
19
  import { readBoundedBuffer } from "./sizeGuard.js";
21
20
  import { validateAndResolveUrl } from "./ssrfGuard.js";
22
21
  const DEFAULT_TIMEOUT_MS = 60_000;
@@ -30,8 +29,14 @@ const DEFAULT_TIMEOUT_MS = 60_000;
30
29
  * the OS resolver prefers AAAA, that broke every safeDownload (Replicate /
31
30
  * Runway / Kling asset fetches) while plain curl of the same URL succeeded —
32
31
  * curl races both families (Happy Eyeballs); a one-address pin cannot.
32
+ *
33
+ * `undici` is imported dynamically (this function is only ever called from
34
+ * the already-async `safeDownload`) so that requiring this module does not
35
+ * force-load undici until a download actually happens. See
36
+ * test/continuous-test-suite-import-cost.ts.
33
37
  */
34
- function buildPinnedAgent(hostname, addresses) {
38
+ async function buildPinnedAgent(hostname, addresses) {
39
+ const { Agent } = await import("undici");
35
40
  const primary = addresses[0];
36
41
  if (!primary) {
37
42
  throw new Error(`safeFetch: no validated addresses to pin for "${hostname}"`);
@@ -85,7 +90,11 @@ export async function safeDownload(url, options) {
85
90
  const { url: validatedUrl, addresses } = await validateAndResolveUrl(url);
86
91
  const parsed = new URL(validatedUrl);
87
92
  const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
88
- const agent = buildPinnedAgent(hostname, addresses);
93
+ // Dynamic import keeps undici off the package's static import graph — it
94
+ // is only needed once a download actually happens. See
95
+ // test/continuous-test-suite-import-cost.ts.
96
+ const { fetch: undiciFetch } = await import("undici");
97
+ const agent = await buildPinnedAgent(hostname, addresses);
89
98
  const timeoutCtrl = new AbortController();
90
99
  const timeoutId = setTimeout(() => timeoutCtrl.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
91
100
  const composedSignal = options.signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.35.1",
3
+ "version": "12.35.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {