@juspay/neurolink 12.9.2 → 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
  },
@@ -29,10 +29,13 @@ import type { CliAccountUsageTotals } from "../types/index.js";
29
29
  /** UTC date stamp of the log file the totals cover. */
30
30
  export declare function currentUsageDate(now?: Date): string;
31
31
  /**
32
- * Token and cost totals for one UTC day, keyed by bare account label.
32
+ * Token and cost totals for one UTC day, keyed by provider-qualified account
33
+ * key ("anthropic:<label>" / "codex:<label>").
33
34
  *
34
- * Only Anthropic-pool rows are attributed, so a Codex account sharing a label
35
- * with an Anthropic one cannot contribute its tokens to the wrong row.
35
+ * Both engines' rows are attributed, each under its own key, so a Codex login
36
+ * sharing an email with an Anthropic one contributes to its own row and never
37
+ * to the other's. Keying by bare label used to force a choice between merging
38
+ * them and dropping Codex outright; it dropped Codex.
36
39
  */
37
40
  export declare function readAccountUsage(date?: string): Promise<Map<string, CliAccountUsageTotals>>;
38
41
  /** Drop all cached cursors. Exported for tests, which vary HOME per case. */
@@ -33,9 +33,29 @@ const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
33
33
  /**
34
34
  * Account types written by the Anthropic pool. The request log is shared with
35
35
  * the Codex engine, and an operator can use the same email for both, so rows
36
- * must be filtered by engine before being attributed to an Anthropic account.
36
+ * are attributed by provider-qualified key, never by bare label.
37
37
  */
38
38
  const ANTHROPIC_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
39
+ const CODEX_ACCOUNT_TYPE = "codex-oauth";
40
+ /**
41
+ * The key a log row is attributed under. Current builds write `accountKey`;
42
+ * rows from before that field existed carry only the label and the engine's
43
+ * account type, which is enough to rebuild it — the two engines never share a
44
+ * type string.
45
+ */
46
+ function ledgerAccountKey(record, account) {
47
+ if (typeof record.accountKey === "string" && record.accountKey.length > 0) {
48
+ return record.accountKey;
49
+ }
50
+ const type = typeof record.accountType === "string" ? record.accountType : "";
51
+ if (ANTHROPIC_ACCOUNT_TYPES.has(type)) {
52
+ return `anthropic:${account.trim().toLowerCase()}`;
53
+ }
54
+ if (type === CODEX_ACCOUNT_TYPE) {
55
+ return `codex:${account}`;
56
+ }
57
+ return account;
58
+ }
39
59
  const cursors = new Map();
40
60
  function getLogsDir() {
41
61
  return join(homedir(), ".neurolink", "logs");
@@ -134,9 +154,11 @@ async function advanceCursor(fileName, cursor) {
134
154
  // on the triple means two distinct calls that collide on id but differ in
135
155
  // account or model are still counted separately; a true re-log of the same
136
156
  // request keeps the same triple and merges.
137
- const entryKey = `${requestId}\u0000${account}\u0000${typeof record.model === "string" ? record.model : ""}`;
157
+ const accountKey = ledgerAccountKey(record, account);
158
+ const entryKey = `${requestId}\u0000${accountKey}\u0000${typeof record.model === "string" ? record.model : ""}`;
138
159
  const next = {
139
160
  account,
161
+ accountKey,
140
162
  accountType: typeof record.accountType === "string" ? record.accountType : "",
141
163
  model: typeof record.model === "string" ? record.model : "",
142
164
  provider: typeof record.provider === "string" ? record.provider : undefined,
@@ -254,10 +276,13 @@ function emptyTotals() {
254
276
  };
255
277
  }
256
278
  /**
257
- * Token and cost totals for one UTC day, keyed by bare account label.
279
+ * Token and cost totals for one UTC day, keyed by provider-qualified account
280
+ * key ("anthropic:<label>" / "codex:<label>").
258
281
  *
259
- * Only Anthropic-pool rows are attributed, so a Codex account sharing a label
260
- * with an Anthropic one cannot contribute its tokens to the wrong row.
282
+ * Both engines' rows are attributed, each under its own key, so a Codex login
283
+ * sharing an email with an Anthropic one contributes to its own row and never
284
+ * to the other's. Keying by bare label used to force a choice between merging
285
+ * them and dropping Codex outright; it dropped Codex.
261
286
  */
262
287
  export async function readAccountUsage(date = currentUsageDate()) {
263
288
  const fileName = `proxy-${date}.jsonl`;
@@ -281,10 +306,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
281
306
  const totals = new Map();
282
307
  const unpriced = new Map();
283
308
  for (const entry of cursor.entries.values()) {
284
- if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType)) {
309
+ if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType) &&
310
+ entry.accountType !== CODEX_ACCOUNT_TYPE) {
311
+ // Translation and internal rows are proxy plumbing, not a login's spend.
285
312
  continue;
286
313
  }
287
- const row = totals.get(entry.account) ?? emptyTotals();
314
+ const row = totals.get(entry.accountKey) ?? emptyTotals();
288
315
  row.requests += 1;
289
316
  row.inputTokens += entry.inputTokens;
290
317
  row.outputTokens += entry.outputTokens;
@@ -318,12 +345,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
318
345
  }
319
346
  else {
320
347
  row.unpricedRequests += 1;
321
- const seen = unpriced.get(entry.account) ?? new Set();
348
+ const seen = unpriced.get(entry.accountKey) ?? new Set();
322
349
  seen.add(entry.model);
323
- unpriced.set(entry.account, seen);
350
+ unpriced.set(entry.accountKey, seen);
324
351
  }
325
352
  }
326
- totals.set(entry.account, row);
353
+ totals.set(entry.accountKey, row);
327
354
  }
328
355
  for (const [account, row] of totals) {
329
356
  row.costUsd = Number(row.costUsd.toFixed(6));
@@ -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) {
@@ -12,6 +12,7 @@
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
14
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
15
+ import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
15
16
  import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
17
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
17
18
  declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
@@ -388,6 +389,20 @@ declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughA
388
389
  export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
389
390
  export declare function getOverloadRotationDelayMs(attemptNumber: number): number;
390
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;
391
406
  /**
392
407
  * Determine whether a POST can be retried without risking duplicate provider
393
408
  * work. Only failures that prove connection establishment did not complete are
@@ -467,7 +482,11 @@ export declare const __testHooks: {
467
482
  refreshAccountLimits: typeof refreshAccountLimits;
468
483
  applyAccountUsageResult: typeof applyAccountUsageResult;
469
484
  clearLimitsRefreshStateForTests: () => void;
485
+ setAccountDirectoryForTests: (override: ProxyAccountDirectoryOverride | null) => void;
470
486
  isRetryableNetworkError: typeof isRetryableNetworkError;
487
+ isConnectPhaseNetworkError: typeof isConnectPhaseNetworkError;
488
+ MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES: number;
489
+ clearProviderTransportCoordinatorForTests: () => void;
471
490
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
472
491
  getStreamFailureDetails: typeof getStreamFailureDetails;
473
492
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;