@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.
@@ -18,6 +18,8 @@ import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from
18
18
  import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
19
19
  import { getUnifiedRateLimitStatus, isQuotaOverageAvailable, loadAccountQuotas, mergeQuotaSnapshot, modelFamilyToken, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
20
20
  import { fetchAccountUsage, listAnthropicAccountsForUsage, usageToQuota, } from "../../proxy/accountUsage.js";
21
+ import { tokenStore } from "../../auth/tokenStore.js";
22
+ import { fetchCodexAccountUsage, listCodexAccountsForUsage, resolveProxyStatusAccountIdentity, } from "../../proxy/codexAccountUsage.js";
21
23
  import { AccountQuotaRefreshCoordinator } from "../../proxy/accountQuotaRefreshCoordinator.js";
22
24
  import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoordinator.js";
23
25
  import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
@@ -107,6 +109,15 @@ let lastKnownAccountCount = 0;
107
109
  const MAX_AUTH_RETRIES = 5;
108
110
  const MAX_TRANSIENT_SAME_ACCOUNT_RETRIES = 2;
109
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;
110
121
  const OVERLOAD_ACCOUNT_ROTATION_DELAYS_MS = [250, 500, 1_000, 2_000];
111
122
  const MAX_FALLBACK_NETWORK_RETRIES = 1;
112
123
  const FALLBACK_STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 1000;
@@ -780,6 +791,45 @@ async function refreshAccountQuotaInBackground(account, trigger) {
780
791
  }
781
792
  await applyAccountUsageResult(account, refresh.result, refresh.startedAt);
782
793
  }
794
+ /**
795
+ * The logins the account-exposing routes enumerate, per engine, plus every
796
+ * key the token store holds at all (disabled ones included). Overridable by
797
+ * the suite because the token store is a singleton bound to the real home at
798
+ * import — a case cannot point it elsewhere, and reading the operator's own
799
+ * logins inside a fixture test is exactly how a phantom would hide.
800
+ */
801
+ let accountDirectoryOverride = null;
802
+ async function listRoutableAccountsByEngine(allowlist) {
803
+ if (accountDirectoryOverride) {
804
+ return {
805
+ anthropic: accountDirectoryOverride.anthropic,
806
+ codex: accountDirectoryOverride.codex,
807
+ };
808
+ }
809
+ // The allowlist is an Anthropic routing concept, keyed by anthropic:
810
+ // prefixes; applying it to Codex keys would exclude every Codex login.
811
+ const [anthropic, codex] = await Promise.all([
812
+ listAnthropicAccountsForUsage(allowlist),
813
+ listCodexAccountsForUsage(),
814
+ ]);
815
+ return { anthropic, codex };
816
+ }
817
+ /**
818
+ * Every login the token store knows, routable or not. This is what separates
819
+ * a DISABLED login (still here, shown as unrouted) from a REMOVED one (gone,
820
+ * and not shown): usage counters and quota snapshots outlive a logout, and
821
+ * without this check a deleted login renders as an unrouted account forever.
822
+ */
823
+ async function listKnownAccountKeys() {
824
+ if (accountDirectoryOverride) {
825
+ return accountDirectoryOverride.knownKeys;
826
+ }
827
+ const [anthropic, codex] = await Promise.all([
828
+ tokenStore.listByPrefix("anthropic:"),
829
+ tokenStore.listByPrefix("codex:"),
830
+ ]);
831
+ return new Set([...anthropic.map(normalizeAnthropicAccountKey), ...codex]);
832
+ }
783
833
  /**
784
834
  * Fetch fresh limits from Anthropic's usage endpoint for every eligible OAuth
785
835
  * account and write them through the exact same chain the passive header
@@ -789,24 +839,38 @@ async function refreshAccountQuotaInBackground(account, trigger) {
789
839
  */
790
840
  async function refreshAccountLimits(options = {}) {
791
841
  const fetchedAt = Date.now();
792
- const allAccounts = await listAnthropicAccountsForUsage(options.accountAllowlist);
842
+ const directory = await listRoutableAccountsByEngine(options.accountAllowlist);
843
+ const allAccounts = [
844
+ ...directory.anthropic.map((account) => ({
845
+ account,
846
+ provider: "anthropic",
847
+ })),
848
+ ...directory.codex.map((account) => ({
849
+ account,
850
+ provider: "codex",
851
+ })),
852
+ ];
793
853
  const accounts = options.accountFilter
794
- ? allAccounts.filter((account) => account.label === options.accountFilter ||
854
+ ? allAccounts.filter(({ account }) => account.label === options.accountFilter ||
795
855
  account.key === options.accountFilter)
796
856
  : allAccounts;
797
857
  const persisted = await loadAccountQuotas().catch(() => ({}));
798
- const buildResult = (account, status, quota, error) => {
858
+ const buildResult = (account, provider, status, quota, error) => {
799
859
  const state = accountRuntimeState.get(account.key);
860
+ // The quota store keys Anthropic snapshots by bare label for historical
861
+ // reasons (see CLAUDE.md) and Codex snapshots by full key. A Codex login
862
+ // must never fall back to the bare label: with one email on both engines
863
+ // that label holds the ANTHROPIC account's windows.
864
+ const persistedQuota = provider === "anthropic"
865
+ ? (persisted[account.key] ?? persisted[account.label] ?? null)
866
+ : (persisted[account.key] ?? null);
800
867
  const result = {
801
868
  account: account.label,
802
869
  key: account.key,
870
+ provider,
803
871
  type: account.type,
804
872
  status,
805
- quota: quota ??
806
- state?.quota ??
807
- persisted[account.key] ??
808
- persisted[account.label] ??
809
- null,
873
+ quota: quota ?? state?.quota ?? persistedQuota,
810
874
  };
811
875
  if (error !== undefined) {
812
876
  result.error = error;
@@ -823,7 +887,7 @@ async function refreshAccountLimits(options = {}) {
823
887
  return {
824
888
  fetchedAt,
825
889
  snapshot: true,
826
- results: accounts.map((account) => buildResult(account, "snapshot", null)),
890
+ results: accounts.map(({ account, provider }) => buildResult(account, provider, "snapshot", null)),
827
891
  refreshMetrics: accountQuotaRefreshCoordinator.getMetrics(),
828
892
  };
829
893
  }
@@ -835,23 +899,41 @@ async function refreshAccountLimits(options = {}) {
835
899
  if (index >= accounts.length) {
836
900
  return;
837
901
  }
838
- const account = accounts[index];
902
+ const { account, provider } = accounts[index];
839
903
  if (account.type !== "oauth") {
840
- results[index] = buildResult(account, "skipped_api_key", null);
904
+ results[index] = buildResult(account, provider, "skipped_api_key", null);
841
905
  continue;
842
906
  }
843
907
  const lastFetch = lastUsageFetchAt.get(account.key) ?? 0;
844
908
  if (Date.now() - lastFetch < MIN_USAGE_REFETCH_INTERVAL_MS) {
845
- results[index] = buildResult(account, "throttled", null);
909
+ results[index] = buildResult(account, provider, "throttled", null);
846
910
  continue;
847
911
  }
848
912
  lastUsageFetchAt.set(account.key, Date.now());
913
+ if (provider === "codex") {
914
+ // Codex has its own usage endpoint and no overage/cooldown
915
+ // reconciliation to run; the snapshot is written under the full key,
916
+ // which is the only key the Codex engine ever reads it back by.
917
+ try {
918
+ const fetched = await fetchCodexAccountUsage(account);
919
+ if (fetched.ok === false) {
920
+ results[index] = buildResult(account, provider, "error", null, `codex usage fetch failed: ${fetched.reason}`);
921
+ continue;
922
+ }
923
+ await saveAccountQuota(account.key, fetched.quota);
924
+ results[index] = buildResult(account, provider, "refreshed", fetched.quota);
925
+ }
926
+ catch (err) {
927
+ results[index] = buildResult(account, provider, "error", null, err instanceof Error ? err.message : String(err));
928
+ }
929
+ continue;
930
+ }
849
931
  // Isolate failures per account: an unexpected rejection must not abort
850
932
  // the Promise.all sweep and turn the whole /limits response into a 502.
851
933
  try {
852
934
  const refresh = await accountQuotaRefreshCoordinator.run(account, `manual:${account.key}`, fetchValidatedAccountUsage, { force: true });
853
935
  if (refresh.kind !== "completed") {
854
- results[index] = buildResult(account, "throttled", null);
936
+ results[index] = buildResult(account, provider, "throttled", null);
855
937
  continue;
856
938
  }
857
939
  const fetchResult = refresh.result;
@@ -859,18 +941,18 @@ async function refreshAccountLimits(options = {}) {
859
941
  // file without strictNullChecks, where negated boolean-discriminant
860
942
  // narrowing does not apply.
861
943
  if (fetchResult.ok === false) {
862
- results[index] = buildResult(account, "error", null, fetchResult.error);
944
+ results[index] = buildResult(account, provider, "error", null, fetchResult.error);
863
945
  continue;
864
946
  }
865
947
  const quota = await applyAccountUsageResult(account, fetchResult, refresh.startedAt, persisted[account.key] ?? persisted[account.label] ?? null);
866
948
  if (!quota) {
867
- results[index] = buildResult(account, "error", null, "usage payload had no recognizable limit windows");
949
+ results[index] = buildResult(account, provider, "error", null, "usage payload had no recognizable limit windows");
868
950
  continue;
869
951
  }
870
- results[index] = buildResult(account, "refreshed", quota);
952
+ results[index] = buildResult(account, provider, "refreshed", quota);
871
953
  }
872
954
  catch (err) {
873
- results[index] = buildResult(account, "error", null, err instanceof Error ? err.message : String(err));
955
+ results[index] = buildResult(account, provider, "error", null, err instanceof Error ? err.message : String(err));
874
956
  }
875
957
  }
876
958
  };
@@ -5768,6 +5850,9 @@ function createAnthropicAttemptLogger(args) {
5768
5850
  ? { cacheReadTokens: extra.cacheReadTokens }
5769
5851
  : {}),
5770
5852
  ...(extra?.retryable !== undefined ? { retryable: extra.retryable } : {}),
5853
+ ...(extra?.connectPhase !== undefined
5854
+ ? { connectPhase: extra.connectPhase }
5855
+ : {}),
5771
5856
  ...(extra?.rateLimitKind ? { rateLimitKind: extra.rateLimitKind } : {}),
5772
5857
  ...(extra?.cooldownReason
5773
5858
  ? { cooldownReason: extra.cooldownReason }
@@ -5971,6 +6056,7 @@ async function fetchAnthropicAccountResponse(args) {
5971
6056
  }
5972
6057
  catch (fetchErr) {
5973
6058
  const retryable = isRetryableNetworkError(fetchErr);
6059
+ const connectPhase = isConnectPhaseNetworkError(fetchErr);
5974
6060
  const transportScope = classifyNetworkTransportScope(fetchErr);
5975
6061
  // Every dispatched upstream request is an attempt, including terminal
5976
6062
  // transport failures. Record it once before preserving the throw behavior.
@@ -5982,6 +6068,7 @@ async function fetchAnthropicAccountResponse(args) {
5982
6068
  logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (${retryable ? "retryable" : "terminal"}): ${errorMessage}`);
5983
6069
  logAttempt(502, "network_error", errorMessage, {
5984
6070
  retryable,
6071
+ connectPhase,
5985
6072
  errorCode,
5986
6073
  transportScope,
5987
6074
  });
@@ -5998,6 +6085,7 @@ async function fetchAnthropicAccountResponse(args) {
5998
6085
  retrySameAccount: true,
5999
6086
  transportScope,
6000
6087
  errorCode,
6088
+ connectPhase,
6001
6089
  lastError,
6002
6090
  sawRateLimit,
6003
6091
  sawNetworkError,
@@ -6267,6 +6355,20 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6267
6355
  transportPermit.errorCode ?? undefined;
6268
6356
  loopState.lastTransportScope = transportPermit.transportScope;
6269
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
+ }
6270
6372
  break accountLoop;
6271
6373
  }
6272
6374
  loopState.attemptNumber += 1;
@@ -6368,7 +6470,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6368
6470
  throw error;
6369
6471
  }
6370
6472
  if (fetchResult.transportScope) {
6371
- providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit);
6473
+ providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit, fetchResult.connectPhase === true);
6372
6474
  }
6373
6475
  else {
6374
6476
  providerTransportCoordinator.reportSuccess(transportPermit);
@@ -6445,12 +6547,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6445
6547
  }
6446
6548
  continue accountLoop;
6447
6549
  }
6448
- // 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;
6449
6555
  if (fetchResult.retrySameAccount &&
6450
- transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
6556
+ transientSameAccountRetries < sameAccountRetryBudget) {
6451
6557
  transientSameAccountRetries += 1;
6452
6558
  const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
6453
- 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`);
6454
6560
  await sleep(delayMs);
6455
6561
  continue;
6456
6562
  }
@@ -6769,11 +6875,6 @@ function buildEarlyClaudeRequestError(args) {
6769
6875
  * millisecond fields tens of thousands of years into the future, so only the
6770
6876
  * seconds fields are converted, and 0 becomes null rather than epoch zero.
6771
6877
  */
6772
- /**
6773
- * Account types that represent a real credential rather than proxy plumbing.
6774
- * Mirrors the Anthropic pool's own account types.
6775
- */
6776
- const REAL_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
6777
6878
  function toMillis(value) {
6778
6879
  return typeof value === "number" && Number.isFinite(value) && value > 0
6779
6880
  ? Math.round(value * 1000)
@@ -7498,9 +7599,31 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
7498
7599
  // Usage is the optional half; quota and status must still render.
7499
7600
  usageError = error instanceof Error ? error.message : String(error);
7500
7601
  }
7501
- const statsByLabel = new Map();
7502
- for (const entry of Object.values(statsAccounts)) {
7503
- statsByLabel.set(entry.label, entry);
7602
+ // Joined by provider-qualified KEY, never by label. One email can
7603
+ // be logged in to both engines; joined by label, the Codex login was
7604
+ // swallowed by the Anthropic row (and its counters could land on
7605
+ // that row, last write winning). Legacy stats entries that predate
7606
+ // the key carry only a label and a type, which is enough to derive
7607
+ // it; a keyed entry for the same login wins over a legacy one.
7608
+ const statsByKey = new Map();
7609
+ for (const [mapKey, entry] of Object.entries(statsAccounts)) {
7610
+ const identity = resolveProxyStatusAccountIdentity(entry.label, entry.type, entry.key ?? mapKey);
7611
+ const identityKey = identity.key ?? mapKey;
7612
+ const existing = statsByKey.get(identityKey);
7613
+ if (!existing || (entry.key && !existing.key)) {
7614
+ statsByKey.set(identityKey, { ...entry, identityKey });
7615
+ }
7616
+ }
7617
+ // Which logins exist at all, disabled ones included. A stats entry
7618
+ // with no login behind it is a REMOVED account, not an unrouted one.
7619
+ // If the store cannot be read, err towards showing rows: hiding a
7620
+ // real login is the worse mistake, and the old behaviour.
7621
+ let knownKeys;
7622
+ try {
7623
+ knownKeys = await listKnownAccountKeys();
7624
+ }
7625
+ catch {
7626
+ knownKeys = null;
7504
7627
  }
7505
7628
  const rows = [];
7506
7629
  const claimed = new Set();
@@ -7509,13 +7632,14 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
7509
7632
  // today, which is exactly when an operator most wants to see it.
7510
7633
  for (const result of limits.results) {
7511
7634
  const label = result.account;
7512
- claimed.add(label);
7513
- const stat = statsByLabel.get(label);
7635
+ claimed.add(result.key);
7636
+ const stat = statsByKey.get(result.key);
7514
7637
  const quota = normalizeQuotaForAccounts(result.quota);
7515
7638
  const cooling = isCooling(result.key ?? null);
7516
7639
  rows.push({
7517
7640
  label,
7518
7641
  key: result.key ?? null,
7642
+ provider: result.provider,
7519
7643
  kind: "account",
7520
7644
  type: result.type ?? stat?.type ?? "oauth",
7521
7645
  // result.status describes how the quota was obtained
@@ -7549,39 +7673,53 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
7549
7673
  weeklyHealth: quotaHealth(quota.weeklyStatus),
7550
7674
  }
7551
7675
  : null,
7552
- usage: usageByAccount.get(label) ?? null,
7676
+ usage: usageByAccount.get(result.key) ?? null,
7553
7677
  });
7554
7678
  }
7555
7679
  // Plumbing rows are still reported, but tagged, so a consumer can
7556
7680
  // show or hide them rather than rendering them as credentials.
7557
7681
  //
7558
- // A real login can land here too: listAnthropicAccountsForUsage
7559
- // skips accounts the token store has disabled or the allowlist
7560
- // excludes, so an account with real usage history but no current
7561
- // route is absent from limits.results. Tagging that as plumbing hid
7562
- // the one account an operator is looking for when they ask why
7563
- // traffic stopped the docs tell consumers to filter internal rows
7564
- // out. It stays kind "account", with a status saying why it has no
7565
- // quota block.
7566
- for (const entry of Object.values(statsAccounts)) {
7567
- if (claimed.has(entry.label)) {
7682
+ // A real login can land here too: the listers skip accounts the
7683
+ // token store has disabled or the allowlist excludes, so a login
7684
+ // with real usage history but no current route is absent from
7685
+ // limits.results. Tagging that as plumbing hid the one account an
7686
+ // operator is looking for when they ask why traffic stopped. It
7687
+ // stays kind "account", with a status saying why it has no quota
7688
+ // block but only while the login still EXISTS. A stats entry whose
7689
+ // login has been removed from the store is history, not a
7690
+ // credential, and rendering it as "unrouted" put a phantom account
7691
+ // on every dashboard for as long as the counters file lived.
7692
+ for (const entry of statsByKey.values()) {
7693
+ const identity = resolveProxyStatusAccountIdentity(entry.label, entry.type, entry.identityKey);
7694
+ if (identity.key !== null && claimed.has(identity.key)) {
7695
+ continue;
7696
+ }
7697
+ const isLogin = identity.provider !== "other";
7698
+ if (isLogin &&
7699
+ identity.key !== null &&
7700
+ knownKeys !== null &&
7701
+ !knownKeys.has(identity.key)) {
7568
7702
  continue;
7569
7703
  }
7570
- const isRealAccount = REAL_ACCOUNT_TYPES.has(entry.type);
7571
7704
  rows.push({
7572
7705
  label: entry.label,
7573
- key: null,
7574
- kind: isRealAccount
7706
+ key: isLogin ? identity.key : null,
7707
+ ...(isLogin ? { provider: identity.provider } : {}),
7708
+ kind: isLogin
7575
7709
  ? "account"
7576
7710
  : entry.type === "translation"
7577
7711
  ? "translation"
7578
7712
  : "internal",
7579
- type: entry.type,
7580
- status: isRealAccount ? "unrouted" : null,
7713
+ // The row's type is the credential kind; the engine is
7714
+ // `provider`. Stats record Codex logins as "codex-oauth", which
7715
+ // consumers that only know the credential kinds read as
7716
+ // plumbing, so it is reported as the OAuth login it is.
7717
+ type: identity.provider === "codex" ? "oauth" : entry.type,
7718
+ status: isLogin ? "unrouted" : null,
7581
7719
  cooling: false,
7582
7720
  allowed: null,
7583
7721
  expired: null,
7584
- isPrimary: isPrimaryAccount(entry.label),
7722
+ isPrimary: isLogin ? isPrimaryAccount(identity.key) : false,
7585
7723
  requests: entry.successCount + entry.errorCount,
7586
7724
  errors: entry.errorCount,
7587
7725
  rateLimits: entry.rateLimitCount,
@@ -7593,7 +7731,9 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
7593
7731
  // and cost in the ledger — hardcoding null here discarded
7594
7732
  // exactly the usage an operator is looking for when they ask
7595
7733
  // why traffic stopped.
7596
- usage: usageByAccount.get(entry.label) ?? null,
7734
+ usage: identity.key !== null
7735
+ ? (usageByAccount.get(identity.key) ?? null)
7736
+ : null,
7597
7737
  });
7598
7738
  }
7599
7739
  const response = {
@@ -7776,12 +7916,46 @@ function describeTransportError(error) {
7776
7916
  .join(": ");
7777
7917
  return detail ? `${message} (${detail})` : message;
7778
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
+ }
7779
7950
  /**
7780
7951
  * Determine whether a POST can be retried without risking duplicate provider
7781
7952
  * work. Only failures that prove connection establishment did not complete are
7782
7953
  * safe; a reset, socket error, or response timeout can happen after dispatch.
7783
7954
  */
7784
7955
  function isRetryableNetworkError(error) {
7956
+ if (isConnectPhaseNetworkError(error)) {
7957
+ return true;
7958
+ }
7785
7959
  const code = getErrorCode(error);
7786
7960
  return (code !== undefined &&
7787
7961
  [
@@ -8001,7 +8175,15 @@ export const __testHooks = {
8001
8175
  limitsRefreshInFlight = null;
8002
8176
  accountQuotaRefreshCoordinator.clear();
8003
8177
  },
8178
+ setAccountDirectoryForTests: (override) => {
8179
+ accountDirectoryOverride = override;
8180
+ },
8004
8181
  isRetryableNetworkError,
8182
+ isConnectPhaseNetworkError,
8183
+ MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES,
8184
+ clearProviderTransportCoordinatorForTests: () => {
8185
+ providerTransportCoordinator.clear();
8186
+ },
8005
8187
  isPermanentRefreshFailure,
8006
8188
  getStreamFailureDetails,
8007
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. */
@@ -1209,8 +1218,14 @@ export type ProxyQuotaRefreshRunResult = {
1209
1218
  export type ProxyLimitsAccountResult = {
1210
1219
  /** Account label (quota-store key). */
1211
1220
  account: string;
1212
- /** Token-store key ("anthropic:<label>"). */
1221
+ /** Token-store key ("anthropic:<label>" or "codex:<label>"). */
1213
1222
  key: string;
1223
+ /**
1224
+ * Which pool engine owns this login. Two logins can share a label — an
1225
+ * operator may use one email for both — so the key, not the label, is the
1226
+ * identity, and this names the engine without parsing the key's prefix.
1227
+ */
1228
+ provider: ProxyAccountProvider;
1214
1229
  type: ProxyAccountType;
1215
1230
  status: "refreshed" | "throttled" | "skipped_api_key" | "snapshot" | "error";
1216
1231
  /** Fresh quota on "refreshed"; last known snapshot otherwise (may be null). */
@@ -1219,6 +1234,20 @@ export type ProxyLimitsAccountResult = {
1219
1234
  coolingUntil?: number;
1220
1235
  coolingReason?: AccountCoolingReason;
1221
1236
  };
1237
+ /** The pool engine a login belongs to, as named on limits and accounts rows. */
1238
+ export type ProxyAccountProvider = "anthropic" | "codex";
1239
+ /**
1240
+ * Test-only replacement for the token store behind the account-exposing
1241
+ * routes. The token store is a module singleton bound to the real home at
1242
+ * import, so a suite cannot redirect it; this lets a case state which logins
1243
+ * exist (`knownKeys`, including disabled ones) and which are routable per
1244
+ * engine, exactly as the real listers would answer.
1245
+ */
1246
+ export type ProxyAccountDirectoryOverride = {
1247
+ knownKeys: Set<string>;
1248
+ anthropic: ProxyPassthroughAccount[];
1249
+ codex: ProxyPassthroughAccount[];
1250
+ };
1222
1251
  /** Response body of the proxy's GET /limits endpoint. */
1223
1252
  export type ProxyLimitsRefreshResponse = {
1224
1253
  fetchedAt: number;
@@ -141,10 +141,21 @@ export type CliClientUsageTotals = {
141
141
  };
142
142
  /** One row of GET /accounts. */
143
143
  export type CliAccountsRow = {
144
- /** Bare label, e.g. "someone@example.com". The join key across all sources. */
144
+ /**
145
+ * Bare label, e.g. "someone@example.com". Display only: two rows can share
146
+ * it when one email is logged in to both engines. `key` is the identity.
147
+ */
145
148
  label: string;
146
- /** Full pool key, e.g. "anthropic:someone@example.com". */
149
+ /**
150
+ * Full pool key, e.g. "anthropic:someone@example.com" or
151
+ * "codex:someone@example.com". Null only for plumbing rows.
152
+ */
147
153
  key: string | null;
154
+ /**
155
+ * Which pool engine owns this login. Absent on plumbing rows. Consumers
156
+ * that key a list by row must key by `key`, not `label` — see above.
157
+ */
158
+ provider?: "anthropic" | "codex";
148
159
  /**
149
160
  * What this row actually is. Only "account" rows are real logins; the proxy
150
161
  * also tracks internal and translation pseudo-accounts, which have no quota
@@ -183,6 +194,13 @@ export type CliAccountsResponse = {
183
194
  /** One request as recorded in the proxy request log, reduced to what costing needs. */
184
195
  export type ProxyLedgerEntry = {
185
196
  account: string;
197
+ /**
198
+ * Provider-qualified identity, "anthropic:<label>" or "codex:<label>".
199
+ * Read from the log row when present; derived from `accountType` for rows
200
+ * written before the pool logged it. This, not `account`, is the join key:
201
+ * one email can be logged in to both engines.
202
+ */
203
+ accountKey: string;
186
204
  /** Derived calling CLI; see CliAccountUsageTotals.byClient. */
187
205
  clientApp: string;
188
206
  accountType: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.9.2",
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",