@juspay/neurolink 12.26.1 → 12.26.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.
@@ -531,6 +531,46 @@ function claimTransientRateLimitRetry(accountKey, coolingUntil, now = Date.now()
531
531
  budget.retriesClaimed += 1;
532
532
  return budget.retriesClaimed;
533
533
  }
534
+ /**
535
+ * Claims one same-account retry after a transient rate limit and returns its
536
+ * delay, or undefined once this request's retries or the account's shared
537
+ * budget are spent. Both the HTTP 429 and the in-stream rate-limit paths use
538
+ * it, so a burst is paced the same way however it arrives.
539
+ */
540
+ function claimTransientRateLimitRetryDelay(accountKey, coolingUntil, retryAfterMs, retriesThisRequest) {
541
+ const slot = claimTransientRateLimitRetry(accountKey, coolingUntil);
542
+ if (retryAfterMs === undefined ||
543
+ retriesThisRequest >= MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES ||
544
+ slot === undefined) {
545
+ return undefined;
546
+ }
547
+ const base = Math.min(retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
548
+ // Stagger the two shared slots, then cap after jitter so the final sleep
549
+ // never exceeds the configured maximum.
550
+ return {
551
+ slot,
552
+ delayMs: Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * slot)),
553
+ };
554
+ }
555
+ /**
556
+ * A same-account retry that was served shows the burst is over. Without this
557
+ * the burst cooldown outlives it, the session's next request goes to another
558
+ * account and loses its prompt cache — the A→B→A bounce this retry exists to
559
+ * prevent. Only a transient cooldown is lifted: an exhaustion cooldown that a
560
+ * concurrent request recorded meanwhile stays.
561
+ */
562
+ function liftTransientCooldownAfterServedRetry(account, state) {
563
+ transientRateLimitRetryBudgets.delete(account.key);
564
+ if (state.coolingReason !== "transient" || !state.coolingUntil) {
565
+ return;
566
+ }
567
+ const previousCoolingUntil = state.coolingUntil;
568
+ state.coolingUntil = undefined;
569
+ state.coolingReason = undefined;
570
+ clearAccountCooldown(account.key, previousCoolingUntil).catch(() => {
571
+ // Best-effort: routing already reads the cleared in-memory state.
572
+ });
573
+ }
534
574
  function claimTransientCooldownAdmission(accountKey, coolingUntil, now = Date.now()) {
535
575
  if (coolingUntil <= now) {
536
576
  return 0;
@@ -4978,8 +5018,10 @@ async function handleAnthropicStreamingSuccessResponse(args) {
4978
5018
  const logicalStatus = isRateLimit ? 429 : 502;
4979
5019
  const quota = parseQuotaHeaders(responseHeaders, { model: body.model });
4980
5020
  const now = Date.now();
5021
+ let sameAccountRetry;
4981
5022
  if (isRateLimit) {
4982
- const cooldownPlan = planCooldownFor429(quota, parseRetryAfterMs(responseHeaders["retry-after"] ?? null), now, getUnifiedRateLimitStatus(responseHeaders), overagePolicy, typeof body.model === "string" ? body.model : undefined);
5023
+ const retryAfterMs = parseRetryAfterMs(responseHeaders["retry-after"] ?? null);
5024
+ const cooldownPlan = planCooldownFor429(quota, retryAfterMs, now, getUnifiedRateLimitStatus(responseHeaders), overagePolicy, typeof body.model === "string" ? body.model : undefined);
4983
5025
  accountState.quota = quota
4984
5026
  ? mergeQuotaSnapshot(accountState.quota, quota)
4985
5027
  : accountState.quota;
@@ -4999,6 +5041,12 @@ async function handleAnthropicStreamingSuccessResponse(args) {
4999
5041
  rateLimitKind,
5000
5042
  cooldownReason: cooldownPlan.reason,
5001
5043
  });
5044
+ if (!cooldownPlan.rotateImmediately) {
5045
+ sameAccountRetry = {
5046
+ coolingUntil: cooldownPlan.coolingUntil,
5047
+ retryAfterMs,
5048
+ };
5049
+ }
5002
5050
  }
5003
5051
  else {
5004
5052
  recordAttemptError(account.label, account.type, logicalStatus);
@@ -5006,7 +5054,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
5006
5054
  retryable: true,
5007
5055
  });
5008
5056
  }
5009
- logger.always(`[proxy] immediate SSE ${preflight.errorType} account=${account.label}: ${preflight.message}; rotating before client commit`);
5057
+ logger.always(`[proxy] immediate SSE ${preflight.errorType} account=${account.label}: ${preflight.message}; ${sameAccountRetry ? "transient, same account may retry" : "rotating"} before client commit`);
5010
5058
  tracer?.recordRetry(account.label, isRateLimit
5011
5059
  ? "stream_rate_limit_before_commit"
5012
5060
  : "stream_error_before_commit");
@@ -5035,6 +5083,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
5035
5083
  ...(preflight.errorType === "overloaded_error"
5036
5084
  ? { retryDelayMs: getOverloadRotationDelayMs(attemptNumber) }
5037
5085
  : {}),
5086
+ ...(sameAccountRetry ? { sameAccountRetry } : {}),
5038
5087
  },
5039
5088
  };
5040
5089
  }
@@ -7371,21 +7420,13 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7371
7420
  // Transient retries are budgeted across all concurrent requests for
7372
7421
  // this account/window. Exhaustion plans rotate immediately and never
7373
7422
  // claim this budget.
7374
- const sharedRetrySlot = fetchResult.retrySameAccount
7375
- ? claimTransientRateLimitRetry(account.key, plan.coolingUntil)
7423
+ const retry = fetchResult.retrySameAccount
7424
+ ? claimTransientRateLimitRetryDelay(account.key, plan.coolingUntil, fetchResult.retryAfterMs, rateLimitSameAccountRetries)
7376
7425
  : undefined;
7377
- if (fetchResult.retrySameAccount &&
7378
- fetchResult.retryAfterMs !== undefined &&
7379
- rateLimitSameAccountRetries <
7380
- MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES &&
7381
- sharedRetrySlot !== undefined) {
7426
+ if (retry) {
7382
7427
  rateLimitSameAccountRetries += 1;
7383
- const base = Math.min(fetchResult.retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
7384
- // Stagger the two shared slots, then cap after jitter so the final
7385
- // sleep never exceeds the configured maximum.
7386
- const delayMs = Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * sharedRetrySlot));
7387
- logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${sharedRetrySlot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
7388
- await sleep(delayMs);
7428
+ logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${retry.slot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${retry.delayMs}ms`);
7429
+ await sleep(retry.delayMs);
7389
7430
  continue;
7390
7431
  }
7391
7432
  // Exhaustion, or the shared transient retry budget being used up:
@@ -7560,6 +7601,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7560
7601
  loopState.sawRateLimit ||= successResult.failure.rateLimit;
7561
7602
  loopState.sawTransientFailure ||=
7562
7603
  !successResult.failure.rateLimit;
7604
+ const { sameAccountRetry } = successResult.failure;
7605
+ const retry = sameAccountRetry
7606
+ ? claimTransientRateLimitRetryDelay(account.key, sameAccountRetry.coolingUntil, sameAccountRetry.retryAfterMs, rateLimitSameAccountRetries)
7607
+ : undefined;
7608
+ if (retry) {
7609
+ rateLimitSameAccountRetries += 1;
7610
+ logger.always(`[proxy] retrying same account=${account.label} after transient in-stream rate limit (shared slot ${retry.slot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${retry.delayMs}ms`);
7611
+ await sleep(retry.delayMs);
7612
+ continue;
7613
+ }
7563
7614
  if (hasNextAccount && successResult.failure.retryDelayMs) {
7564
7615
  logger.always(`[proxy] pacing cross-account SSE overload rotation for ${successResult.failure.retryDelayMs}ms`);
7565
7616
  await sleep(successResult.failure.retryDelayMs);
@@ -7569,6 +7620,9 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7569
7620
  }
7570
7621
  admissionTransferredToStream =
7571
7622
  successResult.holdsAccountAdmission === true;
7623
+ if (successResult.served && rateLimitSameAccountRetries > 0) {
7624
+ liftTransientCooldownAfterServedRetry(account, accountState);
7625
+ }
7572
7626
  if (bindServedSession && successResult.served) {
7573
7627
  bindServedSession(account.key);
7574
7628
  }
@@ -1128,6 +1128,15 @@ export type AnthropicSuccessResult = {
1128
1128
  message: string;
1129
1129
  rateLimit: boolean;
1130
1130
  retryDelayMs?: number;
1131
+ /**
1132
+ * Set only for a transient (burst) rate limit: the attempt loop may
1133
+ * retry the same account within the shared budget before rotating,
1134
+ * exactly as it does for an HTTP 429.
1135
+ */
1136
+ sameAccountRetry?: {
1137
+ coolingUntil: number;
1138
+ retryAfterMs: number;
1139
+ };
1131
1140
  };
1132
1141
  } | {
1133
1142
  response: Response | unknown;
@@ -2884,7 +2884,7 @@
2884
2884
  {"objectID":"d3e03ca160a62086a328b7f6782dd27a4cc252801bd4ebbf6c6b15961a0dd314","title":"Account 3: API key for fallback","url":"/docs/features/claude-proxy#account-3-api-key-for-fallback","content":"neurolink auth login anthropic --method api-key --add --label api\n`","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Account 3: API key for fallback","lvl3":""}},
2885
2885
  {"objectID":"2167cdac36707378e3d95b4ae7f07988af5ef02cf9d09b8809b897263316efcc","title":"How accounts are selected","url":"/docs/features/claude-proxy#how-accounts-are-selected","content":"The proxy discovers accounts in this order:\nCompound keys from TokenStore (e.g., , )\nLegacy credentials file (if no compound keys exist)\nenvironment variable (if no other accounts exist)\n\nWhen is configured, this discovery happens only within the allowed set. A disabled or unavailable TokenStore account does not cause the proxy to activate a legacy file or environment key while TokenStore entries still exist.\n\nWithin the account pool, the proxy uses fill-first routing: it always tries the first non-cooling account and only switches on failure. This avoids unnecessary identity switches that could confuse Claude Code's session state.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"How accounts are selected","lvl3":""}},
2886
2886
  {"objectID":"8c3a17a43510fb3eeca918e6c91dd4f38f37f0392e1440e021488aefa1e56b80","title":"Model-scoped weekly limits","url":"/docs/features/claude-proxy#model-scoped-weekly-limits","content":"Some plans cap a specific model separately from the overall weekly window — a\nFable-only weekly allowance, for instance. Anthropic reports that cap as its own\nheader family (), sent only on responses\nfor the model it applies to, so the proxy learns about it from live traffic\nrather than needing a refresh.\n\nRouting treats a spent model-scoped cap as per-model, never per-account:\nAn account whose cap for the requested model is spent is skipped for that\n model only. It stays fully available for every other model, and no cooldown\n is set — cooling is account-wide and would wrongly withhold a healthy account.\nAmong accounts that do have headroom, the one closest to spending its\n allowance is preferred, so the pool finishes an allowance rather than spreading\n across all of them. This rung only applies when both candidates report a window\n for the model.\nIf every account has spent the cap, the request is not attempted. The\n client gets a naming the model, the real reset time, and that other\n models remain available — switch model, or add an account with headroom.\nA scoped window older than the quota freshness budget is ignored, so stale or\n mis-parsed data can never take the pool down. Routing falls back to attempting\n the request.\n\n shows any scoped window under its account, and\n returns the full array.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Model-scoped weekly limits","lvl3":""}},
2887
- {"objectID":"971d8917e081ed8fe1f30633edee82462617b534a0645dd9f370c37c0769494f","title":"Cooldown and backoff","url":"/docs/features/claude-proxy#cooldown-and-backoff","content":"When an account encounters an error, it enters a cooldown period based on the error type:\n\n| Failure | Cooldown | Behavior |\n| ------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------ |\n| Authoritative unified, 5-hour, or 7-day rejection | Upstream reset or , capped per reason | Persist cooldown and rotate immediately |\n| Transient burst 429 | Upstream delay, capped at 15 minutes | At most 2 same-account retries, then rotate |\n| Refresh credential rejection (///) | Disabled until explicit login | Rotate without retrying an invalid refresh token |\n| Refresh network, , or | 30 seconds to 5 minutes | Persist auth cooldown and rotate |\n| Upstream or network error | Bounded same-account retries | Rotate after retry budget |\n\nCooldown updates are extend-only: a late concurrent response cannot shorten a longer known reset window.\n\nEach cooldown is also capped by what its reason can mean — a cooldown\ndescribes a 5-hour window, so it can never run for days no matter what reset the\nupstream reports. The cap is applied when the cooldown is written and again when\nit is read back from disk, so an entry written by an older build heals itself on\nload and logs that it did:\n\nUse to see what is currently parked, and\n to release one.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Cooldown and backoff","lvl3":""}},
2887
+ {"objectID":"971d8917e081ed8fe1f30633edee82462617b534a0645dd9f370c37c0769494f","title":"Cooldown and backoff","url":"/docs/features/claude-proxy#cooldown-and-backoff","content":"When an account encounters an error, it enters a cooldown period based on the error type:\n\n| Failure | Cooldown | Behavior |\n| --------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------ |\n| Authoritative unified, 5-hour, or 7-day rejection | Upstream reset or , capped per reason | Persist cooldown and rotate immediately |\n| Transient burst 429, as a status or as the stream's first event | Upstream delay, capped at 15 minutes | At most 2 same-account retries, then rotate |\n| Refresh credential rejection (///) | Disabled until explicit login | Rotate without retrying an invalid refresh token |\n| Refresh network, , or | 30 seconds to 5 minutes | Persist auth cooldown and rotate |\n| Upstream or network error | Bounded same-account retries | Rotate after retry budget |\n\nCooldown updates are extend-only: a late concurrent response cannot shorten a longer known reset window.\n\nThe one exception is a transient burst. When a same-account retry is served, the\nburst is over, so its cooldown is lifted and the session's next request stays on\nthat account instead of moving away and losing its prompt cache. An exhaustion\ncooldown recorded in the meantime is left in place.\n\nEach cooldown is also capped by what its reason can mean — a cooldown\ndescribes a 5-hour window, so it can never run for days no matter what reset the\nupstream reports. The cap is applied when the cooldown is written and again when\nit is read back from disk, so an entry written by an older build heals itself on\nload and logs that it did:\n\nUse to se","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Cooldown and backoff","lvl3":""}},
2888
2888
  {"objectID":"c95cca165ca71f8c198ba1ad405ec47ab1666e20d2d9ad4120139306226f26f0","title":"Error Handling","url":"/docs/features/claude-proxy#error-handling","content":"The proxy classifies upstream errors and applies different strategies:","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Error Handling","lvl3":""}},
2889
2889
  {"objectID":"7a743b6bb13366c925cd238d4853826a08f0b161659bcb0575d5b2e83709a2e5","title":"429 Rate Limit","url":"/docs/features/claude-proxy#429-rate-limit","content":"Treat top-level as authoritative, even if sub-windows still say .\nPrefer the rejected 5-hour or 7-day reset and otherwise use .\nPersist the cooldown and rotate immediately for authoritative exhaustion.\nReturn the earliest recovery timestamp without another upstream request when all accounts are cooling.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"429 Rate Limit","lvl3":""}},
2890
2890
  {"objectID":"1522c22c8467bae6a5a51e9e4c4dca5792a7b45c457b0628ef5536d068d642b3","title":"401/402/403 Authentication Errors","url":"/docs/features/claude-proxy#401402403-authentication-errors","content":"OAuth accounts with refresh token: Serialize refresh, persist the new rotating token, and retry. A rejected refresh credential disables the account until re-authentication; transient refresh infrastructure errors cool and rotate.\nOAuth accounts without refresh token: Disable until re-authentication and rotate.\nAPI key accounts: Rotate after authentication failure.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"401/402/403 Authentication Errors","lvl3":""}},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.26.1",
3
+ "version": "12.26.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": {