@juspay/neurolink 12.24.1 → 12.25.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 +2 -2
- package/dist/adapters/providerImageAdapter.js +2 -4
- package/dist/browser/neurolink.min.js +410 -410
- package/dist/cli/commands/proxy.js +30 -0
- package/dist/cli/utils/interactiveSetup.js +1 -1
- package/dist/constants/contextWindows.js +0 -5
- package/dist/constants/enums.d.ts +3 -1
- package/dist/constants/enums.js +2 -0
- package/dist/models/manifests/deepseek.js +3 -3
- package/dist/providers/catalog/deepseek.json +28 -14
- package/dist/providers/catalog/huggingface.json +85 -85
- package/dist/proxy/accountRanking.d.ts +67 -4
- package/dist/proxy/accountRanking.js +188 -8
- package/dist/proxy/proxyAnalysis.js +76 -0
- package/dist/proxy/proxyConfig.d.ts +24 -1
- package/dist/proxy/proxyConfig.js +131 -3
- package/dist/proxy/routingEvidence.d.ts +1 -1
- package/dist/proxy/routingEvidence.js +5 -0
- package/dist/proxy/runtimeConfig.js +15 -0
- package/dist/proxy/sessionAffinity.d.ts +23 -0
- package/dist/proxy/sessionAffinity.js +62 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +48 -3
- package/dist/server/routes/claudeProxyRoutes.js +324 -29
- package/dist/types/proxy.d.ts +48 -0
- package/dist/types/subscription.d.ts +16 -4
- package/docs-site/static/search-index.json +25 -24
- package/package.json +1 -1
|
@@ -24,7 +24,8 @@ import { fetchCodexAccountUsage, listCodexAccountsForUsage, resolveProxyStatusAc
|
|
|
24
24
|
import { AccountQuotaRefreshCoordinator } from "../../proxy/accountQuotaRefreshCoordinator.js";
|
|
25
25
|
import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoordinator.js";
|
|
26
26
|
import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
27
|
-
import { compareExpiryFirst, rankAccounts, } from "../../proxy/accountRanking.js";
|
|
27
|
+
import { applyAffinityAndPrimary, applySpill, compareExpiryFirst, isPrecedenceEligible, rankAccounts, } from "../../proxy/accountRanking.js";
|
|
28
|
+
import { sessionAffinity } from "../../proxy/sessionAffinity.js";
|
|
28
29
|
import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
|
|
29
30
|
import { buildClaudeError, ClaudeStreamSerializer, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
30
31
|
import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
|
|
@@ -1506,17 +1507,24 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
|
|
|
1506
1507
|
weeklyUsedForSort: weeklyUsed ?? -1,
|
|
1507
1508
|
};
|
|
1508
1509
|
}
|
|
1509
|
-
function
|
|
1510
|
-
|
|
1510
|
+
function buildAccountSortMetricsByKey(accounts, now, sessionSoftLimit, sessionResetToleranceMs, requestedModel) {
|
|
1511
|
+
return new Map(accounts.map((account) => [
|
|
1511
1512
|
account.key,
|
|
1512
1513
|
accountSortMetrics(account.key, now, sessionSoftLimit, sessionResetToleranceMs, requestedModel),
|
|
1513
1514
|
]));
|
|
1514
|
-
|
|
1515
|
+
}
|
|
1516
|
+
function orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs, options) {
|
|
1517
|
+
const policy = options?.policy;
|
|
1518
|
+
const metricsByKey = buildAccountSortMetricsByKey(accounts, now, sessionSoftLimit, sessionResetToleranceMs, options?.requestedModel);
|
|
1519
|
+
const { orderedAccounts, reason, affinitySkippedReason } = rankAccounts({
|
|
1515
1520
|
accounts,
|
|
1516
1521
|
metricsByKey,
|
|
1517
1522
|
primaryKey,
|
|
1523
|
+
ranking: policy?.ranking,
|
|
1524
|
+
affinityKey: policy?.affinityKey,
|
|
1525
|
+
preferPrimary: policy?.preferPrimary,
|
|
1518
1526
|
});
|
|
1519
|
-
return { orderedAccounts, metricsByKey };
|
|
1527
|
+
return { orderedAccounts, metricsByKey, reason, affinitySkippedReason };
|
|
1520
1528
|
}
|
|
1521
1529
|
/**
|
|
1522
1530
|
* Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
|
|
@@ -1543,7 +1551,7 @@ function orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftL
|
|
|
1543
1551
|
* last resort.
|
|
1544
1552
|
*/
|
|
1545
1553
|
function orderAccountsByQuota(accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), requestedModel) {
|
|
1546
|
-
return orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs, requestedModel).orderedAccounts;
|
|
1554
|
+
return orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs, { requestedModel }).orderedAccounts;
|
|
1547
1555
|
}
|
|
1548
1556
|
function scheduleAdaptiveQuotaRefreshes(accounts, orderedAccounts, sessionSoftLimit, routingMetrics) {
|
|
1549
1557
|
for (const account of accounts) {
|
|
@@ -1673,7 +1681,9 @@ function buildRoutingDecision(args) {
|
|
|
1673
1681
|
}
|
|
1674
1682
|
else if (quotaOrdered) {
|
|
1675
1683
|
mode = "quota";
|
|
1676
|
-
selectionReason =
|
|
1684
|
+
selectionReason =
|
|
1685
|
+
args.policySelectionReason ??
|
|
1686
|
+
compareExpiryFirst(orderedAccounts[0], orderedAccounts[1], metricsByKey, primaryKey)[1];
|
|
1677
1687
|
}
|
|
1678
1688
|
else if (strategy === "round-robin") {
|
|
1679
1689
|
mode = "round_robin";
|
|
@@ -1682,9 +1692,10 @@ function buildRoutingDecision(args) {
|
|
|
1682
1692
|
else {
|
|
1683
1693
|
mode = "primary";
|
|
1684
1694
|
selectionReason =
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1695
|
+
args.policySelectionReason ??
|
|
1696
|
+
(configuredPrimaryMatched && initialAccount?.key === primaryKey
|
|
1697
|
+
? "configured_primary"
|
|
1698
|
+
: "insertion_order");
|
|
1688
1699
|
}
|
|
1689
1700
|
return {
|
|
1690
1701
|
schemaVersion: 1,
|
|
@@ -1701,10 +1712,54 @@ function buildRoutingDecision(args) {
|
|
|
1701
1712
|
rotationOffset,
|
|
1702
1713
|
initialAccount: initialAccount?.label ?? "",
|
|
1703
1714
|
candidates,
|
|
1715
|
+
...(args.policy ? { policy: args.policy } : {}),
|
|
1716
|
+
...(args.affinity ? { affinity: args.affinity } : {}),
|
|
1717
|
+
...(args.spill ? { spill: args.spill } : {}),
|
|
1704
1718
|
};
|
|
1705
1719
|
}
|
|
1720
|
+
// A policy error can repeat on every request, so each (site, error) pair is
|
|
1721
|
+
// logged once. Keyed per pair rather than once per worker, so a later,
|
|
1722
|
+
// unrelated failure still reaches the log. Insertion order is eviction order.
|
|
1723
|
+
const MAX_LOGGED_ROUTING_POLICY_ERRORS = 32;
|
|
1724
|
+
const loggedRoutingPolicyErrors = new Set();
|
|
1725
|
+
function logRoutingPolicyErrorOnce(error, fallbackOrder) {
|
|
1726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1727
|
+
const signature = JSON.stringify([
|
|
1728
|
+
fallbackOrder,
|
|
1729
|
+
error instanceof Error ? error.name : typeof error,
|
|
1730
|
+
message,
|
|
1731
|
+
]);
|
|
1732
|
+
if (loggedRoutingPolicyErrors.has(signature)) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
loggedRoutingPolicyErrors.add(signature);
|
|
1736
|
+
if (loggedRoutingPolicyErrors.size > MAX_LOGGED_ROUTING_POLICY_ERRORS) {
|
|
1737
|
+
const oldest = loggedRoutingPolicyErrors.values().next().value;
|
|
1738
|
+
if (oldest !== undefined) {
|
|
1739
|
+
loggedRoutingPolicyErrors.delete(oldest);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
logger.always(`[proxy] routing policy threw; falling back to ${fallbackOrder}: ${message}`);
|
|
1743
|
+
}
|
|
1744
|
+
// Test-only: makes maybeSpill() throw before its own guard, including on a
|
|
1745
|
+
// "session_affinity" first choice it would otherwise skip. That is the only
|
|
1746
|
+
// deterministic way to reach the policy catch after a genuine reorder, since
|
|
1747
|
+
// the policy steps return the input order unchanged when nothing applies.
|
|
1748
|
+
let forceSpillThrowForTests = false;
|
|
1749
|
+
// Test-only: make the selection-time affinity lookup, or the post-serve
|
|
1750
|
+
// binding re-check, throw. Each enters its own policy catch without reaching
|
|
1751
|
+
// into the binding store, so the store can stay frozen.
|
|
1752
|
+
let forceAffinityLookupThrowForTests = false;
|
|
1753
|
+
let forceBindRecheckThrowForTests = false;
|
|
1706
1754
|
function selectClaudeProxyAccountOrder(args) {
|
|
1707
|
-
const { enabledAccounts, accountStrategy,
|
|
1755
|
+
const { enabledAccounts, accountStrategy, quotaRoutingEnabled, sessionSoftLimit, sessionResetToleranceMs, requestedModel, sessionId, ranking, preferPrimary, sessionAffinityEnabled, sessionAffinityIdleTtlMs, spillInflight, setRoutingDecision, } = args;
|
|
1756
|
+
// The configured primary arrives normalized (lower-cased, prefixed), while
|
|
1757
|
+
// account keys keep the token store's spelling, and every primary check
|
|
1758
|
+
// below compares with ===. Resolve it to the loaded account's own key once.
|
|
1759
|
+
const configuredPrimaryKey = args.primaryAccountKey;
|
|
1760
|
+
const primaryAccountKey = configuredPrimaryKey === undefined
|
|
1761
|
+
? undefined
|
|
1762
|
+
: (enabledAccounts.find((account) => anthropicAccountKeysEqual(account.key, configuredPrimaryKey))?.key ?? configuredPrimaryKey);
|
|
1708
1763
|
let orderedAccounts = [...enabledAccounts];
|
|
1709
1764
|
const evaluatedAt = Date.now();
|
|
1710
1765
|
let metricsByKey;
|
|
@@ -1712,14 +1767,111 @@ function selectClaudeProxyAccountOrder(args) {
|
|
|
1712
1767
|
const quotaOrdered = accountStrategy === "fill-first" &&
|
|
1713
1768
|
orderedAccounts.length > 1 &&
|
|
1714
1769
|
quotaRoutingEnabled;
|
|
1770
|
+
// strategy: round-robin ignores every routing policy key.
|
|
1771
|
+
const policiesApply = accountStrategy === "fill-first" && orderedAccounts.length > 1;
|
|
1772
|
+
const policy = policiesApply
|
|
1773
|
+
? {
|
|
1774
|
+
ranking,
|
|
1775
|
+
preferPrimary,
|
|
1776
|
+
sessionAffinity: sessionAffinityEnabled,
|
|
1777
|
+
sessionAffinityIdleTtlMs,
|
|
1778
|
+
spillInflight,
|
|
1779
|
+
}
|
|
1780
|
+
: undefined;
|
|
1781
|
+
let policySelectionReason;
|
|
1782
|
+
let affinity;
|
|
1783
|
+
let spill;
|
|
1784
|
+
const resolveAffinityKey = () => {
|
|
1785
|
+
if (!sessionAffinityEnabled || !sessionId) {
|
|
1786
|
+
return undefined;
|
|
1787
|
+
}
|
|
1788
|
+
if (forceAffinityLookupThrowForTests) {
|
|
1789
|
+
throw new Error("forced failure for affinity lookup throw test");
|
|
1790
|
+
}
|
|
1791
|
+
return sessionAffinity.get(sessionId, evaluatedAt, sessionAffinityIdleTtlMs);
|
|
1792
|
+
};
|
|
1793
|
+
const buildAffinityEvidence = (boundAccountKey, applied, skippedFromPrecedence) => ({
|
|
1794
|
+
sessionBound: !!boundAccountKey,
|
|
1795
|
+
boundAccount: (boundAccountKey &&
|
|
1796
|
+
enabledAccounts.find((account) => account.key === boundAccountKey)
|
|
1797
|
+
?.label) ??
|
|
1798
|
+
null,
|
|
1799
|
+
applied,
|
|
1800
|
+
skippedReason: !sessionAffinityEnabled
|
|
1801
|
+
? "disabled"
|
|
1802
|
+
: !sessionId
|
|
1803
|
+
? "no_session"
|
|
1804
|
+
: !boundAccountKey
|
|
1805
|
+
? "expired"
|
|
1806
|
+
: applied
|
|
1807
|
+
? null
|
|
1808
|
+
: skippedFromPrecedence,
|
|
1809
|
+
});
|
|
1810
|
+
const labelOf = (accountKey) => enabledAccounts.find((account) => account.key === accountKey)?.label ??
|
|
1811
|
+
accountKey;
|
|
1812
|
+
const maybeSpill = (order, reason, metrics) => {
|
|
1813
|
+
if (forceSpillThrowForTests) {
|
|
1814
|
+
throw new Error("forced failure for spill throw test");
|
|
1815
|
+
}
|
|
1816
|
+
// Spill never splits a bound session, but a preferred-primary first
|
|
1817
|
+
// choice has no binding and can still be relieved.
|
|
1818
|
+
if (spillInflight <= 0 || reason === "session_affinity") {
|
|
1819
|
+
return undefined;
|
|
1820
|
+
}
|
|
1821
|
+
const inflightByKey = new Map(order.map((account) => [account.key, getAccountInflight(account.key)]));
|
|
1822
|
+
const spillResult = applySpill({
|
|
1823
|
+
orderedAccounts: order,
|
|
1824
|
+
inflightByKey,
|
|
1825
|
+
metricsByKey: metrics,
|
|
1826
|
+
spillInflight,
|
|
1827
|
+
});
|
|
1828
|
+
const move = spillResult.spill;
|
|
1829
|
+
if (!move) {
|
|
1830
|
+
return undefined;
|
|
1831
|
+
}
|
|
1832
|
+
return {
|
|
1833
|
+
orderedAccounts: spillResult.orderedAccounts,
|
|
1834
|
+
spill: {
|
|
1835
|
+
from: labelOf(move.from),
|
|
1836
|
+
to: labelOf(move.to),
|
|
1837
|
+
inflight: move.inflight,
|
|
1838
|
+
},
|
|
1839
|
+
};
|
|
1840
|
+
};
|
|
1715
1841
|
if (!quotaOrdered && accountStrategy === "fill-first") {
|
|
1716
1842
|
// A hot-reloaded primary change must apply to this request.
|
|
1717
1843
|
maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey);
|
|
1718
1844
|
}
|
|
1719
1845
|
if (quotaOrdered) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
metricsByKey =
|
|
1846
|
+
// Metrics are not a policy step: a throw here propagates, as it did
|
|
1847
|
+
// before the policies existed, instead of reading as a policy failure.
|
|
1848
|
+
metricsByKey = buildAccountSortMetricsByKey(enabledAccounts, evaluatedAt, sessionSoftLimit, sessionResetToleranceMs, requestedModel);
|
|
1849
|
+
try {
|
|
1850
|
+
const boundAccountKey = resolveAffinityKey();
|
|
1851
|
+
const quotaOrder = rankAccounts({
|
|
1852
|
+
accounts: enabledAccounts,
|
|
1853
|
+
metricsByKey,
|
|
1854
|
+
primaryKey: primaryAccountKey,
|
|
1855
|
+
ranking,
|
|
1856
|
+
affinityKey: boundAccountKey,
|
|
1857
|
+
preferPrimary,
|
|
1858
|
+
});
|
|
1859
|
+
const spilled = maybeSpill(quotaOrder.orderedAccounts, quotaOrder.reason, metricsByKey);
|
|
1860
|
+
const affinityEvidence = buildAffinityEvidence(boundAccountKey, quotaOrder.reason === "session_affinity", quotaOrder.affinitySkippedReason);
|
|
1861
|
+
orderedAccounts = spilled?.orderedAccounts ?? quotaOrder.orderedAccounts;
|
|
1862
|
+
policySelectionReason = spilled ? "spill_inflight" : quotaOrder.reason;
|
|
1863
|
+
affinity = affinityEvidence;
|
|
1864
|
+
spill = spilled?.spill;
|
|
1865
|
+
}
|
|
1866
|
+
catch (error) {
|
|
1867
|
+
logRoutingPolicyErrorOnce(error, "expiry-first order");
|
|
1868
|
+
orderedAccounts = rankAccounts({
|
|
1869
|
+
accounts: enabledAccounts,
|
|
1870
|
+
metricsByKey,
|
|
1871
|
+
primaryKey: primaryAccountKey,
|
|
1872
|
+
}).orderedAccounts;
|
|
1873
|
+
policySelectionReason = "routing_policy_error";
|
|
1874
|
+
}
|
|
1723
1875
|
if (logger.shouldLog("debug")) {
|
|
1724
1876
|
logger.debug(`[proxy] quota-ordered fill sequence: ${orderedAccounts
|
|
1725
1877
|
.map((account) => account.label)
|
|
@@ -1743,10 +1895,36 @@ function selectClaudeProxyAccountOrder(args) {
|
|
|
1743
1895
|
orderedAccounts.push(...head);
|
|
1744
1896
|
}
|
|
1745
1897
|
}
|
|
1746
|
-
metricsByKey =
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1898
|
+
metricsByKey = buildAccountSortMetricsByKey(enabledAccounts, evaluatedAt, sessionSoftLimit, sessionResetToleranceMs, requestedModel);
|
|
1899
|
+
// Quota routing off: the rotation above stays the base order, and
|
|
1900
|
+
// affinity/prefer-primary/spill still apply on top of it. Nothing is
|
|
1901
|
+
// assigned until every policy step has succeeded, so a throw leaves the
|
|
1902
|
+
// pre-policy order in place.
|
|
1903
|
+
if (policiesApply) {
|
|
1904
|
+
try {
|
|
1905
|
+
const boundAccountKey = resolveAffinityKey();
|
|
1906
|
+
const precedence = applyAffinityAndPrimary({
|
|
1907
|
+
orderedAccounts,
|
|
1908
|
+
metricsByKey,
|
|
1909
|
+
affinityKey: boundAccountKey,
|
|
1910
|
+
primaryKey: primaryAccountKey,
|
|
1911
|
+
preferPrimary,
|
|
1912
|
+
});
|
|
1913
|
+
const spilled = maybeSpill(precedence.orderedAccounts, precedence.reason, metricsByKey);
|
|
1914
|
+
const affinityEvidence = buildAffinityEvidence(boundAccountKey, precedence.reason === "session_affinity", precedence.affinitySkippedReason);
|
|
1915
|
+
orderedAccounts =
|
|
1916
|
+
spilled?.orderedAccounts ?? precedence.orderedAccounts;
|
|
1917
|
+
policySelectionReason = spilled
|
|
1918
|
+
? "spill_inflight"
|
|
1919
|
+
: (precedence.reason ?? undefined);
|
|
1920
|
+
affinity = affinityEvidence;
|
|
1921
|
+
spill = spilled?.spill;
|
|
1922
|
+
}
|
|
1923
|
+
catch (error) {
|
|
1924
|
+
logRoutingPolicyErrorOnce(error, "pre-policy order");
|
|
1925
|
+
policySelectionReason = "routing_policy_error";
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1750
1928
|
}
|
|
1751
1929
|
const routingDecision = buildRoutingDecision({
|
|
1752
1930
|
accounts: enabledAccounts,
|
|
@@ -1760,12 +1938,50 @@ function selectClaudeProxyAccountOrder(args) {
|
|
|
1760
1938
|
sessionSoftLimit,
|
|
1761
1939
|
sessionResetToleranceMs,
|
|
1762
1940
|
rotationOffset,
|
|
1941
|
+
policySelectionReason,
|
|
1942
|
+
policy,
|
|
1943
|
+
affinity,
|
|
1944
|
+
spill,
|
|
1763
1945
|
});
|
|
1764
1946
|
if (routingDecision) {
|
|
1765
1947
|
setRoutingDecision(routingDecision);
|
|
1766
1948
|
}
|
|
1767
1949
|
return { orderedAccounts, metricsByKey };
|
|
1768
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Records that a session's request was served. A session keeps its current
|
|
1953
|
+
* binding only when this request never tried the bound account and that
|
|
1954
|
+
* account is still eligible: a request that overflowed its bound account's
|
|
1955
|
+
* admission cap onto another account must not move the session off its warm
|
|
1956
|
+
* prompt cache. Otherwise the serving account becomes the binding. Tried
|
|
1957
|
+
* outranks eligible because an account that failed this request can look
|
|
1958
|
+
* eligible again by now: a short 429 cooldown can lapse while the next
|
|
1959
|
+
* account is still answering.
|
|
1960
|
+
*/
|
|
1961
|
+
function bindSessionAfterServe(args) {
|
|
1962
|
+
const now = Date.now();
|
|
1963
|
+
// This runs after the response has already been served (and, on the
|
|
1964
|
+
// stream path, after admission-lease ownership has transferred to the
|
|
1965
|
+
// stream): a throw here must never drop that response or leak the lease,
|
|
1966
|
+
// so eligibility re-checks fall back to binding the serving account.
|
|
1967
|
+
let accountKey = args.servingAccountKey;
|
|
1968
|
+
try {
|
|
1969
|
+
if (forceBindRecheckThrowForTests) {
|
|
1970
|
+
throw new Error("forced failure for bind recheck throw test");
|
|
1971
|
+
}
|
|
1972
|
+
const boundKey = sessionAffinity.get(args.sessionId, now, args.idleTtlMs);
|
|
1973
|
+
if (boundKey !== undefined &&
|
|
1974
|
+
!args.attemptedAccountKeys.has(boundKey) &&
|
|
1975
|
+
args.enabledAccounts.some((account) => account.key === boundKey) &&
|
|
1976
|
+
isPrecedenceEligible(accountSortMetrics(boundKey, now, args.sessionSoftLimit, args.sessionResetToleranceMs, args.requestedModel))) {
|
|
1977
|
+
accountKey = boundKey;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
catch (error) {
|
|
1981
|
+
logRoutingPolicyErrorOnce(error, "serving-account binding");
|
|
1982
|
+
}
|
|
1983
|
+
sessionAffinity.bind(args.sessionId, accountKey, now);
|
|
1984
|
+
}
|
|
1769
1985
|
// ---------------------------------------------------------------------------
|
|
1770
1986
|
// OAuth polyfill helpers (extracted to reduce block nesting)
|
|
1771
1987
|
// ---------------------------------------------------------------------------
|
|
@@ -3254,7 +3470,7 @@ function earliestShareRecoverySeconds(views, now) {
|
|
|
3254
3470
|
return Math.max(1, Math.round((Math.min(...resets) - now) / 1000));
|
|
3255
3471
|
}
|
|
3256
3472
|
async function loadClaudeProxyAccounts(args) {
|
|
3257
|
-
const { ctx, body, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), setRoutingDecision, } = args;
|
|
3473
|
+
const { ctx, body, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), ranking = "expiry-first", preferPrimary = false, sessionAffinityEnabled = false, sessionAffinityIdleTtlMs = 3_600_000, spillInflight = 0, setRoutingDecision, } = args;
|
|
3258
3474
|
const fs = await import("fs");
|
|
3259
3475
|
const os = await import("os");
|
|
3260
3476
|
const accounts = [];
|
|
@@ -3510,6 +3726,13 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3510
3726
|
},
|
|
3511
3727
|
};
|
|
3512
3728
|
}
|
|
3729
|
+
// Resolved ahead of account selection (rather than after, as before) so
|
|
3730
|
+
// sessionId is available to selectClaudeProxyAccountOrder for session
|
|
3731
|
+
// affinity. normalizeClaudeRequestForAnthropic below returns a
|
|
3732
|
+
// shallow clone and never mutates `body`, so extracting earlier sees
|
|
3733
|
+
// exactly the same snapshot extractSnapshotBody always saw.
|
|
3734
|
+
const clientHeaders = ctx.headers ?? {};
|
|
3735
|
+
const clientSnapshotBody = extractSnapshotBody(body);
|
|
3513
3736
|
const { orderedAccounts, metricsByKey } = selectClaudeProxyAccountOrder({
|
|
3514
3737
|
enabledAccounts,
|
|
3515
3738
|
accountStrategy,
|
|
@@ -3518,6 +3741,12 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3518
3741
|
sessionSoftLimit,
|
|
3519
3742
|
sessionResetToleranceMs,
|
|
3520
3743
|
requestedModel: typeof body.model === "string" ? body.model : undefined,
|
|
3744
|
+
sessionId: clientSnapshotBody?.sessionId,
|
|
3745
|
+
ranking,
|
|
3746
|
+
preferPrimary,
|
|
3747
|
+
sessionAffinityEnabled,
|
|
3748
|
+
sessionAffinityIdleTtlMs,
|
|
3749
|
+
spillInflight,
|
|
3521
3750
|
setRoutingDecision,
|
|
3522
3751
|
});
|
|
3523
3752
|
if (accountStrategy === "fill-first" &&
|
|
@@ -3530,8 +3759,6 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3530
3759
|
const requestStart = Date.now();
|
|
3531
3760
|
const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
3532
3761
|
const url = "https://api.anthropic.com/v1/messages?beta=true";
|
|
3533
|
-
const clientHeaders = ctx.headers ?? {};
|
|
3534
|
-
const clientSnapshotBody = extractSnapshotBody(body);
|
|
3535
3762
|
return {
|
|
3536
3763
|
accounts,
|
|
3537
3764
|
enabledAccounts,
|
|
@@ -3542,6 +3769,7 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3542
3769
|
url,
|
|
3543
3770
|
clientHeaders,
|
|
3544
3771
|
isClaudeClientRequest: isLikelyClaudeClient(clientHeaders, clientSnapshotBody),
|
|
3772
|
+
sessionId: clientSnapshotBody?.sessionId,
|
|
3545
3773
|
};
|
|
3546
3774
|
}
|
|
3547
3775
|
/**
|
|
@@ -4658,7 +4886,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
4658
4886
|
responseStatus: 502,
|
|
4659
4887
|
durationMs: Date.now() - requestStartTime,
|
|
4660
4888
|
});
|
|
4661
|
-
return { response: clientError };
|
|
4889
|
+
return { response: clientError, served: false };
|
|
4662
4890
|
}
|
|
4663
4891
|
const reader = response.body.getReader();
|
|
4664
4892
|
const preflight = await preflightAnthropicStream(reader);
|
|
@@ -4695,6 +4923,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
4695
4923
|
errorType: "stream_error",
|
|
4696
4924
|
message,
|
|
4697
4925
|
}),
|
|
4926
|
+
served: false,
|
|
4698
4927
|
};
|
|
4699
4928
|
}
|
|
4700
4929
|
if (preflight.kind === "empty") {
|
|
@@ -4858,7 +5087,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
4858
5087
|
logFinalRequest,
|
|
4859
5088
|
});
|
|
4860
5089
|
void telemetryDone.then(() => onStreamTerminal?.(), () => onStreamTerminal?.());
|
|
4861
|
-
return { response: result, holdsAccountAdmission: true };
|
|
5090
|
+
return { response: result, holdsAccountAdmission: true, served: true };
|
|
4862
5091
|
}
|
|
4863
5092
|
function getStreamFailureDetails(outcome) {
|
|
4864
5093
|
if (outcome.kind === "upstream_error") {
|
|
@@ -5235,7 +5464,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
|
|
|
5235
5464
|
cacheReadTokens: noTracerUsage?.cache_read_input_tokens,
|
|
5236
5465
|
});
|
|
5237
5466
|
}
|
|
5238
|
-
return { response: responseJson };
|
|
5467
|
+
return { response: responseJson, served: true };
|
|
5239
5468
|
}
|
|
5240
5469
|
async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
|
|
5241
5470
|
const { ctx, account, accountState, requestedModel, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
|
|
@@ -5330,7 +5559,7 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
|
|
|
5330
5559
|
return retryJson;
|
|
5331
5560
|
}
|
|
5332
5561
|
async function handleAnthropicAuthRetry(args) {
|
|
5333
|
-
const { ctx, body, account, accountState, headers, buildUpstreamBody, url, enabledAccounts, orderedAccounts, tracer, requestStartTime, allocateAttemptNumber, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, lastError, authFailureMessage, entitlementFailure, sawRateLimit, sawTransientFailure, sawNetworkError, } = args;
|
|
5562
|
+
const { ctx, body, account, accountState, headers, buildUpstreamBody, url, enabledAccounts, orderedAccounts, tracer, requestStartTime, allocateAttemptNumber, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, lastError, authFailureMessage, entitlementFailure, sawRateLimit, sawTransientFailure, sawNetworkError, bindServedSession, } = args;
|
|
5334
5563
|
recordAttemptError(account.label, account.type, 401);
|
|
5335
5564
|
logAttempt(401, "authentication_error", "received 401 from Anthropic", {
|
|
5336
5565
|
retryable: true,
|
|
@@ -5432,6 +5661,10 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5432
5661
|
logProxyBody,
|
|
5433
5662
|
logFinalRequest,
|
|
5434
5663
|
}),
|
|
5664
|
+
// handleAnthropicSuccessfulNonStreamRetryResponse is only ever
|
|
5665
|
+
// called when retryResp.ok — unlike the streaming path, it has
|
|
5666
|
+
// no terminal-error return sharing this shape.
|
|
5667
|
+
served: true,
|
|
5435
5668
|
};
|
|
5436
5669
|
if ("retryNextAccount" in successResult) {
|
|
5437
5670
|
const failure = successResult.failure;
|
|
@@ -5450,6 +5683,9 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5450
5683
|
upstreamSpan: undefined,
|
|
5451
5684
|
};
|
|
5452
5685
|
}
|
|
5686
|
+
if (bindServedSession && successResult.served) {
|
|
5687
|
+
bindServedSession(account.key);
|
|
5688
|
+
}
|
|
5453
5689
|
return {
|
|
5454
5690
|
response: successResult.response,
|
|
5455
5691
|
holdsAccountAdmission: successResult.holdsAccountAdmission,
|
|
@@ -6750,7 +6986,7 @@ function buildDeferredClaudeAccountFailureResponse(args) {
|
|
|
6750
6986
|
return buildLoggedClaudeError(failure.status, failure.message, failure.errorType);
|
|
6751
6987
|
}
|
|
6752
6988
|
async function handleAnthropicRoutedClaudeRequest(args) {
|
|
6753
|
-
const { ctx, body, modelRouter, configGeneration, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
|
|
6989
|
+
const { ctx, body, modelRouter, configGeneration, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), ranking = "expiry-first", preferPrimary = false, sessionAffinityEnabled = false, sessionAffinityIdleTtlMs = 3_600_000, spillInflight = 0, buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
|
|
6754
6990
|
const parsedRequest = parseClaudeRequest(body);
|
|
6755
6991
|
const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedRequest);
|
|
6756
6992
|
const hasConfiguredFallback = configuredFallbackPlan.attempts
|
|
@@ -6765,6 +7001,11 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6765
7001
|
quotaRoutingEnabled,
|
|
6766
7002
|
sessionSoftLimit,
|
|
6767
7003
|
sessionResetToleranceMs,
|
|
7004
|
+
ranking,
|
|
7005
|
+
preferPrimary,
|
|
7006
|
+
sessionAffinityEnabled,
|
|
7007
|
+
sessionAffinityIdleTtlMs,
|
|
7008
|
+
spillInflight,
|
|
6768
7009
|
setRoutingDecision,
|
|
6769
7010
|
});
|
|
6770
7011
|
if ("failure" in loadedAccounts) {
|
|
@@ -6826,7 +7067,27 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6826
7067
|
buildLoggedClaudeError,
|
|
6827
7068
|
});
|
|
6828
7069
|
}
|
|
6829
|
-
const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, } = loadedAccounts;
|
|
7070
|
+
const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, sessionId, } = loadedAccounts;
|
|
7071
|
+
// Accounts this request sent an upstream attempt to; one skipped only for
|
|
7072
|
+
// its admission cap is not among them.
|
|
7073
|
+
const attemptedAccountKeys = new Set();
|
|
7074
|
+
// Only fill-first over more than one account reads a binding (see
|
|
7075
|
+
// selectClaudeProxyAccountOrder), so nothing else records one.
|
|
7076
|
+
const bindServedSession = sessionAffinityEnabled &&
|
|
7077
|
+
sessionId &&
|
|
7078
|
+
accountStrategy === "fill-first" &&
|
|
7079
|
+
enabledAccounts.length > 1
|
|
7080
|
+
? (servingAccountKey) => bindSessionAfterServe({
|
|
7081
|
+
sessionId,
|
|
7082
|
+
servingAccountKey,
|
|
7083
|
+
attemptedAccountKeys,
|
|
7084
|
+
enabledAccounts,
|
|
7085
|
+
idleTtlMs: sessionAffinityIdleTtlMs,
|
|
7086
|
+
sessionSoftLimit,
|
|
7087
|
+
sessionResetToleranceMs,
|
|
7088
|
+
requestedModel: typeof body.model === "string" ? body.model : undefined,
|
|
7089
|
+
})
|
|
7090
|
+
: undefined;
|
|
6830
7091
|
// Snapshot the operator policy once. Reading the module value later would let
|
|
6831
7092
|
// a concurrent /limits call or a hot config reload change this request's
|
|
6832
7093
|
// answer partway through its own account loop.
|
|
@@ -6999,6 +7260,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6999
7260
|
}
|
|
7000
7261
|
continue accountLoop;
|
|
7001
7262
|
}
|
|
7263
|
+
attemptedAccountKeys.add(account.key);
|
|
7002
7264
|
let admissionTransferredToStream = false;
|
|
7003
7265
|
try {
|
|
7004
7266
|
let fetchResult;
|
|
@@ -7164,6 +7426,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
7164
7426
|
sawRateLimit: loopState.sawRateLimit,
|
|
7165
7427
|
sawTransientFailure: loopState.sawTransientFailure,
|
|
7166
7428
|
sawNetworkError: loopState.sawNetworkError,
|
|
7429
|
+
bindServedSession,
|
|
7167
7430
|
});
|
|
7168
7431
|
loopState.lastError = authRetryResult.lastError;
|
|
7169
7432
|
loopState.authFailureMessage = authRetryResult.authFailureMessage;
|
|
@@ -7281,6 +7544,9 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
7281
7544
|
}
|
|
7282
7545
|
admissionTransferredToStream =
|
|
7283
7546
|
successResult.holdsAccountAdmission === true;
|
|
7547
|
+
if (bindServedSession && successResult.served) {
|
|
7548
|
+
bindServedSession(account.key);
|
|
7549
|
+
}
|
|
7284
7550
|
return successResult.response;
|
|
7285
7551
|
}
|
|
7286
7552
|
finally {
|
|
@@ -7539,6 +7805,11 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7539
7805
|
sessionSoftLimit: getSessionSoftLimit(),
|
|
7540
7806
|
sessionResetToleranceMs: getSessionResetToleranceMs(),
|
|
7541
7807
|
useOverage: "auto",
|
|
7808
|
+
accountRanking: "expiry-first",
|
|
7809
|
+
preferPrimary: false,
|
|
7810
|
+
sessionAffinity: false,
|
|
7811
|
+
sessionAffinityIdleTtlMs: 3_600_000,
|
|
7812
|
+
spillInflight: 0,
|
|
7542
7813
|
};
|
|
7543
7814
|
setOveragePolicy(requestRouting.useOverage);
|
|
7544
7815
|
const requestModelRouter = requestRouting.modelRouter;
|
|
@@ -7612,6 +7883,11 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7612
7883
|
quotaRoutingEnabled: requestRouting.quotaRoutingEnabled,
|
|
7613
7884
|
sessionSoftLimit: requestRouting.sessionSoftLimit,
|
|
7614
7885
|
sessionResetToleranceMs: requestRouting.sessionResetToleranceMs,
|
|
7886
|
+
ranking: requestRouting.accountRanking,
|
|
7887
|
+
preferPrimary: requestRouting.preferPrimary,
|
|
7888
|
+
sessionAffinityEnabled: requestRouting.sessionAffinity,
|
|
7889
|
+
sessionAffinityIdleTtlMs: requestRouting.sessionAffinityIdleTtlMs,
|
|
7890
|
+
spillInflight: requestRouting.spillInflight,
|
|
7615
7891
|
buildLoggedClaudeError,
|
|
7616
7892
|
logProxyBody,
|
|
7617
7893
|
logFinalRequest,
|
|
@@ -8822,8 +9098,8 @@ export const __testHooks = {
|
|
|
8822
9098
|
scheduleAdaptiveQuotaRefreshes,
|
|
8823
9099
|
scheduleHandoffQuotaRefresh,
|
|
8824
9100
|
getQuotaRefreshState: (key) => accountQuotaRefreshCoordinator.getState(key),
|
|
8825
|
-
buildQuotaRoutingDecision: (accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(),
|
|
8826
|
-
const order = orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs,
|
|
9101
|
+
buildQuotaRoutingDecision: (accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), options) => {
|
|
9102
|
+
const order = orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs, options);
|
|
8827
9103
|
return buildRoutingDecision({
|
|
8828
9104
|
accounts,
|
|
8829
9105
|
orderedAccounts: order.orderedAccounts,
|
|
@@ -8836,6 +9112,7 @@ export const __testHooks = {
|
|
|
8836
9112
|
sessionSoftLimit,
|
|
8837
9113
|
sessionResetToleranceMs,
|
|
8838
9114
|
rotationOffset: 0,
|
|
9115
|
+
policySelectionReason: order.reason,
|
|
8839
9116
|
});
|
|
8840
9117
|
},
|
|
8841
9118
|
buildRoutingDecision,
|
|
@@ -8862,10 +9139,28 @@ export const __testHooks = {
|
|
|
8862
9139
|
accountAdmissionStates.clear();
|
|
8863
9140
|
accountQuotaRefreshCoordinator.clear();
|
|
8864
9141
|
providerTransportCoordinator.clear();
|
|
9142
|
+
loggedRoutingPolicyErrors.clear();
|
|
9143
|
+
forceSpillThrowForTests = false;
|
|
9144
|
+
forceAffinityLookupThrowForTests = false;
|
|
9145
|
+
forceBindRecheckThrowForTests = false;
|
|
8865
9146
|
primaryAccountIndex = 0;
|
|
8866
9147
|
lastKnownAccountCount = 0;
|
|
8867
9148
|
},
|
|
8868
9149
|
polyfillOAuthBody: (bodyStr, isClaudeClientRequest) => polyfillOAuthBody(bodyStr, "test-account-token", null, isClaudeClientRequest),
|
|
9150
|
+
// Pins the sessionId path loadClaudeProxyAccounts resolves ahead of
|
|
9151
|
+
// account selection and feeds into selectClaudeProxyAccountOrder for
|
|
9152
|
+
// session affinity.
|
|
9153
|
+
extractSnapshotBodySessionIdForTests: (body) => extractSnapshotBody(body)?.sessionId,
|
|
9154
|
+
selectClaudeProxyAccountOrderForTests: selectClaudeProxyAccountOrder,
|
|
9155
|
+
setForceSpillThrowForTests: (value) => {
|
|
9156
|
+
forceSpillThrowForTests = value;
|
|
9157
|
+
},
|
|
9158
|
+
setForceAffinityLookupThrowForTests: (value) => {
|
|
9159
|
+
forceAffinityLookupThrowForTests = value;
|
|
9160
|
+
},
|
|
9161
|
+
setForceBindRecheckThrowForTests: (value) => {
|
|
9162
|
+
forceBindRecheckThrowForTests = value;
|
|
9163
|
+
},
|
|
8869
9164
|
isAntiAbuseConstruction429,
|
|
8870
9165
|
fetchAnthropicAccountResponse,
|
|
8871
9166
|
finalizeAnthropicTerminalFetchError,
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -534,6 +534,9 @@ export type ProxyAccountRoutingDecision = {
|
|
|
534
534
|
rotationOffset: number;
|
|
535
535
|
initialAccount: string;
|
|
536
536
|
candidates: ProxyAccountRoutingCandidate[];
|
|
537
|
+
policy?: ProxyRoutingPolicySnapshot;
|
|
538
|
+
affinity?: ProxyAccountRoutingAffinityEvidence;
|
|
539
|
+
spill?: ProxyAccountRoutingSpillEvidence;
|
|
537
540
|
};
|
|
538
541
|
export type ProxyAccountSortMetrics = {
|
|
539
542
|
usable: boolean;
|
|
@@ -578,6 +581,36 @@ export type ProxyAccountSortMetrics = {
|
|
|
578
581
|
scopedUsedForSort: number;
|
|
579
582
|
scopedSaturated: boolean;
|
|
580
583
|
};
|
|
584
|
+
export type ProxyAccountRankingPolicy = "expiry-first" | "headroom-first";
|
|
585
|
+
export type ProxyAccountRoutingAffinitySkipReason = "unusable" | "session_saturated" | "expired" | "no_session" | "disabled";
|
|
586
|
+
/** The five routing.* values in effect for one request, as reported on the decision. */
|
|
587
|
+
export type ProxyRoutingPolicySnapshot = {
|
|
588
|
+
ranking: ProxyAccountRankingPolicy;
|
|
589
|
+
preferPrimary: boolean;
|
|
590
|
+
sessionAffinity: boolean;
|
|
591
|
+
sessionAffinityIdleTtlMs: number;
|
|
592
|
+
spillInflight: number;
|
|
593
|
+
};
|
|
594
|
+
export type ProxyAccountRoutingAffinityEvidence = {
|
|
595
|
+
sessionBound: boolean;
|
|
596
|
+
boundAccount: string | null;
|
|
597
|
+
applied: boolean;
|
|
598
|
+
skippedReason: ProxyAccountRoutingAffinitySkipReason | null;
|
|
599
|
+
};
|
|
600
|
+
export type ProxyAccountRoutingSpillEvidence = {
|
|
601
|
+
from: string;
|
|
602
|
+
to: string;
|
|
603
|
+
inflight: number;
|
|
604
|
+
};
|
|
605
|
+
/** Why the precedence step skipped a bound account it could see. */
|
|
606
|
+
export type ProxyAffinityPrecedenceSkipReason = Extract<ProxyAccountRoutingAffinitySkipReason, "unusable" | "session_saturated">;
|
|
607
|
+
/** The reasons a precedence step (affinity, then prefer-primary) places rank 0. */
|
|
608
|
+
export type ProxyAccountPrecedenceReason = Extract<ProxyAccountRoutingReason, "session_affinity" | "preferred_primary">;
|
|
609
|
+
/**
|
|
610
|
+
* A spill decision in account keys; the route reports it with account labels
|
|
611
|
+
* as ProxyAccountRoutingSpillEvidence.
|
|
612
|
+
*/
|
|
613
|
+
export type ProxyAccountSpillMove = ProxyAccountRoutingSpillEvidence;
|
|
581
614
|
export type RequestLogEntry = {
|
|
582
615
|
/** Whether model identifies observed serving output rather than a routing target. */
|
|
583
616
|
servingModelStatus?: "observed" | "unavailable";
|
|
@@ -1075,6 +1108,7 @@ export type LoadedClaudeAccountContext = {
|
|
|
1075
1108
|
url: string;
|
|
1076
1109
|
clientHeaders: Record<string, string | undefined>;
|
|
1077
1110
|
isClaudeClientRequest: boolean;
|
|
1111
|
+
sessionId?: string;
|
|
1078
1112
|
};
|
|
1079
1113
|
export type AnthropicSuccessResult = {
|
|
1080
1114
|
retryNextAccount: true;
|
|
@@ -1086,6 +1120,15 @@ export type AnthropicSuccessResult = {
|
|
|
1086
1120
|
} | {
|
|
1087
1121
|
response: Response | unknown;
|
|
1088
1122
|
holdsAccountAdmission?: boolean;
|
|
1123
|
+
/**
|
|
1124
|
+
* True only when an Anthropic account genuinely produced this response.
|
|
1125
|
+
* False for a same-shaped terminal error synthesized locally (e.g. no
|
|
1126
|
+
* upstream body, or the stream failing before its first chunk) — these
|
|
1127
|
+
* reuse the `{ response }` shape because the client still gets a
|
|
1128
|
+
* response, but no account served anything, so session affinity must
|
|
1129
|
+
* not bind on them.
|
|
1130
|
+
*/
|
|
1131
|
+
served: boolean;
|
|
1089
1132
|
};
|
|
1090
1133
|
/** A release handle for one in-flight request admitted to an OAuth account. */
|
|
1091
1134
|
export type AccountAdmissionLease = {
|
|
@@ -3119,6 +3162,11 @@ export type ProxyRequestRoutingSnapshot = {
|
|
|
3119
3162
|
/** Operator policy on spending paid extra usage once a subscription window is
|
|
3120
3163
|
* spent. Only "never" can override the provider's own signal. */
|
|
3121
3164
|
useOverage: ProxyOveragePolicy;
|
|
3165
|
+
accountRanking: ProxyAccountRankingPolicy;
|
|
3166
|
+
preferPrimary: boolean;
|
|
3167
|
+
sessionAffinity: boolean;
|
|
3168
|
+
sessionAffinityIdleTtlMs: number;
|
|
3169
|
+
spillInflight: number;
|
|
3122
3170
|
};
|
|
3123
3171
|
/** Operator policy for paid extra usage. */
|
|
3124
3172
|
export type ProxyOveragePolicy = "auto" | "always" | "never";
|