@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +355 -355
- package/dist/cli/commands/proxyAnalyze.js +12 -0
- package/dist/lib/proxy/accountCooldown.js +2 -7
- package/dist/lib/proxy/proxyAnalysis.js +215 -0
- package/dist/lib/proxy/requestLogger.js +11 -0
- package/dist/lib/proxy/routingEvidence.d.ts +6 -0
- package/dist/lib/proxy/routingEvidence.js +33 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +16 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +262 -86
- package/dist/lib/types/proxy.d.ts +100 -2
- package/dist/proxy/accountCooldown.js +2 -7
- package/dist/proxy/proxyAnalysis.js +215 -0
- package/dist/proxy/requestLogger.js +11 -0
- package/dist/proxy/routingEvidence.d.ts +6 -0
- package/dist/proxy/routingEvidence.js +32 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +16 -1
- package/dist/server/routes/claudeProxyRoutes.js +262 -86
- package/dist/types/proxy.d.ts +100 -2
- package/package.json +1 -1
|
@@ -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
|
|
437
|
-
const weeklyUsed = weeklyTicking ? (q
|
|
438
|
-
const sessionStatus =
|
|
439
|
-
?
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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 (
|
|
518
|
-
|
|
679
|
+
if (rotationOffset > 0) {
|
|
680
|
+
const head = orderedAccounts.splice(0, rotationOffset);
|
|
681
|
+
orderedAccounts.push(...head);
|
|
519
682
|
}
|
|
520
683
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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 =
|
|
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:
|
|
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 = {
|
|
@@ -2,15 +2,10 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { AsyncMutex } from "../utils/asyncMutex.js";
|
|
5
|
+
import { ACCOUNT_COOLING_REASONS } from "./routingEvidence.js";
|
|
5
6
|
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
|
|
6
7
|
const COOLDOWN_FILE = "account-cooldowns.json";
|
|
7
|
-
const VALID_REASONS = new Set(
|
|
8
|
-
"weekly",
|
|
9
|
-
"session",
|
|
10
|
-
"unified",
|
|
11
|
-
"transient",
|
|
12
|
-
"auth",
|
|
13
|
-
]);
|
|
8
|
+
const VALID_REASONS = new Set(ACCOUNT_COOLING_REASONS);
|
|
14
9
|
let customCooldownFilePath = null;
|
|
15
10
|
let cacheLoaded = false;
|
|
16
11
|
let cacheLoadPromise = null;
|