@juspay/neurolink 10.8.8 → 10.8.10

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.
@@ -12,6 +12,14 @@
12
12
  import type { AccountQuota } from "../types/index.js";
13
13
  /** Read and normalize Anthropic's authoritative top-level unified status. */
14
14
  export declare function getUnifiedRateLimitStatus(headers: Headers | Record<string, string>): string | undefined;
15
+ /**
16
+ * Whether Anthropic explicitly permits a request to use overage after a
17
+ * subscription window is exhausted. Fresh responses require all three
18
+ * provider signals. Older persisted snapshots predate the raw fallback and
19
+ * upgrade-path fields, but retain a positive fallback percentage together with
20
+ * an allowed overage status, which is the equivalent provider state.
21
+ */
22
+ export declare function isQuotaOverageAvailable(quota: Pick<AccountQuota, "fallbackPercentage" | "fallbackStatus" | "overageStatus" | "upgradePaths"> | null | undefined): boolean;
15
23
  /**
16
24
  * Parse Anthropic rate-limit / quota headers into an `AccountQuota`.
17
25
  * Returns `null` when key headers are absent.
@@ -39,6 +39,27 @@ export function getUnifiedRateLimitStatus(headers) {
39
39
  const normalized = value?.trim().toLowerCase();
40
40
  return normalized || undefined;
41
41
  }
42
+ /**
43
+ * Whether Anthropic explicitly permits a request to use overage after a
44
+ * subscription window is exhausted. Fresh responses require all three
45
+ * provider signals. Older persisted snapshots predate the raw fallback and
46
+ * upgrade-path fields, but retain a positive fallback percentage together with
47
+ * an allowed overage status, which is the equivalent provider state.
48
+ */
49
+ export function isQuotaOverageAvailable(quota) {
50
+ if (quota?.overageStatus?.trim().toLowerCase() !== "allowed") {
51
+ return false;
52
+ }
53
+ const explicitFallback = quota.fallbackStatus?.trim().toLowerCase();
54
+ const hasExplicitOveragePath = (quota.upgradePaths ?? "")
55
+ .split(",")
56
+ .map((path) => path.trim().toLowerCase())
57
+ .includes("overage");
58
+ if (explicitFallback === "available" && hasExplicitOveragePath) {
59
+ return true;
60
+ }
61
+ return explicitFallback === undefined && (quota.fallbackPercentage ?? 0) > 0;
62
+ }
42
63
  /**
43
64
  * Parse Anthropic rate-limit / quota headers into an `AccountQuota`.
44
65
  * Returns `null` when key headers are absent.
@@ -69,6 +90,8 @@ export function parseQuotaHeaders(headers) {
69
90
  weeklyStatus: getHeader(headers, `${P}unified-7d-status`) ?? "unknown",
70
91
  weeklyResetAt: weeklyResetRaw ? parseInt(weeklyResetRaw, 10) || 0 : 0,
71
92
  fallbackPercentage: fallbackRaw ? parseFloat(fallbackRaw) || 0 : 0,
93
+ fallbackStatus: getHeader(headers, `${P}unified-fallback`) ?? "unknown",
94
+ upgradePaths: getHeader(headers, `${P}unified-upgrade-paths`),
72
95
  overageStatus: getHeader(headers, `${P}unified-overage-status`) ?? "unknown",
73
96
  lastUpdated: Date.now(),
74
97
  };
@@ -87,6 +87,7 @@ function routingCandidateValue(value) {
87
87
  "sessionStatus",
88
88
  "weeklyStatus",
89
89
  ];
90
+ const optionalNullableStringFields = ["fallbackStatus", "upgradePaths"];
90
91
  if (!stringValue(candidate.account) ||
91
92
  typeof candidate.accountType !== "string" ||
92
93
  !ROUTING_ACCOUNT_TYPES.has(candidate.accountType) ||
@@ -97,6 +98,12 @@ function routingCandidateValue(value) {
97
98
  requiredBooleanFields.some((field) => typeof candidate[field] !== "boolean") ||
98
99
  requiredNullableNumberFields.some((field) => !isNullableFiniteNumber(candidate[field])) ||
99
100
  requiredNullableStringFields.some((field) => !isNullableString(candidate[field])) ||
101
+ optionalNullableStringFields.some((field) => field in candidate &&
102
+ candidate[field] !== undefined &&
103
+ !isNullableString(candidate[field])) ||
104
+ ("overageEligible" in candidate &&
105
+ candidate.overageEligible !== undefined &&
106
+ typeof candidate.overageEligible !== "boolean") ||
100
107
  !(candidate.coolingReason === null ||
101
108
  (typeof candidate.coolingReason === "string" &&
102
109
  COOLING_REASONS.has(candidate.coolingReason)))) {
@@ -117,6 +124,9 @@ function routingCandidateValue(value) {
117
124
  coolingReason: candidate.coolingReason,
118
125
  coolingUntil: candidate.coolingUntil,
119
126
  unifiedStatus: candidate.unifiedStatus,
127
+ fallbackStatus: candidate.fallbackStatus,
128
+ upgradePaths: candidate.upgradePaths,
129
+ overageEligible: candidate.overageEligible,
120
130
  overageStatus: candidate.overageStatus,
121
131
  sessionStatus: candidate.sessionStatus,
122
132
  sessionUsed: candidate.sessionUsed,
@@ -12,7 +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 { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyQuotaCooldownUpdate, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
16
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number): AccountAdmissionLease | undefined;
17
17
  declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
18
18
  declare function acquireFirstAvailableAccountAdmission(accountKeys: string[], capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<{
@@ -45,15 +45,23 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
45
45
  * The unified subscription limits expose per-window status + reset:
46
46
  * - weekly (7d) "rejected" → hard cap for the week; cool until the 7d reset.
47
47
  * - session (5h) "rejected" → paced out for this session; cool until the 5h reset.
48
- * Both mean "retrying this account is futile until its window resets" → rotate
49
- * immediately (no same-account retries) and park the account until the ACTUAL
50
- * reset never the legacy 60s hardcap that let us re-hammer a spent account.
48
+ * Both mean "retrying this account is futile until its window resets" unless
49
+ * the provider explicitly enables overage. In that case, the subscription
50
+ * window is exhausted but the account remains usable for paid fallback.
51
51
  *
52
52
  * Anything else (window still "allowed" but momentarily 429'd — a per-minute
53
53
  * burst / acceleration limit) is transient: honor retry-after as a floor,
54
54
  * allow a couple of jittered same-account retries, then a short cooldown.
55
55
  */
56
56
  declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined): AccountCooldownPlan;
57
+ /**
58
+ * Proactively cool an account when a SUCCESS response reveals a window has just
59
+ * flipped to "rejected" (the boundary request that spends the last of the quota
60
+ * still returns 200 but reports rejected/next-reset). Parks the account until
61
+ * its reset so the next request skips it instead of discovering the limit via a
62
+ * 429, except when the provider explicitly enables paid overage.
63
+ */
64
+ declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number): ProxyQuotaCooldownUpdate;
57
65
  /**
58
66
  * Seed each account's runtime quota from the persisted snapshots in
59
67
  * ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
@@ -329,6 +337,7 @@ export declare const __testHooks: {
329
337
  resolveHomeIndex: typeof resolveHomeIndex;
330
338
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
331
339
  planCooldownFor429: typeof planCooldownFor429;
340
+ reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
332
341
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
333
342
  getStreamFailureDetails: typeof getStreamFailureDetails;
334
343
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
@@ -15,7 +15,7 @@ import { join } from "node:path";
15
15
  import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
16
16
  import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
17
17
  import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
18
- import { getUnifiedRateLimitStatus, loadAccountQuotas, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
18
+ import { getUnifiedRateLimitStatus, isQuotaOverageAvailable, loadAccountQuotas, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
19
19
  import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
20
20
  import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
21
21
  import { tracers } from "../../telemetry/tracers.js";
@@ -411,9 +411,9 @@ function clampCooldownUntil(untilMs, now) {
411
411
  * The unified subscription limits expose per-window status + reset:
412
412
  * - weekly (7d) "rejected" → hard cap for the week; cool until the 7d reset.
413
413
  * - session (5h) "rejected" → paced out for this session; cool until the 5h reset.
414
- * Both mean "retrying this account is futile until its window resets" → rotate
415
- * immediately (no same-account retries) and park the account until the ACTUAL
416
- * reset never the legacy 60s hardcap that let us re-hammer a spent account.
414
+ * Both mean "retrying this account is futile until its window resets" unless
415
+ * the provider explicitly enables overage. In that case, the subscription
416
+ * window is exhausted but the account remains usable for paid fallback.
417
417
  *
418
418
  * Anything else (window still "allowed" but momentarily 429'd — a per-minute
419
419
  * burst / acceleration limit) is transient: honor retry-after as a floor,
@@ -430,7 +430,8 @@ function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.uni
430
430
  rotateImmediately: true,
431
431
  };
432
432
  }
433
- if (quota && quota.sessionStatus === "rejected") {
433
+ const overageAvailable = isQuotaOverageAvailable(quota);
434
+ if (quota && quota.sessionStatus === "rejected" && !overageAvailable) {
434
435
  const reset = resetEpochToMs(quota.sessionResetAt, now) ??
435
436
  (retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_COOLING_PERIOD_MS);
436
437
  return {
@@ -442,7 +443,7 @@ function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.uni
442
443
  // Anthropic may reject the authoritative top-level unified limit while both
443
444
  // 5h and 7d sub-window statuses still say "allowed". Treating this as a
444
445
  // transient burst retries a known-exhausted account and delays failover.
445
- if (unifiedStatus?.trim().toLowerCase() === "rejected") {
446
+ if (unifiedStatus?.trim().toLowerCase() === "rejected" && !overageAvailable) {
446
447
  const reset = retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_HARD_COOLDOWN_MS;
447
448
  return {
448
449
  reason: "unified",
@@ -470,34 +471,53 @@ function minutesUntil(untilMs, now) {
470
471
  * flipped to "rejected" (the boundary request that spends the last of the quota
471
472
  * still returns 200 but reports rejected/next-reset). Parks the account until
472
473
  * its reset so the next request skips it instead of discovering the limit via a
473
- * 429. Never shortens an existing, longer cooldown.
474
+ * 429, except when the provider explicitly enables paid overage.
474
475
  */
475
- function maybeCoolFromQuota(state, quota, now) {
476
+ function reconcileCooldownFromQuota(state, quota, now) {
477
+ const overageAvailable = isQuotaOverageAvailable(quota);
476
478
  let until;
477
479
  let reason;
478
480
  if (quota.weeklyStatus === "rejected") {
479
481
  until = resetEpochToMs(quota.weeklyResetAt, now);
480
482
  reason = "weekly";
481
483
  }
482
- else if (quota.sessionStatus === "rejected") {
484
+ if (until === undefined &&
485
+ overageAvailable &&
486
+ state.coolingUntil &&
487
+ (state.coolingReason === "session" || state.coolingReason === "unified")) {
488
+ const previousCoolingUntil = state.coolingUntil;
489
+ state.coolingUntil = undefined;
490
+ state.coolingReason = undefined;
491
+ logger.always("[proxy] clearing subscription cooldown because Anthropic explicitly permits overage");
492
+ return { kind: "cleared", coolingUntil: previousCoolingUntil };
493
+ }
494
+ if (until === undefined &&
495
+ quota.sessionStatus === "rejected" &&
496
+ !overageAvailable) {
483
497
  until = resetEpochToMs(quota.sessionResetAt, now);
484
498
  reason = "session";
485
499
  }
486
- else if (quota.unifiedStatus === "rejected") {
500
+ else if (until === undefined &&
501
+ quota.unifiedStatus === "rejected" &&
502
+ !overageAvailable) {
487
503
  until = now + DEFAULT_HARD_COOLDOWN_MS;
488
504
  reason = "unified";
489
505
  }
490
506
  if (until === undefined) {
491
- return false;
507
+ return null;
492
508
  }
493
509
  const clamped = clampCooldownUntil(until, now);
494
510
  if (!state.coolingUntil || clamped > state.coolingUntil) {
495
511
  state.coolingUntil = clamped;
496
512
  state.coolingReason = reason;
497
513
  logger.always(`[proxy] proactively cooling account (${reason}) ~${minutesUntil(clamped, now)}m from success-response quota (status rejected)`);
498
- return true;
514
+ return {
515
+ kind: "cooled",
516
+ coolingUntil: clamped,
517
+ coolingReason: reason ?? "unified",
518
+ };
499
519
  }
500
- return false;
520
+ return null;
501
521
  }
502
522
  /**
503
523
  * Seed each account's runtime quota from the persisted snapshots in
@@ -528,6 +548,15 @@ async function seedRuntimeQuotasFromDisk(accounts) {
528
548
  state.coolingUntil = persistedCooldown.coolingUntil;
529
549
  state.coolingReason = persistedCooldown.reason;
530
550
  }
551
+ if (state.quota) {
552
+ const cooldownUpdate = reconcileCooldownFromQuota(state, state.quota, now);
553
+ if (cooldownUpdate?.kind === "cooled") {
554
+ await saveAccountCooldown(account.key, cooldownUpdate.coolingUntil, cooldownUpdate.coolingReason);
555
+ }
556
+ else if (cooldownUpdate?.kind === "cleared") {
557
+ await clearAccountCooldown(account.key, cooldownUpdate.coolingUntil);
558
+ }
559
+ }
531
560
  }
532
561
  }
533
562
  catch {
@@ -594,13 +623,16 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
594
623
  ? (q.weeklyStatus ?? "unknown")
595
624
  : "allowed"
596
625
  : null;
626
+ const overageEligible = isQuotaOverageAvailable(q);
597
627
  const saturated = sessionStatus === "throttled" ||
598
628
  (sessionTicking && (sessionUsed ?? 0) >= sessionSoftLimit);
599
629
  const quotaLastUpdated = q && Number.isFinite(q.lastUpdated) ? q.lastUpdated : null;
600
630
  return {
601
631
  usable: !coolingActive &&
602
632
  weeklyStatus !== "rejected" &&
603
- sessionStatus !== "rejected",
633
+ (sessionStatus !== "rejected" || overageEligible) &&
634
+ (q?.unifiedStatus?.trim().toLowerCase() !== "rejected" ||
635
+ overageEligible),
604
636
  saturated,
605
637
  hasQuota: !!q,
606
638
  quotaLastUpdated,
@@ -609,6 +641,9 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
609
641
  coolingReason: st?.coolingReason ?? null,
610
642
  coolingUntil: st?.coolingUntil ?? 0,
611
643
  unifiedStatus: q?.unifiedStatus ?? null,
644
+ fallbackStatus: q?.fallbackStatus ?? null,
645
+ upgradePaths: q?.upgradePaths ?? null,
646
+ overageEligible,
612
647
  overageStatus: q?.overageStatus ?? null,
613
648
  sessionStatus,
614
649
  sessionUsed,
@@ -735,6 +770,9 @@ function buildRoutingDecision(args) {
735
770
  ? metrics.coolingUntil
736
771
  : null,
737
772
  unifiedStatus: metrics.unifiedStatus,
773
+ fallbackStatus: metrics.fallbackStatus,
774
+ upgradePaths: metrics.upgradePaths,
775
+ overageEligible: metrics.overageEligible,
738
776
  overageStatus: metrics.overageStatus,
739
777
  sessionStatus: metrics.sessionStatus,
740
778
  sessionUsed: metrics.sessionUsed,
@@ -2385,15 +2423,18 @@ async function handleAnthropicSuccessfulResponse(args) {
2385
2423
  if (quota) {
2386
2424
  // Stash the latest quota on runtime state so the next request can pick the
2387
2425
  // account whose window resets soonest (max-utilization) and proactively
2388
- // skip any whose window is already rejected without eating a 429 first.
2426
+ // skip rejected windows unless Anthropic explicitly permits overage.
2389
2427
  accountState.quota = quota;
2390
- if (maybeCoolFromQuota(accountState, quota, Date.now())) {
2391
- const { coolingUntil, coolingReason } = accountState;
2392
- if (coolingUntil !== undefined && coolingReason !== undefined) {
2393
- saveAccountCooldown(account.key, coolingUntil, coolingReason).catch(() => {
2394
- // Non-fatal: cooldown is already active in memory.
2395
- });
2396
- }
2428
+ const cooldownUpdate = reconcileCooldownFromQuota(accountState, quota, Date.now());
2429
+ if (cooldownUpdate?.kind === "cooled") {
2430
+ saveAccountCooldown(account.key, cooldownUpdate.coolingUntil, cooldownUpdate.coolingReason).catch(() => {
2431
+ // Non-fatal: cooldown is already active in memory.
2432
+ });
2433
+ }
2434
+ else if (cooldownUpdate?.kind === "cleared") {
2435
+ clearAccountCooldown(account.key, cooldownUpdate.coolingUntil).catch(() => {
2436
+ // Non-fatal: the next successful response will reconcile again.
2437
+ });
2397
2438
  }
2398
2439
  saveAccountQuota(account.label, quota).catch(() => {
2399
2440
  // Non-fatal: quota persistence is best-effort
@@ -2995,16 +3036,18 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
2995
3036
  const retryQuota = parseQuotaHeaders(retryResp.headers);
2996
3037
  if (retryQuota) {
2997
3038
  // Keep the auth-retry success path in parity with the main success path:
2998
- // stash quota for proactive selection and proactively cool if this
2999
- // response reveals the window flipped to "rejected".
3039
+ // stash quota for proactive selection and reconcile a rejected window.
3000
3040
  accountState.quota = retryQuota;
3001
- if (maybeCoolFromQuota(accountState, retryQuota, Date.now())) {
3002
- const { coolingUntil, coolingReason } = accountState;
3003
- if (coolingUntil !== undefined && coolingReason !== undefined) {
3004
- saveAccountCooldown(account.key, coolingUntil, coolingReason).catch(() => {
3005
- // Non-fatal: cooldown is already active in memory.
3006
- });
3007
- }
3041
+ const cooldownUpdate = reconcileCooldownFromQuota(accountState, retryQuota, Date.now());
3042
+ if (cooldownUpdate?.kind === "cooled") {
3043
+ saveAccountCooldown(account.key, cooldownUpdate.coolingUntil, cooldownUpdate.coolingReason).catch(() => {
3044
+ // Non-fatal: cooldown is already active in memory.
3045
+ });
3046
+ }
3047
+ else if (cooldownUpdate?.kind === "cleared") {
3048
+ clearAccountCooldown(account.key, cooldownUpdate.coolingUntil).catch(() => {
3049
+ // Non-fatal: the next successful response will reconcile again.
3050
+ });
3008
3051
  }
3009
3052
  saveAccountQuota(account.label, retryQuota).catch((error) => {
3010
3053
  logger.debug("[proxy] Failed to persist account quota after auth retry", {
@@ -4461,8 +4504,8 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4461
4504
  // Clear cooling on success — but only if the stored cooldown has already
4462
4505
  // expired, so an older in-flight success can't wipe an active exhaustion
4463
4506
  // cooldown just set by a concurrent 429. The success handler re-applies a
4464
- // cooldown via maybeCoolFromQuota if the fresh quota headers report the
4465
- // window flipped to "rejected" on this very request.
4507
+ // cooldown via reconcileCooldownFromQuota when fresh quota headers
4508
+ // report a rejected window without explicit overage availability.
4466
4509
  if (accountState.coolingUntil &&
4467
4510
  Date.now() >= accountState.coolingUntil) {
4468
4511
  const expiredCooldown = accountState.coolingUntil;
@@ -5114,6 +5157,7 @@ export const __testHooks = {
5114
5157
  resolveHomeIndex,
5115
5158
  maybeResetPrimaryToHome,
5116
5159
  planCooldownFor429,
5160
+ reconcileCooldownFromQuota,
5117
5161
  isPermanentRefreshFailure,
5118
5162
  getStreamFailureDetails,
5119
5163
  trackUpstreamReadableStream,
@@ -445,6 +445,9 @@ export type ProxyAccountRoutingCandidate = {
445
445
  coolingReason: AccountCoolingReason | null;
446
446
  coolingUntil: number | null;
447
447
  unifiedStatus: string | null;
448
+ fallbackStatus?: string | null;
449
+ upgradePaths?: string | null;
450
+ overageEligible?: boolean;
448
451
  overageStatus: string | null;
449
452
  sessionStatus: string | null;
450
453
  sessionUsed: number | null;
@@ -480,6 +483,9 @@ export type ProxyAccountSortMetrics = {
480
483
  coolingReason: AccountCoolingReason | null;
481
484
  coolingUntil: number;
482
485
  unifiedStatus: string | null;
486
+ fallbackStatus?: string | null;
487
+ upgradePaths?: string | null;
488
+ overageEligible?: boolean;
483
489
  overageStatus: string | null;
484
490
  sessionStatus: string | null;
485
491
  sessionUsed: number | null;
@@ -737,6 +743,14 @@ export type AccountCooldownPlan = {
737
743
  * burst), a small number of jittered same-account retries is allowed first. */
738
744
  rotateImmediately: boolean;
739
745
  };
746
+ export type ProxyQuotaCooldownUpdate = {
747
+ kind: "cooled";
748
+ coolingUntil: number;
749
+ coolingReason: AccountCoolingReason;
750
+ } | {
751
+ kind: "cleared";
752
+ coolingUntil: number;
753
+ } | null;
740
754
  export type TransientRateLimitRetryBudget = {
741
755
  coolingUntil: number;
742
756
  retriesClaimed: number;
@@ -921,6 +935,10 @@ export type AccountQuota = {
921
935
  weeklyResetAt: number;
922
936
  /** 0.0-1.0 (from fallback-percentage) */
923
937
  fallbackPercentage: number;
938
+ /** Provider fallback availability, for example "available". */
939
+ fallbackStatus?: string;
940
+ /** Comma-separated provider upgrade paths, for example "overage". */
941
+ upgradePaths?: string;
924
942
  /** "allowed" | "rejected" */
925
943
  overageStatus: string;
926
944
  /** Epoch ms when we last captured this data */
@@ -12,6 +12,14 @@
12
12
  import type { AccountQuota } from "../types/index.js";
13
13
  /** Read and normalize Anthropic's authoritative top-level unified status. */
14
14
  export declare function getUnifiedRateLimitStatus(headers: Headers | Record<string, string>): string | undefined;
15
+ /**
16
+ * Whether Anthropic explicitly permits a request to use overage after a
17
+ * subscription window is exhausted. Fresh responses require all three
18
+ * provider signals. Older persisted snapshots predate the raw fallback and
19
+ * upgrade-path fields, but retain a positive fallback percentage together with
20
+ * an allowed overage status, which is the equivalent provider state.
21
+ */
22
+ export declare function isQuotaOverageAvailable(quota: Pick<AccountQuota, "fallbackPercentage" | "fallbackStatus" | "overageStatus" | "upgradePaths"> | null | undefined): boolean;
15
23
  /**
16
24
  * Parse Anthropic rate-limit / quota headers into an `AccountQuota`.
17
25
  * Returns `null` when key headers are absent.
@@ -39,6 +39,27 @@ export function getUnifiedRateLimitStatus(headers) {
39
39
  const normalized = value?.trim().toLowerCase();
40
40
  return normalized || undefined;
41
41
  }
42
+ /**
43
+ * Whether Anthropic explicitly permits a request to use overage after a
44
+ * subscription window is exhausted. Fresh responses require all three
45
+ * provider signals. Older persisted snapshots predate the raw fallback and
46
+ * upgrade-path fields, but retain a positive fallback percentage together with
47
+ * an allowed overage status, which is the equivalent provider state.
48
+ */
49
+ export function isQuotaOverageAvailable(quota) {
50
+ if (quota?.overageStatus?.trim().toLowerCase() !== "allowed") {
51
+ return false;
52
+ }
53
+ const explicitFallback = quota.fallbackStatus?.trim().toLowerCase();
54
+ const hasExplicitOveragePath = (quota.upgradePaths ?? "")
55
+ .split(",")
56
+ .map((path) => path.trim().toLowerCase())
57
+ .includes("overage");
58
+ if (explicitFallback === "available" && hasExplicitOveragePath) {
59
+ return true;
60
+ }
61
+ return explicitFallback === undefined && (quota.fallbackPercentage ?? 0) > 0;
62
+ }
42
63
  /**
43
64
  * Parse Anthropic rate-limit / quota headers into an `AccountQuota`.
44
65
  * Returns `null` when key headers are absent.
@@ -69,6 +90,8 @@ export function parseQuotaHeaders(headers) {
69
90
  weeklyStatus: getHeader(headers, `${P}unified-7d-status`) ?? "unknown",
70
91
  weeklyResetAt: weeklyResetRaw ? parseInt(weeklyResetRaw, 10) || 0 : 0,
71
92
  fallbackPercentage: fallbackRaw ? parseFloat(fallbackRaw) || 0 : 0,
93
+ fallbackStatus: getHeader(headers, `${P}unified-fallback`) ?? "unknown",
94
+ upgradePaths: getHeader(headers, `${P}unified-upgrade-paths`),
72
95
  overageStatus: getHeader(headers, `${P}unified-overage-status`) ?? "unknown",
73
96
  lastUpdated: Date.now(),
74
97
  };
@@ -87,6 +87,7 @@ function routingCandidateValue(value) {
87
87
  "sessionStatus",
88
88
  "weeklyStatus",
89
89
  ];
90
+ const optionalNullableStringFields = ["fallbackStatus", "upgradePaths"];
90
91
  if (!stringValue(candidate.account) ||
91
92
  typeof candidate.accountType !== "string" ||
92
93
  !ROUTING_ACCOUNT_TYPES.has(candidate.accountType) ||
@@ -97,6 +98,12 @@ function routingCandidateValue(value) {
97
98
  requiredBooleanFields.some((field) => typeof candidate[field] !== "boolean") ||
98
99
  requiredNullableNumberFields.some((field) => !isNullableFiniteNumber(candidate[field])) ||
99
100
  requiredNullableStringFields.some((field) => !isNullableString(candidate[field])) ||
101
+ optionalNullableStringFields.some((field) => field in candidate &&
102
+ candidate[field] !== undefined &&
103
+ !isNullableString(candidate[field])) ||
104
+ ("overageEligible" in candidate &&
105
+ candidate.overageEligible !== undefined &&
106
+ typeof candidate.overageEligible !== "boolean") ||
100
107
  !(candidate.coolingReason === null ||
101
108
  (typeof candidate.coolingReason === "string" &&
102
109
  COOLING_REASONS.has(candidate.coolingReason)))) {
@@ -117,6 +124,9 @@ function routingCandidateValue(value) {
117
124
  coolingReason: candidate.coolingReason,
118
125
  coolingUntil: candidate.coolingUntil,
119
126
  unifiedStatus: candidate.unifiedStatus,
127
+ fallbackStatus: candidate.fallbackStatus,
128
+ upgradePaths: candidate.upgradePaths,
129
+ overageEligible: candidate.overageEligible,
120
130
  overageStatus: candidate.overageStatus,
121
131
  sessionStatus: candidate.sessionStatus,
122
132
  sessionUsed: candidate.sessionUsed,
@@ -12,7 +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 { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyQuotaCooldownUpdate, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
16
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number): AccountAdmissionLease | undefined;
17
17
  declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
18
18
  declare function acquireFirstAvailableAccountAdmission(accountKeys: string[], capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<{
@@ -45,15 +45,23 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
45
45
  * The unified subscription limits expose per-window status + reset:
46
46
  * - weekly (7d) "rejected" → hard cap for the week; cool until the 7d reset.
47
47
  * - session (5h) "rejected" → paced out for this session; cool until the 5h reset.
48
- * Both mean "retrying this account is futile until its window resets" → rotate
49
- * immediately (no same-account retries) and park the account until the ACTUAL
50
- * reset never the legacy 60s hardcap that let us re-hammer a spent account.
48
+ * Both mean "retrying this account is futile until its window resets" unless
49
+ * the provider explicitly enables overage. In that case, the subscription
50
+ * window is exhausted but the account remains usable for paid fallback.
51
51
  *
52
52
  * Anything else (window still "allowed" but momentarily 429'd — a per-minute
53
53
  * burst / acceleration limit) is transient: honor retry-after as a floor,
54
54
  * allow a couple of jittered same-account retries, then a short cooldown.
55
55
  */
56
56
  declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined): AccountCooldownPlan;
57
+ /**
58
+ * Proactively cool an account when a SUCCESS response reveals a window has just
59
+ * flipped to "rejected" (the boundary request that spends the last of the quota
60
+ * still returns 200 but reports rejected/next-reset). Parks the account until
61
+ * its reset so the next request skips it instead of discovering the limit via a
62
+ * 429, except when the provider explicitly enables paid overage.
63
+ */
64
+ declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number): ProxyQuotaCooldownUpdate;
57
65
  /**
58
66
  * Seed each account's runtime quota from the persisted snapshots in
59
67
  * ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
@@ -329,6 +337,7 @@ export declare const __testHooks: {
329
337
  resolveHomeIndex: typeof resolveHomeIndex;
330
338
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
331
339
  planCooldownFor429: typeof planCooldownFor429;
340
+ reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
332
341
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
333
342
  getStreamFailureDetails: typeof getStreamFailureDetails;
334
343
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;