@juspay/neurolink 12.9.3 → 12.9.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.
@@ -47,7 +47,7 @@
47
47
  "contextWindow": 24000,
48
48
  "vision": false,
49
49
  "status": "production",
50
- "description": "Llama 3.2 11B Vision Instruct — gated: Cloudflare returns 403 until the model's Community License is accepted once on the account, so it is unusable here and its vision flag is false despite being a vision model"
50
+ "description": "Llama 3.2 11B Vision Instruct — Meta Community License accepted on the account 2026-09-02, but Cloudflare's OpenAI-compatible endpoint accepts no image input for it: an image content part is rejected (400) and a top-level image is ignored ('There is no image'). The model reads images only through the native /ai/run prompt+image shape, which this OpenAI-compatible provider does not speak, so vision stays false here"
51
51
  },
52
52
  "@cf/mistral/mistral-7b-instruct-v0.2": {
53
53
  "enumMember": "MISTRAL_7B_INSTRUCT_V0_2",
@@ -114,8 +114,8 @@
114
114
  "method": "real chat call per id against the live account"
115
115
  },
116
116
  "liveMatrix": {
117
- "date": "2026-08-30",
118
- "result": "Live tool round-trip verified on @cf/meta/llama-3.1-8b-instruct-fast, both non-streaming and streaming, once messageContentFormat normalization was applied; before it the follow-up turn was rejected with HTTP 400 (content sent as array/null). Of the three ids that failed, only two are actually gone: mistral-7b-instruct-v0.2 ('No such model') and qwen1.5-14b-chat-awq ('deprecated on 2025-10-01'). llama-3.2-11b-vision-instruct is alive but gated behind a one-time Community License acceptance on the account, so it answers 403 and is left production with vision false rather than retired."
117
+ "date": "2026-09-02",
118
+ "result": "Live tool round-trip verified on @cf/meta/llama-3.1-8b-instruct-fast, both non-streaming and streaming, once messageContentFormat normalization was applied; before it the follow-up turn was rejected with HTTP 400 (content sent as array/null). Of the three ids that failed, only two are actually gone: mistral-7b-instruct-v0.2 ('No such model') and qwen1.5-14b-chat-awq ('deprecated on 2025-10-01'). llama-3.2-11b-vision-instruct is alive but gated behind a one-time Community License acceptance on the account, so it answers 403 and is left production with vision false rather than retired. Update 2026-09-02: the Community License was accepted on the account; plain chat on llama-3.2-11b-vision-instruct now returns 200 on both endpoints, and the native /ai/run prompt+image call describes a 64x64 test image correctly. The OpenAI-compatible endpoint still cannot carry an image (content-part array → 400, top-level image ignored), so the vision flag remains false for this provider."
119
119
  },
120
120
  "addedInPR": "https://github.com/juspay/neurolink/pull/1587"
121
121
  },
@@ -5,10 +5,13 @@ export declare class ProviderTransportCoordinator {
5
5
  private backoffUntil;
6
6
  private lastErrorCode;
7
7
  private lastTransportScope;
8
+ /** Whether the failure that degraded the transport happened before any
9
+ * request byte was sent. Waiters use it to pick their retry budget. */
10
+ private lastFailureConnectPhase;
8
11
  private probe;
9
12
  acquire(signal?: AbortSignal): Promise<ProxyProviderTransportPermit>;
10
13
  reportSuccess(permit: ProxyProviderTransportPermit): void;
11
- reportTransportFailure(errorCode: string | undefined, transportScope: "shared_provider_transport" | "connection_transport", permit: ProxyProviderTransportPermit): void;
14
+ reportTransportFailure(errorCode: string | undefined, transportScope: "shared_provider_transport" | "connection_transport", permit: ProxyProviderTransportPermit, connectPhase?: boolean): void;
12
15
  reportProbeAbandoned(permit: ProxyProviderTransportPermit): void;
13
16
  clear(): void;
14
17
  private wait;
@@ -7,6 +7,9 @@ export class ProviderTransportCoordinator {
7
7
  backoffUntil = 0;
8
8
  lastErrorCode = null;
9
9
  lastTransportScope = "shared_provider_transport";
10
+ /** Whether the failure that degraded the transport happened before any
11
+ * request byte was sent. Waiters use it to pick their retry budget. */
12
+ lastFailureConnectPhase = false;
10
13
  probe;
11
14
  async acquire(signal) {
12
15
  if (!this.degraded) {
@@ -31,6 +34,7 @@ export class ProviderTransportCoordinator {
31
34
  allowed: false,
32
35
  errorCode: this.lastErrorCode,
33
36
  transportScope: this.lastTransportScope,
37
+ connectPhase: this.lastFailureConnectPhase,
34
38
  };
35
39
  }
36
40
  let resolveProbe;
@@ -55,12 +59,13 @@ export class ProviderTransportCoordinator {
55
59
  this.backoffUntil = 0;
56
60
  this.lastErrorCode = null;
57
61
  this.lastTransportScope = "shared_provider_transport";
62
+ this.lastFailureConnectPhase = false;
58
63
  if (permit.probe && this.probe?.generation === permit.generation) {
59
64
  this.probe.resolve("recovered");
60
65
  this.probe = undefined;
61
66
  }
62
67
  }
63
- reportTransportFailure(errorCode, transportScope, permit) {
68
+ reportTransportFailure(errorCode, transportScope, permit, connectPhase = false) {
64
69
  if (!permit.allowed || permit.generation !== this.generation) {
65
70
  return;
66
71
  }
@@ -71,6 +76,7 @@ export class ProviderTransportCoordinator {
71
76
  this.backoffUntil = Date.now() + SHARED_TRANSPORT_BACKOFF_MS;
72
77
  this.lastErrorCode = errorCode ?? null;
73
78
  this.lastTransportScope = transportScope;
79
+ this.lastFailureConnectPhase = connectPhase;
74
80
  if (permit.probe && this.probe?.generation === permit.generation) {
75
81
  this.probe.resolve("failed");
76
82
  this.probe = undefined;
@@ -96,6 +102,7 @@ export class ProviderTransportCoordinator {
96
102
  this.backoffUntil = 0;
97
103
  this.lastErrorCode = null;
98
104
  this.lastTransportScope = "shared_provider_transport";
105
+ this.lastFailureConnectPhase = false;
99
106
  }
100
107
  async wait(ms, signal) {
101
108
  if (signal?.aborted) {
@@ -389,6 +389,20 @@ declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughA
389
389
  export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
390
390
  export declare function getOverloadRotationDelayMs(attemptNumber: number): number;
391
391
  declare function describeTransportError(error: unknown): string;
392
+ /**
393
+ * Whether a transport failure provably happened while connecting, before any
394
+ * byte of the request left the process.
395
+ *
396
+ * Node tags its connect errors with `syscall: "connect"`; with happy-eyeballs
397
+ * enabled (the default) the error is an AggregateError whose every entry is
398
+ * one such attempt, and undici wraps either as `fetch failed` with the
399
+ * original as `cause`. The code alone cannot make this call: an `ETIMEDOUT`
400
+ * from the connect timer and an `ETIMEDOUT` from a socket that went quiet
401
+ * after dispatch look identical by code, and only the first is safe to
402
+ * retry. Both proxies behind one lossy Wi-Fi uplink returned 116 terminal
403
+ * 502s in a day on exactly that ambiguity.
404
+ */
405
+ declare function isConnectPhaseNetworkError(error: unknown): boolean;
392
406
  /**
393
407
  * Determine whether a POST can be retried without risking duplicate provider
394
408
  * work. Only failures that prove connection establishment did not complete are
@@ -470,6 +484,9 @@ export declare const __testHooks: {
470
484
  clearLimitsRefreshStateForTests: () => void;
471
485
  setAccountDirectoryForTests: (override: ProxyAccountDirectoryOverride | null) => void;
472
486
  isRetryableNetworkError: typeof isRetryableNetworkError;
487
+ isConnectPhaseNetworkError: typeof isConnectPhaseNetworkError;
488
+ MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES: number;
489
+ clearProviderTransportCoordinatorForTests: () => void;
473
490
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
474
491
  getStreamFailureDetails: typeof getStreamFailureDetails;
475
492
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
@@ -109,6 +109,15 @@ let lastKnownAccountCount = 0;
109
109
  const MAX_AUTH_RETRIES = 5;
110
110
  const MAX_TRANSIENT_SAME_ACCOUNT_RETRIES = 2;
111
111
  const TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS = [250, 1_000];
112
+ /**
113
+ * Retry budget for failures that happened before any request byte was sent
114
+ * (a SYN lost on a lossy uplink). Nothing was dispatched, so each retry is
115
+ * free of duplicate-work risk; on a link losing one connect in five, four
116
+ * retries take the per-request failure rate from about 20% to under 1%.
117
+ * Delays follow TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS, clamped to its last
118
+ * entry.
119
+ */
120
+ const MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES = 4;
112
121
  const OVERLOAD_ACCOUNT_ROTATION_DELAYS_MS = [250, 500, 1_000, 2_000];
113
122
  const MAX_FALLBACK_NETWORK_RETRIES = 1;
114
123
  const FALLBACK_STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 1000;
@@ -5841,6 +5850,9 @@ function createAnthropicAttemptLogger(args) {
5841
5850
  ? { cacheReadTokens: extra.cacheReadTokens }
5842
5851
  : {}),
5843
5852
  ...(extra?.retryable !== undefined ? { retryable: extra.retryable } : {}),
5853
+ ...(extra?.connectPhase !== undefined
5854
+ ? { connectPhase: extra.connectPhase }
5855
+ : {}),
5844
5856
  ...(extra?.rateLimitKind ? { rateLimitKind: extra.rateLimitKind } : {}),
5845
5857
  ...(extra?.cooldownReason
5846
5858
  ? { cooldownReason: extra.cooldownReason }
@@ -6044,6 +6056,7 @@ async function fetchAnthropicAccountResponse(args) {
6044
6056
  }
6045
6057
  catch (fetchErr) {
6046
6058
  const retryable = isRetryableNetworkError(fetchErr);
6059
+ const connectPhase = isConnectPhaseNetworkError(fetchErr);
6047
6060
  const transportScope = classifyNetworkTransportScope(fetchErr);
6048
6061
  // Every dispatched upstream request is an attempt, including terminal
6049
6062
  // transport failures. Record it once before preserving the throw behavior.
@@ -6055,6 +6068,7 @@ async function fetchAnthropicAccountResponse(args) {
6055
6068
  logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (${retryable ? "retryable" : "terminal"}): ${errorMessage}`);
6056
6069
  logAttempt(502, "network_error", errorMessage, {
6057
6070
  retryable,
6071
+ connectPhase,
6058
6072
  errorCode,
6059
6073
  transportScope,
6060
6074
  });
@@ -6071,6 +6085,7 @@ async function fetchAnthropicAccountResponse(args) {
6071
6085
  retrySameAccount: true,
6072
6086
  transportScope,
6073
6087
  errorCode,
6088
+ connectPhase,
6074
6089
  lastError,
6075
6090
  sawRateLimit,
6076
6091
  sawNetworkError,
@@ -6340,6 +6355,20 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6340
6355
  transportPermit.errorCode ?? undefined;
6341
6356
  loopState.lastTransportScope = transportPermit.transportScope;
6342
6357
  loopState.lastError = `Anthropic transport recovery probe failed (${transportPermit.errorCode ?? "unknown"})`;
6358
+ // The probe that failed was another request's attempt; this one has
6359
+ // sent nothing. Give it the same bounded same-account budget a direct
6360
+ // transport failure gets, instead of failing every queued request the
6361
+ // moment one probe times out.
6362
+ const probeRetryBudget = transportPermit.connectPhase
6363
+ ? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
6364
+ : MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
6365
+ if (transientSameAccountRetries < probeRetryBudget) {
6366
+ transientSameAccountRetries += 1;
6367
+ const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
6368
+ logger.always(`[proxy] retrying same account=${account.label} after failed transport recovery probe (${transientSameAccountRetries}/${probeRetryBudget}) in ${delayMs}ms`);
6369
+ await sleep(delayMs);
6370
+ continue;
6371
+ }
6343
6372
  break accountLoop;
6344
6373
  }
6345
6374
  loopState.attemptNumber += 1;
@@ -6441,7 +6470,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6441
6470
  throw error;
6442
6471
  }
6443
6472
  if (fetchResult.transportScope) {
6444
- providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit);
6473
+ providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit, fetchResult.connectPhase === true);
6445
6474
  }
6446
6475
  else {
6447
6476
  providerTransportCoordinator.reportSuccess(transportPermit);
@@ -6518,12 +6547,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6518
6547
  }
6519
6548
  continue accountLoop;
6520
6549
  }
6521
- // Transient error retry (network errors, 529 overloaded)
6550
+ // Transient error retry (network errors, 529 overloaded). A failure
6551
+ // from the connect phase sent nothing, so it earns the larger budget.
6552
+ const sameAccountRetryBudget = fetchResult.connectPhase
6553
+ ? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
6554
+ : MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
6522
6555
  if (fetchResult.retrySameAccount &&
6523
- transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
6556
+ transientSameAccountRetries < sameAccountRetryBudget) {
6524
6557
  transientSameAccountRetries += 1;
6525
6558
  const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
6526
- logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
6559
+ logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${sameAccountRetryBudget}) in ${delayMs}ms`);
6527
6560
  await sleep(delayMs);
6528
6561
  continue;
6529
6562
  }
@@ -7883,12 +7916,46 @@ function describeTransportError(error) {
7883
7916
  .join(": ");
7884
7917
  return detail ? `${message} (${detail})` : message;
7885
7918
  }
7919
+ /**
7920
+ * Whether a transport failure provably happened while connecting, before any
7921
+ * byte of the request left the process.
7922
+ *
7923
+ * Node tags its connect errors with `syscall: "connect"`; with happy-eyeballs
7924
+ * enabled (the default) the error is an AggregateError whose every entry is
7925
+ * one such attempt, and undici wraps either as `fetch failed` with the
7926
+ * original as `cause`. The code alone cannot make this call: an `ETIMEDOUT`
7927
+ * from the connect timer and an `ETIMEDOUT` from a socket that went quiet
7928
+ * after dispatch look identical by code, and only the first is safe to
7929
+ * retry. Both proxies behind one lossy Wi-Fi uplink returned 116 terminal
7930
+ * 502s in a day on exactly that ambiguity.
7931
+ */
7932
+ function isConnectPhaseNetworkError(error) {
7933
+ const seen = new Set();
7934
+ let current = error;
7935
+ while (current && typeof current === "object" && !seen.has(current)) {
7936
+ seen.add(current);
7937
+ const candidate = current;
7938
+ if (candidate.syscall === "connect") {
7939
+ return true;
7940
+ }
7941
+ if (Array.isArray(candidate.errors) && candidate.errors.length > 0) {
7942
+ return candidate.errors.every((entry) => entry !== null &&
7943
+ typeof entry === "object" &&
7944
+ entry.syscall === "connect");
7945
+ }
7946
+ current = candidate.cause;
7947
+ }
7948
+ return false;
7949
+ }
7886
7950
  /**
7887
7951
  * Determine whether a POST can be retried without risking duplicate provider
7888
7952
  * work. Only failures that prove connection establishment did not complete are
7889
7953
  * safe; a reset, socket error, or response timeout can happen after dispatch.
7890
7954
  */
7891
7955
  function isRetryableNetworkError(error) {
7956
+ if (isConnectPhaseNetworkError(error)) {
7957
+ return true;
7958
+ }
7892
7959
  const code = getErrorCode(error);
7893
7960
  return (code !== undefined &&
7894
7961
  [
@@ -8112,6 +8179,11 @@ export const __testHooks = {
8112
8179
  accountDirectoryOverride = override;
8113
8180
  },
8114
8181
  isRetryableNetworkError,
8182
+ isConnectPhaseNetworkError,
8183
+ MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES,
8184
+ clearProviderTransportCoordinatorForTests: () => {
8185
+ providerTransportCoordinator.clear();
8186
+ },
8115
8187
  isPermanentRefreshFailure,
8116
8188
  getStreamFailureDetails,
8117
8189
  trackUpstreamReadableStream,
@@ -445,6 +445,8 @@ export type ProxyProviderTransportPermit = {
445
445
  allowed: false;
446
446
  errorCode: string | null;
447
447
  transportScope: ProxyNetworkTransportScope;
448
+ /** The degrading failure happened before any request byte was sent. */
449
+ connectPhase: boolean;
448
450
  };
449
451
  export type ProxyAccountRoutingCandidate = {
450
452
  account: string;
@@ -626,6 +628,8 @@ export type RequestAttemptLogEntry = {
626
628
  transportScope?: ProxyTransportScope;
627
629
  /** Whether this failed attempt may be retried without changing the request. */
628
630
  retryable?: boolean;
631
+ /** The transport failure happened before any request byte was sent. */
632
+ connectPhase?: boolean;
629
633
  /** Distinguishes short-lived admission throttles from exhausted quota windows. */
630
634
  rateLimitKind?: "transient" | "quota";
631
635
  /** Reset-aware cooldown reason selected for a rate-limited attempt. */
@@ -701,6 +705,8 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
701
705
  cacheCreationTokens?: number;
702
706
  cacheReadTokens?: number;
703
707
  retryable?: boolean;
708
+ /** The transport failure happened before any request byte was sent. */
709
+ connectPhase?: boolean;
704
710
  /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
705
711
  errorCode?: string;
706
712
  transportScope?: ProxyTransportScope;
@@ -874,6 +880,9 @@ export type AnthropicUpstreamFetchResult = {
874
880
  retrySameAccount?: boolean;
875
881
  transportScope?: ProxyNetworkTransportScope;
876
882
  errorCode?: string;
883
+ /** The transport failure happened while connecting, before any request
884
+ * byte was sent, so retrying it cannot duplicate provider work. */
885
+ connectPhase?: boolean;
877
886
  /** When set, the caller should wait this many ms before retrying (from upstream retry-after). */
878
887
  retryAfterMs?: number;
879
888
  /** Set on a genuine 429: how long / why to cool this account before rotating. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.9.3",
3
+ "version": "12.9.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": {
@@ -138,6 +138,7 @@
138
138
  "test:local-usage": "pnpm exec tsx test/continuous-test-suite-local-usage.ts",
139
139
  "test:dynamic": "pnpm exec tsx test/continuous-test-suite-dynamic.ts",
140
140
  "test:proxy": "pnpm exec tsx test/continuous-test-suite-proxy.ts",
141
+ "test:proxy-connect-retry": "pnpm exec tsx test/continuous-test-suite-proxy-connect-retry.ts",
141
142
  "test:codex": "pnpm exec tsx test/continuous-test-suite-codex.ts",
142
143
  "test:bugfixes": "pnpm exec tsx test/continuous-test-suite-bugfixes.ts",
143
144
  "test:json-e2e": "pnpm exec tsx test/continuous-test-suite-json-e2e.ts",