@juspay/neurolink 10.6.5 → 10.7.1

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.
@@ -433,29 +433,101 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
433
433
  const sessionReset = resetEpochToMs(q?.sessionResetAt, now);
434
434
  const sessionTicking = sessionReset !== undefined;
435
435
  const weeklyTicking = weeklyReset !== undefined;
436
- const sessionUsed = sessionTicking ? (q?.sessionUsed ?? 0) : 0;
437
- const weeklyUsed = weeklyTicking ? (q?.weeklyUsed ?? -1) : q ? 0 : -1;
438
- const sessionStatus = sessionTicking
439
- ? (q?.sessionStatus ?? "unknown")
440
- : "allowed";
441
- const weeklyStatus = weeklyTicking
442
- ? (q?.weeklyStatus ?? "unknown")
443
- : "allowed";
436
+ const sessionUsed = q ? (sessionTicking ? (q.sessionUsed ?? 0) : 0) : null;
437
+ const weeklyUsed = q ? (weeklyTicking ? (q.weeklyUsed ?? null) : 0) : null;
438
+ const sessionStatus = q
439
+ ? sessionTicking
440
+ ? (q.sessionStatus ?? "unknown")
441
+ : "allowed"
442
+ : null;
443
+ const weeklyStatus = q
444
+ ? weeklyTicking
445
+ ? (q.weeklyStatus ?? "unknown")
446
+ : "allowed"
447
+ : null;
444
448
  const saturated = sessionStatus === "throttled" ||
445
- (sessionTicking && sessionUsed >= sessionSoftLimit);
449
+ (sessionTicking && (sessionUsed ?? 0) >= sessionSoftLimit);
450
+ const quotaLastUpdated = q && Number.isFinite(q.lastUpdated) ? q.lastUpdated : null;
446
451
  return {
447
452
  usable: !coolingActive &&
448
453
  weeklyStatus !== "rejected" &&
449
454
  sessionStatus !== "rejected",
450
455
  saturated,
451
456
  hasQuota: !!q,
457
+ quotaLastUpdated,
458
+ quotaAgeMs: quotaLastUpdated === null ? null : Math.max(0, now - quotaLastUpdated),
459
+ coolingActive,
460
+ coolingReason: st?.coolingReason ?? null,
452
461
  coolingUntil: st?.coolingUntil ?? 0,
462
+ unifiedStatus: q?.unifiedStatus ?? null,
463
+ overageStatus: q?.overageStatus ?? null,
464
+ sessionStatus,
465
+ sessionUsed,
453
466
  sessionResetBucket: sessionTicking
454
467
  ? Math.floor(sessionReset / sessionResetToleranceMs)
455
468
  : Number.POSITIVE_INFINITY,
456
469
  sessionReset: sessionReset ?? Number.POSITIVE_INFINITY,
470
+ weeklyStatus,
457
471
  weeklyReset: weeklyReset ?? Number.POSITIVE_INFINITY,
458
472
  weeklyUsed,
473
+ weeklyUsedForSort: weeklyUsed ?? -1,
474
+ };
475
+ }
476
+ function compareAccountRoutingFactors(a, b, metricsByKey, primaryKey) {
477
+ const ma = metricsByKey.get(a.key);
478
+ const mb = metricsByKey.get(b.key);
479
+ if (!ma || !mb) {
480
+ return [0, "insertion_order"];
481
+ }
482
+ if (ma.usable !== mb.usable) {
483
+ return [ma.usable ? -1 : 1, "availability"];
484
+ }
485
+ if (!ma.usable && !mb.usable) {
486
+ const au = ma.coolingUntil || Number.POSITIVE_INFINITY;
487
+ const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
488
+ return [
489
+ au === bu ? 0 : au - bu,
490
+ au === bu ? "insertion_order" : "cooldown_recovery",
491
+ ];
492
+ }
493
+ if (ma.hasQuota !== mb.hasQuota) {
494
+ return [ma.hasQuota ? 1 : -1, "quota_probe"];
495
+ }
496
+ if (ma.saturated !== mb.saturated) {
497
+ return [ma.saturated ? 1 : -1, "session_headroom"];
498
+ }
499
+ if (ma.saturated && mb.saturated) {
500
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
501
+ return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
502
+ }
503
+ if (ma.weeklyReset !== mb.weeklyReset) {
504
+ return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
505
+ }
506
+ }
507
+ else {
508
+ if (ma.weeklyReset !== mb.weeklyReset) {
509
+ return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
510
+ }
511
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
512
+ return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
513
+ }
514
+ }
515
+ if (ma.weeklyUsedForSort !== mb.weeklyUsedForSort) {
516
+ return [mb.weeklyUsedForSort - ma.weeklyUsedForSort, "weekly_utilization"];
517
+ }
518
+ if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
519
+ return [a.key === primaryKey ? -1 : 1, "configured_primary"];
520
+ }
521
+ return [0, "insertion_order"];
522
+ }
523
+ function orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs) {
524
+ const metricsByKey = new Map(accounts.map((account) => [
525
+ account.key,
526
+ accountSortMetrics(account.key, now, sessionSoftLimit, sessionResetToleranceMs),
527
+ ]));
528
+ return {
529
+ orderedAccounts: [...accounts].sort((a, b) => compareAccountRoutingFactors(a, b, metricsByKey, primaryKey)[0]),
530
+ metricsByKey,
459
531
  };
460
532
  }
461
533
  /**
@@ -484,48 +556,153 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
484
556
  * last resort.
485
557
  */
486
558
  function orderAccountsByQuota(accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs()) {
487
- const metrics = (key) => accountSortMetrics(key, now, sessionSoftLimit, sessionResetToleranceMs);
488
- return [...accounts].sort((a, b) => {
489
- const ma = metrics(a.key);
490
- const mb = metrics(b.key);
491
- if (ma.usable !== mb.usable) {
492
- return ma.usable ? -1 : 1;
493
- }
494
- if (!ma.usable && !mb.usable) {
495
- const au = ma.coolingUntil || Number.POSITIVE_INFINITY;
496
- const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
497
- return au - bu;
498
- }
499
- if (ma.hasQuota !== mb.hasQuota) {
500
- return ma.hasQuota ? 1 : -1;
559
+ return orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs).orderedAccounts;
560
+ }
561
+ function buildRoutingDecision(args) {
562
+ const { accounts, orderedAccounts, metricsByKey, evaluatedAt, strategy, primaryKey, quotaRoutingEnabled, quotaOrdered, sessionSoftLimit, sessionResetToleranceMs, rotationOffset, } = args;
563
+ const sourceIndexes = new Map(accounts.map((account, index) => [account.key, index]));
564
+ const configuredPrimaryMatched = !!primaryKey && accounts.some((account) => account.key === primaryKey);
565
+ const candidates = [];
566
+ for (const account of orderedAccounts) {
567
+ const metrics = metricsByKey.get(account.key);
568
+ if (!metrics) {
569
+ logger.warn(`[proxy] routing evidence omitted because metrics are missing for account=${account.label}`);
570
+ return undefined;
501
571
  }
502
- if (ma.saturated !== mb.saturated) {
503
- return ma.saturated ? 1 : -1;
572
+ candidates.push({
573
+ account: account.label,
574
+ accountType: account.type,
575
+ sourceIndex: sourceIndexes.get(account.key) ?? candidates.length,
576
+ rank: candidates.length,
577
+ configuredPrimary: !!primaryKey && account.key === primaryKey,
578
+ usable: metrics.usable,
579
+ saturated: metrics.saturated,
580
+ quotaObserved: metrics.hasQuota,
581
+ quotaLastUpdated: metrics.quotaLastUpdated,
582
+ quotaAgeMs: metrics.quotaAgeMs,
583
+ coolingActive: metrics.coolingActive,
584
+ coolingReason: metrics.coolingReason,
585
+ coolingUntil: metrics.coolingUntil > 0 && Number.isFinite(metrics.coolingUntil)
586
+ ? metrics.coolingUntil
587
+ : null,
588
+ unifiedStatus: metrics.unifiedStatus,
589
+ overageStatus: metrics.overageStatus,
590
+ sessionStatus: metrics.sessionStatus,
591
+ sessionUsed: metrics.sessionUsed,
592
+ sessionResetAt: Number.isFinite(metrics.sessionReset)
593
+ ? metrics.sessionReset
594
+ : null,
595
+ sessionResetBucket: Number.isFinite(metrics.sessionResetBucket)
596
+ ? metrics.sessionResetBucket
597
+ : null,
598
+ weeklyStatus: metrics.weeklyStatus,
599
+ weeklyUsed: metrics.weeklyUsed,
600
+ weeklyResetAt: Number.isFinite(metrics.weeklyReset)
601
+ ? metrics.weeklyReset
602
+ : null,
603
+ });
604
+ }
605
+ const initialAccount = orderedAccounts[0];
606
+ let mode;
607
+ let selectionReason;
608
+ if (orderedAccounts.length === 1) {
609
+ mode = "single_account";
610
+ selectionReason = "single_account";
611
+ }
612
+ else if (quotaOrdered) {
613
+ mode = "quota";
614
+ selectionReason = compareAccountRoutingFactors(orderedAccounts[0], orderedAccounts[1], metricsByKey, primaryKey)[1];
615
+ }
616
+ else if (strategy === "round-robin") {
617
+ mode = "round_robin";
618
+ selectionReason = "round_robin";
619
+ }
620
+ else {
621
+ mode = "primary";
622
+ selectionReason =
623
+ configuredPrimaryMatched && initialAccount?.key === primaryKey
624
+ ? "configured_primary"
625
+ : "insertion_order";
626
+ }
627
+ return {
628
+ schemaVersion: 1,
629
+ evaluatedAt: new Date(evaluatedAt).toISOString(),
630
+ strategy,
631
+ mode,
632
+ selectionReason,
633
+ quotaRoutingEnabled,
634
+ quotaInputsUsed: quotaOrdered,
635
+ sessionSoftLimit,
636
+ sessionResetToleranceMs,
637
+ configuredPrimaryAccount: primaryKey ?? null,
638
+ configuredPrimaryMatched,
639
+ rotationOffset,
640
+ initialAccount: initialAccount?.label ?? "",
641
+ candidates,
642
+ };
643
+ }
644
+ function selectClaudeProxyAccountOrder(args) {
645
+ const { enabledAccounts, accountStrategy, primaryAccountKey, quotaRoutingEnabled, sessionSoftLimit, sessionResetToleranceMs, setRoutingDecision, } = args;
646
+ let orderedAccounts = [...enabledAccounts];
647
+ const evaluatedAt = Date.now();
648
+ let metricsByKey;
649
+ let rotationOffset = 0;
650
+ const quotaOrdered = accountStrategy === "fill-first" &&
651
+ orderedAccounts.length > 1 &&
652
+ quotaRoutingEnabled;
653
+ if (!quotaOrdered && accountStrategy === "fill-first") {
654
+ // A hot-reloaded primary change must apply to this request.
655
+ maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey);
656
+ }
657
+ if (quotaOrdered) {
658
+ const quotaOrder = orderAccountsByQuotaWithMetrics(enabledAccounts, evaluatedAt, primaryAccountKey, sessionSoftLimit, sessionResetToleranceMs);
659
+ orderedAccounts = quotaOrder.orderedAccounts;
660
+ metricsByKey = quotaOrder.metricsByKey;
661
+ if (logger.shouldLog("debug")) {
662
+ logger.debug(`[proxy] quota-ordered fill sequence: ${orderedAccounts
663
+ .map((account) => account.label)
664
+ .join(" → ")}`);
504
665
  }
505
- if (ma.saturated && mb.saturated) {
506
- if (ma.sessionResetBucket !== mb.sessionResetBucket) {
507
- return ma.sessionResetBucket - mb.sessionResetBucket;
508
- }
509
- if (ma.weeklyReset !== mb.weeklyReset) {
510
- return ma.weeklyReset - mb.weeklyReset;
511
- }
666
+ }
667
+ else {
668
+ if (accountStrategy === "round-robin" &&
669
+ orderedAccounts.length !== lastKnownAccountCount) {
670
+ primaryAccountIndex = resolveHomeIndex(orderedAccounts, primaryAccountKey);
671
+ lastKnownAccountCount = orderedAccounts.length;
512
672
  }
513
- else {
514
- if (ma.weeklyReset !== mb.weeklyReset) {
515
- return ma.weeklyReset - mb.weeklyReset;
673
+ if (orderedAccounts.length > 1) {
674
+ rotationOffset = primaryAccountIndex % orderedAccounts.length;
675
+ if (accountStrategy === "round-robin") {
676
+ primaryAccountIndex =
677
+ (primaryAccountIndex + 1) % orderedAccounts.length;
516
678
  }
517
- if (ma.sessionResetBucket !== mb.sessionResetBucket) {
518
- return ma.sessionResetBucket - mb.sessionResetBucket;
679
+ if (rotationOffset > 0) {
680
+ const head = orderedAccounts.splice(0, rotationOffset);
681
+ orderedAccounts.push(...head);
519
682
  }
520
683
  }
521
- if (ma.weeklyUsed !== mb.weeklyUsed) {
522
- return mb.weeklyUsed - ma.weeklyUsed;
523
- }
524
- if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
525
- return a.key === primaryKey ? -1 : 1;
526
- }
527
- return 0;
684
+ metricsByKey = new Map(enabledAccounts.map((account) => [
685
+ account.key,
686
+ accountSortMetrics(account.key, evaluatedAt, sessionSoftLimit, sessionResetToleranceMs),
687
+ ]));
688
+ }
689
+ const routingDecision = buildRoutingDecision({
690
+ accounts: enabledAccounts,
691
+ orderedAccounts,
692
+ metricsByKey,
693
+ evaluatedAt,
694
+ strategy: accountStrategy,
695
+ primaryKey: primaryAccountKey,
696
+ quotaRoutingEnabled,
697
+ quotaOrdered,
698
+ sessionSoftLimit,
699
+ sessionResetToleranceMs,
700
+ rotationOffset,
528
701
  });
702
+ if (routingDecision) {
703
+ setRoutingDecision(routingDecision);
704
+ }
705
+ return orderedAccounts;
529
706
  }
530
707
  // ---------------------------------------------------------------------------
531
708
  // OAuth polyfill helpers (extracted to reduce block nesting)
@@ -1447,7 +1624,7 @@ async function handleClaudePassthroughJsonResponse(args) {
1447
1624
  return responseJson;
1448
1625
  }
1449
1626
  async function loadClaudeProxyAccounts(args) {
1450
- const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, } = args;
1627
+ const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, setRoutingDecision, } = args;
1451
1628
  const fs = await import("fs");
1452
1629
  const os = await import("os");
1453
1630
  const accounts = [];
@@ -1616,45 +1793,15 @@ async function loadClaudeProxyAccounts(args) {
1616
1793
  tracer?.end(401, Date.now() - requestStartTime);
1617
1794
  return { response: buildLoggedClaudeError(401, reauthMsg) };
1618
1795
  }
1619
- let orderedAccounts = [...enabledAccounts];
1620
- const quotaOrdered = accountStrategy === "fill-first" &&
1621
- orderedAccounts.length > 1 &&
1622
- quotaRoutingEnabled;
1623
- if (!quotaOrdered && accountStrategy === "fill-first") {
1624
- // Apply the request-scoped home before deriving this request's order. A
1625
- // hot-reloaded primary change must not lag by one request. Round-robin
1626
- // deliberately skips this reset so its rotating index remains strict.
1627
- maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey);
1628
- }
1629
- if (quotaOrdered) {
1630
- // Fill-first with a smart fill order: spend the account whose weekly window
1631
- // expires soonest while temporarily demoting sessions without headroom.
1632
- // Supersedes the static home/primary index.
1633
- orderedAccounts = orderAccountsByQuota(enabledAccounts, Date.now(), primaryAccountKey, sessionSoftLimit, sessionResetToleranceMs);
1634
- if (logger.shouldLog("debug")) {
1635
- logger.debug(`[proxy] quota-ordered fill sequence: ${orderedAccounts
1636
- .map((a) => a.label)
1637
- .join(" → ")}`);
1638
- }
1639
- }
1640
- else {
1641
- if (accountStrategy === "round-robin" &&
1642
- orderedAccounts.length !== lastKnownAccountCount) {
1643
- primaryAccountIndex = resolveHomeIndex(orderedAccounts, primaryAccountKey);
1644
- lastKnownAccountCount = orderedAccounts.length;
1645
- }
1646
- if (orderedAccounts.length > 1) {
1647
- const idx = primaryAccountIndex % orderedAccounts.length;
1648
- if (accountStrategy === "round-robin") {
1649
- primaryAccountIndex =
1650
- (primaryAccountIndex + 1) % orderedAccounts.length;
1651
- }
1652
- if (idx > 0) {
1653
- const head = orderedAccounts.splice(0, idx);
1654
- orderedAccounts.push(...head);
1655
- }
1656
- }
1657
- }
1796
+ const orderedAccounts = selectClaudeProxyAccountOrder({
1797
+ enabledAccounts,
1798
+ accountStrategy,
1799
+ primaryAccountKey,
1800
+ quotaRoutingEnabled,
1801
+ sessionSoftLimit,
1802
+ sessionResetToleranceMs,
1803
+ setRoutingDecision,
1804
+ });
1658
1805
  const normalizedAnthropicBody = normalizeClaudeRequestForAnthropic(body);
1659
1806
  const bodyStr = JSON.stringify(normalizedAnthropicBody);
1660
1807
  const requestStart = Date.now();
@@ -3345,6 +3492,14 @@ function createClaudeRequestRuntimeContext(args) {
3345
3492
  : {}),
3346
3493
  });
3347
3494
  };
3495
+ let routingDecision;
3496
+ const setRoutingDecision = (decision) => {
3497
+ if (routingDecision) {
3498
+ logger.debug(`[claude-proxy] ignored duplicate routing decision for request ${ctx.requestId}`);
3499
+ return;
3500
+ }
3501
+ routingDecision = decision;
3502
+ };
3348
3503
  let finalRequestLogged = false;
3349
3504
  const logFinalRequest = (status, accountLabel, accountType, errorType, errorMessage, extra) => {
3350
3505
  if (finalRequestLogged) {
@@ -3404,6 +3559,7 @@ function createClaudeRequestRuntimeContext(args) {
3404
3559
  ...(traceCtx
3405
3560
  ? { traceId: traceCtx.traceId, spanId: traceCtx.spanId }
3406
3561
  : {}),
3562
+ ...(routingDecision ? { routingDecision } : {}),
3407
3563
  });
3408
3564
  };
3409
3565
  const buildLoggedClaudeError = (status, message, errorType, extra) => {
@@ -3435,6 +3591,7 @@ function createClaudeRequestRuntimeContext(args) {
3435
3591
  logProxyBody,
3436
3592
  logFinalRequest,
3437
3593
  buildLoggedClaudeError,
3594
+ setRoutingDecision,
3438
3595
  };
3439
3596
  }
3440
3597
  function createAnthropicAttemptLogger(args) {
@@ -3803,7 +3960,7 @@ function shouldAttemptClaudeFallback(loopState) {
3803
3960
  return loopState.invalidRequestFailure === null;
3804
3961
  }
3805
3962
  async function handleAnthropicRoutedClaudeRequest(args) {
3806
- const { ctx, body, modelRouter, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, } = args;
3963
+ const { ctx, body, modelRouter, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
3807
3964
  const parsedRequest = parseClaudeRequest(body);
3808
3965
  const loadedAccounts = await loadClaudeProxyAccounts({
3809
3966
  ctx,
@@ -3817,6 +3974,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
3817
3974
  sessionSoftLimit,
3818
3975
  sessionResetToleranceMs,
3819
3976
  buildLoggedClaudeError,
3977
+ setRoutingDecision,
3820
3978
  });
3821
3979
  if ("response" in loadedAccounts) {
3822
3980
  return loadedAccounts.response;
@@ -4293,7 +4451,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
4293
4451
  };
4294
4452
  const clientRequestBody = JSON.stringify(body);
4295
4453
  // 3. Create request runtime context (tracer, loggers, error builder)
4296
- const { tracer, requestStartTime, logProxyBody, logFinalRequest, buildLoggedClaudeError, } = createClaudeRequestRuntimeContext({
4454
+ const { tracer, requestStartTime, logProxyBody, logFinalRequest, buildLoggedClaudeError, setRoutingDecision, } = createClaudeRequestRuntimeContext({
4297
4455
  ctx,
4298
4456
  body,
4299
4457
  clientRequestBody,
@@ -4333,6 +4491,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
4333
4491
  buildLoggedClaudeError,
4334
4492
  logProxyBody,
4335
4493
  logFinalRequest,
4494
+ setRoutingDecision,
4336
4495
  });
4337
4496
  }
4338
4497
  else {
@@ -4726,6 +4885,23 @@ export const __testHooks = {
4726
4885
  getStreamFailureDetails,
4727
4886
  trackUpstreamReadableStream,
4728
4887
  orderAccountsByQuota,
4888
+ buildQuotaRoutingDecision: (accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs()) => {
4889
+ const order = orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs);
4890
+ return buildRoutingDecision({
4891
+ accounts,
4892
+ orderedAccounts: order.orderedAccounts,
4893
+ metricsByKey: order.metricsByKey,
4894
+ evaluatedAt: now,
4895
+ strategy: "fill-first",
4896
+ primaryKey,
4897
+ quotaRoutingEnabled: true,
4898
+ quotaOrdered: accounts.length > 1,
4899
+ sessionSoftLimit,
4900
+ sessionResetToleranceMs,
4901
+ rotationOffset: 0,
4902
+ });
4903
+ },
4904
+ buildRoutingDecision,
4729
4905
  resetEpochToMs,
4730
4906
  seedRuntimeQuotasFromDisk,
4731
4907
  reconcileEligibleAccountRuntimeState,
@@ -18,6 +18,7 @@ import type { Hono } from "hono";
18
18
  import type { Ora } from "ora";
19
19
  import type { MCPToolRegistry } from "../mcp/toolRegistry.js";
20
20
  import type { ProxyTracer } from "../proxy/proxyTracer.js";
21
+ import type { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES } from "../proxy/routingEvidence.js";
21
22
  import type { FallbackEntry, ModelMapping, ProxyRoutingConfig, CloakingConfig } from "./subscription.js";
22
23
  import type { RouteDeprecation } from "./server.js";
23
24
  /**
@@ -423,6 +424,70 @@ export type LoadProxyConfigOptions = {
423
424
  /** Custom environment object (defaults to process.env) */
424
425
  env?: Record<string, string | undefined>;
425
426
  };
427
+ export type ProxyAccountRoutingStrategy = (typeof PROXY_ACCOUNT_ROUTING_STRATEGIES)[number];
428
+ export type ProxyAccountRoutingMode = (typeof PROXY_ACCOUNT_ROUTING_MODES)[number];
429
+ export type ProxyAccountRoutingReason = (typeof PROXY_ACCOUNT_ROUTING_REASONS)[number];
430
+ export type ProxyAccountType = (typeof PROXY_ACCOUNT_TYPES)[number];
431
+ export type ProxyAccountRoutingCandidate = {
432
+ account: string;
433
+ accountType: ProxyAccountType;
434
+ sourceIndex: number;
435
+ rank: number;
436
+ configuredPrimary: boolean;
437
+ usable: boolean;
438
+ saturated: boolean;
439
+ quotaObserved: boolean;
440
+ quotaLastUpdated: number | null;
441
+ quotaAgeMs: number | null;
442
+ coolingActive: boolean;
443
+ coolingReason: AccountCoolingReason | null;
444
+ coolingUntil: number | null;
445
+ unifiedStatus: string | null;
446
+ overageStatus: string | null;
447
+ sessionStatus: string | null;
448
+ sessionUsed: number | null;
449
+ sessionResetAt: number | null;
450
+ sessionResetBucket: number | null;
451
+ weeklyStatus: string | null;
452
+ weeklyUsed: number | null;
453
+ weeklyResetAt: number | null;
454
+ };
455
+ export type ProxyAccountRoutingDecision = {
456
+ schemaVersion: 1;
457
+ evaluatedAt: string;
458
+ strategy: ProxyAccountRoutingStrategy;
459
+ mode: ProxyAccountRoutingMode;
460
+ selectionReason: ProxyAccountRoutingReason;
461
+ quotaRoutingEnabled: boolean;
462
+ quotaInputsUsed: boolean;
463
+ sessionSoftLimit: number;
464
+ sessionResetToleranceMs: number;
465
+ configuredPrimaryAccount: string | null;
466
+ configuredPrimaryMatched: boolean;
467
+ rotationOffset: number;
468
+ initialAccount: string;
469
+ candidates: ProxyAccountRoutingCandidate[];
470
+ };
471
+ export type ProxyAccountSortMetrics = {
472
+ usable: boolean;
473
+ saturated: boolean;
474
+ hasQuota: boolean;
475
+ quotaLastUpdated: number | null;
476
+ quotaAgeMs: number | null;
477
+ coolingActive: boolean;
478
+ coolingReason: AccountCoolingReason | null;
479
+ coolingUntil: number;
480
+ unifiedStatus: string | null;
481
+ overageStatus: string | null;
482
+ sessionStatus: string | null;
483
+ sessionUsed: number | null;
484
+ sessionResetBucket: number;
485
+ sessionReset: number;
486
+ weeklyStatus: string | null;
487
+ weeklyReset: number;
488
+ weeklyUsed: number | null;
489
+ weeklyUsedForSort: number;
490
+ };
426
491
  export type RequestLogEntry = {
427
492
  timestamp: string;
428
493
  requestId: string;
@@ -447,6 +512,8 @@ export type RequestLogEntry = {
447
512
  traceId?: string;
448
513
  /** OTel span ID for correlation with distributed traces */
449
514
  spanId?: string;
515
+ /** Exact secret-free inputs and result of initial account selection. */
516
+ routingDecision?: ProxyAccountRoutingDecision;
450
517
  };
451
518
  export type RequestAttemptLogEntry = {
452
519
  timestamp: string;
@@ -515,6 +582,9 @@ export type ClaudeRequestRuntimeContext = {
515
582
  logFinalRequest: ClaudeFinalRequestLogger;
516
583
  buildLoggedClaudeError: ClaudeLoggedErrorBuilder;
517
584
  };
585
+ export type RoutedClaudeRequestRuntimeContext = ClaudeRequestRuntimeContext & {
586
+ setRoutingDecision: (decision: ProxyAccountRoutingDecision) => void;
587
+ };
518
588
  export type AnthropicAttemptLogger = (status: number, errorType?: string, errorMessage?: string, extra?: {
519
589
  inputTokens?: number;
520
590
  outputTokens?: number;
@@ -838,7 +908,7 @@ export type AccountQuota = {
838
908
  * - "unified" : top-level unified limit rejected — cool for retry-after.
839
909
  * - "transient" : short per-minute/burst 429 — cool for retry-after only.
840
910
  * - "auth" : transient refresh failure with bounded backoff. */
841
- export type AccountCoolingReason = "weekly" | "session" | "unified" | "transient" | "auth";
911
+ export type AccountCoolingReason = (typeof ACCOUNT_COOLING_REASONS)[number];
842
912
  /** Restart-safe cooldown snapshot for one account. */
843
913
  export type PersistedAccountCooldown = {
844
914
  coolingUntil: number;
@@ -872,7 +942,7 @@ export type ProxyPassthroughAccount = {
872
942
  token: string;
873
943
  refreshToken?: string;
874
944
  expiresAt?: number;
875
- type: "oauth" | "api_key";
945
+ type: ProxyAccountType;
876
946
  persistTarget?: TokenPersistTarget;
877
947
  };
878
948
  /** Dependencies for creating Claude proxy routes. */
@@ -1190,6 +1260,7 @@ export type ProxyAnalysisReport = {
1190
1260
  attempts: boolean;
1191
1261
  attemptLatency: boolean;
1192
1262
  cacheUsage: boolean;
1263
+ routingDecisions: boolean;
1193
1264
  };
1194
1265
  dataQuality: {
1195
1266
  linesRead: number;
@@ -1211,6 +1282,11 @@ export type ProxyAnalysisReport = {
1211
1282
  writeFailures: number;
1212
1283
  truncatedCaptures: number;
1213
1284
  };
1285
+ routingDecisions: {
1286
+ valid: number;
1287
+ invalid: number;
1288
+ absent: number;
1289
+ };
1214
1290
  };
1215
1291
  lifecycle: {
1216
1292
  accepted: number;
@@ -1259,6 +1335,17 @@ export type ProxyAnalysisReport = {
1259
1335
  inputTokens: number;
1260
1336
  requestHitRate: number | null;
1261
1337
  };
1338
+ routing: {
1339
+ modes: Record<string, number>;
1340
+ selectionReasons: Record<string, number>;
1341
+ initialAccounts: Record<string, number>;
1342
+ finalAccountChanges: number;
1343
+ finalOutsideCandidateSet: number;
1344
+ /** Number of routing decisions aggregated before sampling records. */
1345
+ totalRecords: number;
1346
+ /** Most recent bounded sample retained for offline inspection. */
1347
+ records: ProxyAnalysisRoutingRecord[];
1348
+ };
1262
1349
  accounts: ProxyAnalysisAccount[];
1263
1350
  };
1264
1351
  /** Inputs for the offline proxy JSONL analyzer. */
@@ -1276,6 +1363,7 @@ export type ProxyAnalysisAttemptRecord = {
1276
1363
  };
1277
1364
  /** Final request fields retained while joining offline proxy log records. */
1278
1365
  export type ProxyAnalysisFinalRequestRecord = {
1366
+ timestamp: string;
1279
1367
  status: number;
1280
1368
  durationMs: number | null;
1281
1369
  account: string;
@@ -1285,6 +1373,16 @@ export type ProxyAnalysisFinalRequestRecord = {
1285
1373
  cacheCreationTokens: number | null;
1286
1374
  errorType: string | null;
1287
1375
  errorCode: string | null;
1376
+ routingDecision: ProxyAccountRoutingDecision | null;
1377
+ };
1378
+ /** Validated account-routing evidence joined to a final request log. */
1379
+ export type ProxyAnalysisRoutingRecord = {
1380
+ requestId: string;
1381
+ timestamp: string;
1382
+ responseStatus: number;
1383
+ finalAccount: string;
1384
+ finalAccountType: string;
1385
+ decision: ProxyAccountRoutingDecision;
1288
1386
  };
1289
1387
  /** Request metadata retained by the HTTP adapter for terminal error logging. */
1290
1388
  export type RuntimeRequestMetadata = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.6.5",
3
+ "version": "10.7.1",
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": {