@juspay/neurolink 11.2.0 → 11.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/auth/codexOAuth.d.ts +67 -0
- package/dist/auth/codexOAuth.js +202 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/browser/neurolink.min.js +419 -419
- package/dist/cli/commands/auth.d.ts +27 -8
- package/dist/cli/commands/auth.js +425 -6
- package/dist/cli/commands/proxy.js +230 -5
- package/dist/cli/factories/authCommandFactory.d.ts +8 -0
- package/dist/cli/factories/authCommandFactory.js +74 -1
- package/dist/lib/auth/codexOAuth.d.ts +67 -0
- package/dist/lib/auth/codexOAuth.js +203 -0
- package/dist/lib/auth/index.d.ts +1 -0
- package/dist/lib/auth/index.js +4 -0
- package/dist/lib/providers/openaiChatCompletionsBase.js +26 -14
- package/dist/lib/proxy/accountCooldown.js +35 -2
- package/dist/lib/proxy/accountQuota.d.ts +29 -3
- package/dist/lib/proxy/accountQuota.js +203 -12
- package/dist/lib/proxy/accountUsage.js +15 -2
- package/dist/lib/proxy/codexAccountUsage.d.ts +26 -0
- package/dist/lib/proxy/codexAccountUsage.js +174 -0
- package/dist/lib/proxy/proxyAnalysis.js +12 -1
- package/dist/lib/proxy/proxyConfig.js +24 -0
- package/dist/lib/proxy/routingEvidence.d.ts +12 -1
- package/dist/lib/proxy/routingEvidence.js +23 -0
- package/dist/lib/proxy/runtimeConfig.js +3 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +79 -5
- package/dist/lib/server/routes/claudeProxyRoutes.js +653 -72
- package/dist/lib/server/routes/codexProxyRoutes.d.ts +64 -0
- package/dist/lib/server/routes/codexProxyRoutes.js +454 -0
- package/dist/lib/types/cli.d.ts +7 -1
- package/dist/lib/types/codex.d.ts +95 -0
- package/dist/lib/types/codex.js +15 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/proxy.d.ts +83 -0
- package/dist/lib/types/subscription.d.ts +13 -0
- package/dist/providers/openaiChatCompletionsBase.js +26 -14
- package/dist/proxy/accountCooldown.js +35 -2
- package/dist/proxy/accountQuota.d.ts +29 -3
- package/dist/proxy/accountQuota.js +203 -12
- package/dist/proxy/accountUsage.js +15 -2
- package/dist/proxy/codexAccountUsage.d.ts +26 -0
- package/dist/proxy/codexAccountUsage.js +173 -0
- package/dist/proxy/proxyAnalysis.js +12 -1
- package/dist/proxy/proxyConfig.js +24 -0
- package/dist/proxy/routingEvidence.d.ts +12 -1
- package/dist/proxy/routingEvidence.js +23 -0
- package/dist/proxy/runtimeConfig.js +3 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +79 -5
- package/dist/server/routes/claudeProxyRoutes.js +653 -72
- package/dist/server/routes/codexProxyRoutes.d.ts +64 -0
- package/dist/server/routes/codexProxyRoutes.js +453 -0
- package/dist/types/cli.d.ts +7 -1
- package/dist/types/codex.d.ts +95 -0
- package/dist/types/codex.js +14 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxy.d.ts +83 -0
- package/dist/types/subscription.d.ts +13 -0
- package/package.json +3 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex account enumeration + usage/quota normalisation.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `accountUsage.ts` (Anthropic) for the Codex pool. Codex reports two
|
|
5
|
+
* rate-limit windows — `primary` (short) and `secondary` (weekly) — which we
|
|
6
|
+
* map onto the shared AccountQuota session/weekly fields so the same routing,
|
|
7
|
+
* cooldown, and display code works for both providers.
|
|
8
|
+
*/
|
|
9
|
+
import type { AccountQuota, CodexRateLimits, CodexUsageFetchResult, ProxyPassthroughAccount } from "../types/index.js";
|
|
10
|
+
export declare const CODEX_ACCOUNT_PREFIX = "codex:";
|
|
11
|
+
/** Enumerate Codex OAuth accounts from the token store for usage refresh. */
|
|
12
|
+
export declare function listCodexAccountsForUsage(): Promise<ProxyPassthroughAccount[]>;
|
|
13
|
+
/** Normalise a Codex rate-limit block into the shared AccountQuota shape. */
|
|
14
|
+
export declare function codexRateLimitsToQuota(rateLimits: CodexRateLimits | null | undefined, now?: number): AccountQuota;
|
|
15
|
+
/**
|
|
16
|
+
* Parse Codex rate-limit information from response headers. The ChatGPT backend
|
|
17
|
+
* returns a JSON rate-limit blob in `x-codex-ratelimit` / `x-codex-active-limit`
|
|
18
|
+
* on some responses; parse defensively and return null when absent.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseCodexRateLimitHeaders(headers: Headers, now?: number): AccountQuota | null;
|
|
21
|
+
/** Fetch the usage/limits window for one Codex account. */
|
|
22
|
+
export declare function fetchCodexAccountUsage(account: ProxyPassthroughAccount, options?: {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}): Promise<CodexUsageFetchResult>;
|
|
25
|
+
/** Decode plan type from an account's access token (display convenience). */
|
|
26
|
+
export declare function codexAccountPlanType(account: ProxyPassthroughAccount): string | undefined;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex account enumeration + usage/quota normalisation.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `accountUsage.ts` (Anthropic) for the Codex pool. Codex reports two
|
|
5
|
+
* rate-limit windows — `primary` (short) and `secondary` (weekly) — which we
|
|
6
|
+
* map onto the shared AccountQuota session/weekly fields so the same routing,
|
|
7
|
+
* cooldown, and display code works for both providers.
|
|
8
|
+
*/
|
|
9
|
+
import { tokenStore } from "../auth/tokenStore.js";
|
|
10
|
+
import { CODEX_ORIGINATOR, CODEX_USAGE_URL, CODEX_USER_AGENT, decodeCodexAccessToken, resolveCodexAccountId, } from "../auth/codexOAuth.js";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
12
|
+
export const CODEX_ACCOUNT_PREFIX = "codex:";
|
|
13
|
+
/** Enumerate Codex OAuth accounts from the token store for usage refresh. */
|
|
14
|
+
export async function listCodexAccountsForUsage() {
|
|
15
|
+
const keys = await tokenStore.listByPrefix(CODEX_ACCOUNT_PREFIX);
|
|
16
|
+
const accounts = [];
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
if (await tokenStore.isDisabled(key)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const tokens = await tokenStore.loadTokens(key);
|
|
22
|
+
if (!tokens) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const label = key.slice(CODEX_ACCOUNT_PREFIX.length) || key;
|
|
26
|
+
accounts.push({
|
|
27
|
+
key,
|
|
28
|
+
label,
|
|
29
|
+
token: tokens.accessToken,
|
|
30
|
+
refreshToken: tokens.refreshToken,
|
|
31
|
+
expiresAt: tokens.expiresAt,
|
|
32
|
+
type: tokens.tokenType === "Bearer" ? "oauth" : "api_key",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return accounts;
|
|
36
|
+
}
|
|
37
|
+
function toFraction(percent) {
|
|
38
|
+
if (typeof percent !== "number" || !Number.isFinite(percent)) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
// Codex reports 0-100; clamp and normalise to 0-1.
|
|
42
|
+
return Math.min(1, Math.max(0, percent / 100));
|
|
43
|
+
}
|
|
44
|
+
function windowResetEpochSeconds(window, nowSeconds) {
|
|
45
|
+
if (!window) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (typeof window.resets_at === "number" &&
|
|
49
|
+
Number.isFinite(window.resets_at) &&
|
|
50
|
+
window.resets_at > 0) {
|
|
51
|
+
// Tolerate ms epochs (~year 2100 ceiling in seconds).
|
|
52
|
+
return window.resets_at > 4_102_444_800
|
|
53
|
+
? Math.floor(window.resets_at / 1000)
|
|
54
|
+
: window.resets_at;
|
|
55
|
+
}
|
|
56
|
+
const relative = typeof window.resets_in_seconds === "number"
|
|
57
|
+
? window.resets_in_seconds
|
|
58
|
+
: window.reset_after;
|
|
59
|
+
if (typeof relative === "number" &&
|
|
60
|
+
Number.isFinite(relative) &&
|
|
61
|
+
relative > 0) {
|
|
62
|
+
return nowSeconds + Math.floor(relative);
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
function deriveWindowStatus(usedFraction) {
|
|
67
|
+
return usedFraction >= 1 ? "rejected" : "allowed";
|
|
68
|
+
}
|
|
69
|
+
function toQuotaWindow(kind, group, window, nowSeconds) {
|
|
70
|
+
if (!window) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const used = toFraction(window.used_percent);
|
|
74
|
+
return {
|
|
75
|
+
kind,
|
|
76
|
+
group,
|
|
77
|
+
used,
|
|
78
|
+
status: deriveWindowStatus(used),
|
|
79
|
+
resetsAt: windowResetEpochSeconds(window, nowSeconds),
|
|
80
|
+
isActive: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Normalise a Codex rate-limit block into the shared AccountQuota shape. */
|
|
84
|
+
export function codexRateLimitsToQuota(rateLimits, now = Date.now()) {
|
|
85
|
+
const nowSeconds = Math.floor(now / 1000);
|
|
86
|
+
const primary = rateLimits?.primary ?? null;
|
|
87
|
+
const secondary = rateLimits?.secondary ?? null;
|
|
88
|
+
const sessionUsed = toFraction(primary?.used_percent);
|
|
89
|
+
const weeklyUsed = toFraction(secondary?.used_percent);
|
|
90
|
+
const windows = [];
|
|
91
|
+
const primaryWindow = toQuotaWindow("session", "session", primary, nowSeconds);
|
|
92
|
+
const secondaryWindow = toQuotaWindow("weekly_all", "weekly", secondary, nowSeconds);
|
|
93
|
+
if (primaryWindow) {
|
|
94
|
+
windows.push(primaryWindow);
|
|
95
|
+
}
|
|
96
|
+
if (secondaryWindow) {
|
|
97
|
+
windows.push(secondaryWindow);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
sessionUsed,
|
|
101
|
+
sessionStatus: deriveWindowStatus(sessionUsed),
|
|
102
|
+
sessionResetAt: windowResetEpochSeconds(primary, nowSeconds),
|
|
103
|
+
weeklyUsed,
|
|
104
|
+
weeklyStatus: deriveWindowStatus(weeklyUsed),
|
|
105
|
+
weeklyResetAt: windowResetEpochSeconds(secondary, nowSeconds),
|
|
106
|
+
// Codex has no overage/fallback concept; keep neutral defaults.
|
|
107
|
+
fallbackPercentage: 0,
|
|
108
|
+
overageStatus: "rejected",
|
|
109
|
+
lastUpdated: now,
|
|
110
|
+
windows: windows.length > 0 ? windows : undefined,
|
|
111
|
+
windowsUpdatedAt: windows.length > 0 ? now : undefined,
|
|
112
|
+
source: "usage-api",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse Codex rate-limit information from response headers. The ChatGPT backend
|
|
117
|
+
* returns a JSON rate-limit blob in `x-codex-ratelimit` / `x-codex-active-limit`
|
|
118
|
+
* on some responses; parse defensively and return null when absent.
|
|
119
|
+
*/
|
|
120
|
+
export function parseCodexRateLimitHeaders(headers, now = Date.now()) {
|
|
121
|
+
const raw = headers.get("x-codex-ratelimit") ??
|
|
122
|
+
headers.get("x-codex-active-limit") ??
|
|
123
|
+
null;
|
|
124
|
+
if (!raw) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
const rateLimits = "rate_limits" in parsed && parsed.rate_limits
|
|
130
|
+
? parsed.rate_limits
|
|
131
|
+
: parsed;
|
|
132
|
+
return codexRateLimitsToQuota(rateLimits, now);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Fetch the usage/limits window for one Codex account. */
|
|
139
|
+
export async function fetchCodexAccountUsage(account, options = {}) {
|
|
140
|
+
if (account.type !== "oauth") {
|
|
141
|
+
return { ok: false, reason: "not_oauth" };
|
|
142
|
+
}
|
|
143
|
+
const accountId = resolveCodexAccountId(account.token);
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(CODEX_USAGE_URL, {
|
|
146
|
+
method: "GET",
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${account.token}`,
|
|
149
|
+
...(accountId ? { "chatgpt-account-id": accountId } : {}),
|
|
150
|
+
originator: CODEX_ORIGINATOR,
|
|
151
|
+
"User-Agent": CODEX_USER_AGENT,
|
|
152
|
+
Accept: "application/json",
|
|
153
|
+
},
|
|
154
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
|
|
155
|
+
});
|
|
156
|
+
if (response.status === 401 || response.status === 403) {
|
|
157
|
+
return { ok: false, reason: "auth" };
|
|
158
|
+
}
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
return { ok: false, reason: "http" };
|
|
161
|
+
}
|
|
162
|
+
const usage = (await response.json());
|
|
163
|
+
return { ok: true, quota: codexRateLimitsToQuota(usage.rate_limits) };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.debug(`Codex usage fetch failed for ${account.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
|
+
return { ok: false, reason: "network" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Decode plan type from an account's access token (display convenience). */
|
|
171
|
+
export function codexAccountPlanType(account) {
|
|
172
|
+
return decodeCodexAccessToken(account.token).planType;
|
|
173
|
+
}
|
|
@@ -100,7 +100,13 @@ function routingCandidateValue(value) {
|
|
|
100
100
|
"sessionStatus",
|
|
101
101
|
"weeklyStatus",
|
|
102
102
|
];
|
|
103
|
-
const optionalNullableStringFields = [
|
|
103
|
+
const optionalNullableStringFields = [
|
|
104
|
+
"fallbackStatus",
|
|
105
|
+
"upgradePaths",
|
|
106
|
+
"scopedModel",
|
|
107
|
+
"scopedStatus",
|
|
108
|
+
];
|
|
109
|
+
const optionalNullableNumberFields = ["scopedUsed", "scopedResetAt"];
|
|
104
110
|
if (!stringValue(candidate.account) ||
|
|
105
111
|
typeof candidate.accountType !== "string" ||
|
|
106
112
|
!ROUTING_ACCOUNT_TYPES.has(candidate.accountType) ||
|
|
@@ -139,6 +145,7 @@ function routingCandidateValue(value) {
|
|
|
139
145
|
"lastRefreshAttemptAt",
|
|
140
146
|
"lastRefreshSuccessAt",
|
|
141
147
|
"nextRefreshEligibleAt",
|
|
148
|
+
...optionalNullableNumberFields,
|
|
142
149
|
].some((field) => field in candidate &&
|
|
143
150
|
candidate[field] !== undefined &&
|
|
144
151
|
!isNullableFiniteNumber(candidate[field])) ||
|
|
@@ -192,6 +199,10 @@ function routingCandidateValue(value) {
|
|
|
192
199
|
weeklyStatus: candidate.weeklyStatus,
|
|
193
200
|
weeklyUsed: candidate.weeklyUsed,
|
|
194
201
|
weeklyResetAt: candidate.weeklyResetAt,
|
|
202
|
+
scopedModel: candidate.scopedModel,
|
|
203
|
+
scopedStatus: candidate.scopedStatus,
|
|
204
|
+
scopedUsed: candidate.scopedUsed,
|
|
205
|
+
scopedResetAt: candidate.scopedResetAt,
|
|
195
206
|
};
|
|
196
207
|
}
|
|
197
208
|
function routingDecisionValue(value) {
|
|
@@ -238,6 +238,16 @@ export function validateProxyConfig(config) {
|
|
|
238
238
|
normalizedQuotaRouting !== "false") {
|
|
239
239
|
errors.push("routing.quota-routing must be a boolean");
|
|
240
240
|
}
|
|
241
|
+
// Without this a typo (`use-overage: nevr`) loads cleanly and silently
|
|
242
|
+
// falls back to "auto" — the operator asked to block paid extra usage and
|
|
243
|
+
// gets provider-driven overage instead.
|
|
244
|
+
const rawUseOverage = routing["use-overage"] ?? routing.useOverage;
|
|
245
|
+
if (rawUseOverage !== undefined &&
|
|
246
|
+
!["auto", "always", "never"].includes(typeof rawUseOverage === "string"
|
|
247
|
+
? rawUseOverage.trim().toLowerCase()
|
|
248
|
+
: "")) {
|
|
249
|
+
errors.push("routing.use-overage must be auto, always, or never");
|
|
250
|
+
}
|
|
241
251
|
const rawAutoFallback = routing["auto-fallback"] ?? routing.autoFallback;
|
|
242
252
|
const normalizedAutoFallback = typeof rawAutoFallback === "string"
|
|
243
253
|
? rawAutoFallback.trim().toLowerCase()
|
|
@@ -417,6 +427,20 @@ function parseRoutingConfig(raw) {
|
|
|
417
427
|
logger.warn(`[proxy-config] Ignoring routing.quotaRouting: expected boolean, got ${typeof rawQuotaRouting}`);
|
|
418
428
|
}
|
|
419
429
|
}
|
|
430
|
+
const rawUseOverage = raw["use-overage"] ?? raw.useOverage;
|
|
431
|
+
if (rawUseOverage !== undefined) {
|
|
432
|
+
const normalized = typeof rawUseOverage === "string"
|
|
433
|
+
? rawUseOverage.trim().toLowerCase()
|
|
434
|
+
: "";
|
|
435
|
+
if (normalized === "auto" ||
|
|
436
|
+
normalized === "always" ||
|
|
437
|
+
normalized === "never") {
|
|
438
|
+
result.useOverage = normalized;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
logger.warn(`[proxy-config] Ignoring routing.useOverage: expected auto|always|never, got ${String(rawUseOverage)}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
420
444
|
const rawAutoFallback = raw["auto-fallback"] ?? raw.autoFallback;
|
|
421
445
|
if (rawAutoFallback !== undefined) {
|
|
422
446
|
if (typeof rawAutoFallback === "boolean") {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/** Schema-v1 routing evidence values shared by emitters and offline readers. */
|
|
2
2
|
export declare const PROXY_ACCOUNT_ROUTING_STRATEGIES: readonly ["round-robin", "fill-first"];
|
|
3
3
|
export declare const PROXY_ACCOUNT_ROUTING_MODES: readonly ["quota", "primary", "round_robin", "single_account"];
|
|
4
|
-
export declare const PROXY_ACCOUNT_ROUTING_REASONS: readonly ["single_account", "round_robin", "configured_primary", "insertion_order", "availability", "cooldown_recovery", "quota_evidence", "quota_probe", "session_headroom", "session_reset", "weekly_reset", "weekly_utilization"];
|
|
4
|
+
export declare const PROXY_ACCOUNT_ROUTING_REASONS: readonly ["single_account", "round_robin", "configured_primary", "insertion_order", "availability", "cooldown_recovery", "quota_evidence", "quota_probe", "session_headroom", "scoped_headroom", "session_reset", "weekly_reset", "weekly_utilization", "scoped_utilization"];
|
|
5
5
|
export declare const PROXY_ACCOUNT_TYPES: readonly ["oauth", "api_key"];
|
|
6
6
|
export declare const ACCOUNT_COOLING_REASONS: readonly ["weekly", "session", "unified", "transient", "auth"];
|
|
7
|
+
/**
|
|
8
|
+
* Longest a cooldown may last for each reason, measured from when it was set.
|
|
9
|
+
*
|
|
10
|
+
* A reason names a specific provider window, so it also bounds the wait: a
|
|
11
|
+
* "session" cooldown describes a 5-hour window and can never legitimately run
|
|
12
|
+
* for days. Without a per-reason ceiling a single bogus reset timestamp parks an
|
|
13
|
+
* account for as long as the global 8-day clamp allows — observed in the wild as
|
|
14
|
+
* a 206-hour "session" cooldown. Values carry slack so a genuine window that
|
|
15
|
+
* resets slightly late is not cut short.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_COOLDOWN_MS_BY_REASON: Record<string, number>;
|
|
@@ -21,9 +21,11 @@ export const PROXY_ACCOUNT_ROUTING_REASONS = [
|
|
|
21
21
|
// decisions must never select a production request for quota discovery.
|
|
22
22
|
"quota_probe",
|
|
23
23
|
"session_headroom",
|
|
24
|
+
"scoped_headroom",
|
|
24
25
|
"session_reset",
|
|
25
26
|
"weekly_reset",
|
|
26
27
|
"weekly_utilization",
|
|
28
|
+
"scoped_utilization",
|
|
27
29
|
];
|
|
28
30
|
export const PROXY_ACCOUNT_TYPES = ["oauth", "api_key"];
|
|
29
31
|
export const ACCOUNT_COOLING_REASONS = [
|
|
@@ -33,3 +35,24 @@ export const ACCOUNT_COOLING_REASONS = [
|
|
|
33
35
|
"transient",
|
|
34
36
|
"auth",
|
|
35
37
|
];
|
|
38
|
+
/**
|
|
39
|
+
* Longest a cooldown may last for each reason, measured from when it was set.
|
|
40
|
+
*
|
|
41
|
+
* A reason names a specific provider window, so it also bounds the wait: a
|
|
42
|
+
* "session" cooldown describes a 5-hour window and can never legitimately run
|
|
43
|
+
* for days. Without a per-reason ceiling a single bogus reset timestamp parks an
|
|
44
|
+
* account for as long as the global 8-day clamp allows — observed in the wild as
|
|
45
|
+
* a 206-hour "session" cooldown. Values carry slack so a genuine window that
|
|
46
|
+
* resets slightly late is not cut short.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_COOLDOWN_MS_BY_REASON = {
|
|
49
|
+
session: 5 * 60 * 60 * 1000 + 15 * 60 * 1000,
|
|
50
|
+
weekly: 7 * 24 * 60 * 60 * 1000 + 12 * 60 * 60 * 1000,
|
|
51
|
+
// No named window bounds this one: it fires when the 5h/7d statuses still read
|
|
52
|
+
// "allowed" and the provider's own Retry-After is the only signal. Generous on
|
|
53
|
+
// purpose — truncating a provider-directed wait just re-hammers the account on
|
|
54
|
+
// a shorter cycle — while still refusing an absurd multi-day park.
|
|
55
|
+
unified: 12 * 60 * 60 * 1000,
|
|
56
|
+
transient: 15 * 60 * 1000,
|
|
57
|
+
auth: 5 * 60 * 1000,
|
|
58
|
+
};
|
|
@@ -228,6 +228,7 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
|
|
|
228
228
|
: "[missing]")
|
|
229
229
|
.digest("hex")
|
|
230
230
|
.slice(0, 16);
|
|
231
|
+
const useOverage = routing?.useOverage ?? "auto";
|
|
231
232
|
const fingerprintSource = JSON.stringify({
|
|
232
233
|
strategy,
|
|
233
234
|
passthrough: options.passthrough,
|
|
@@ -237,6 +238,7 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
|
|
|
237
238
|
quotaRoutingEnabled,
|
|
238
239
|
sessionSoftLimit,
|
|
239
240
|
sessionResetToleranceMs,
|
|
241
|
+
useOverage,
|
|
240
242
|
});
|
|
241
243
|
const configHash = createHash("sha256")
|
|
242
244
|
.update(fingerprintSource)
|
|
@@ -256,6 +258,7 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
|
|
|
256
258
|
quotaRoutingEnabled,
|
|
257
259
|
sessionSoftLimit,
|
|
258
260
|
sessionResetToleranceMs,
|
|
261
|
+
useOverage,
|
|
259
262
|
}),
|
|
260
263
|
configFilePresent,
|
|
261
264
|
envFilePresent,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
13
13
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
14
14
|
import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
|
|
15
|
-
import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
15
|
+
import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
16
16
|
declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
|
|
17
17
|
declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
|
|
18
18
|
declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
|
|
@@ -54,7 +54,7 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
|
|
|
54
54
|
* burst / acceleration limit) is transient: honor retry-after as a floor,
|
|
55
55
|
* allow a couple of jittered same-account retries, then a short cooldown.
|
|
56
56
|
*/
|
|
57
|
-
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined): AccountCooldownPlan;
|
|
57
|
+
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined, policy?: ProxyOveragePolicy): AccountCooldownPlan;
|
|
58
58
|
/**
|
|
59
59
|
* Proactively cool an account when a SUCCESS response reveals a window has just
|
|
60
60
|
* flipped to "rejected" (the boundary request that spends the last of the quota
|
|
@@ -62,7 +62,7 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
|
|
|
62
62
|
* its reset so the next request skips it instead of discovering the limit via a
|
|
63
63
|
* 429, except when the provider explicitly enables paid overage.
|
|
64
64
|
*/
|
|
65
|
-
declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number): ProxyQuotaCooldownUpdate;
|
|
65
|
+
declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number, policy?: ProxyOveragePolicy): ProxyQuotaCooldownUpdate;
|
|
66
66
|
/**
|
|
67
67
|
* Seed each account's runtime quota from the persisted snapshots in
|
|
68
68
|
* ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
|
|
@@ -88,6 +88,47 @@ declare function refreshAccountLimits(options?: {
|
|
|
88
88
|
accountFilter?: string;
|
|
89
89
|
snapshotOnly?: boolean;
|
|
90
90
|
}): Promise<ProxyLimitsRefreshResponse>;
|
|
91
|
+
/**
|
|
92
|
+
* Publish the operator's extra-usage policy for paths that cannot receive it
|
|
93
|
+
* explicitly. Callers that make a routing or cooldown decision take it as a
|
|
94
|
+
* parameter instead — see {@link isOverageUsable} — so a concurrent request or
|
|
95
|
+
* a hot config reload cannot change the answer mid-flight.
|
|
96
|
+
*
|
|
97
|
+
* `undefined` means "no runtime config on this path", which is not a request to
|
|
98
|
+
* clear an operator's setting, so the current value is kept.
|
|
99
|
+
*/
|
|
100
|
+
declare function setOveragePolicy(policy: ProxyOveragePolicy | undefined): void;
|
|
101
|
+
/**
|
|
102
|
+
* Find the model-scoped quota window that applies to the requested model.
|
|
103
|
+
*
|
|
104
|
+
* Two sources describe the same cap differently: response headers carry the wire
|
|
105
|
+
* id of the model just served, while the usage API reports a DISPLAY name
|
|
106
|
+
* ("Fable", "Claude Opus 4.6"). So an exact wire-id match is tried first and
|
|
107
|
+
* display-name containment over alphanumeric-normalized forms is the fallback.
|
|
108
|
+
* Among equally specific matches the freshest observation wins, which favours
|
|
109
|
+
* the continuously-updated header window over a manually-refreshed one.
|
|
110
|
+
*
|
|
111
|
+
* Returns null whenever the account reports no applicable, still-fresh scoped
|
|
112
|
+
* cap — the common case.
|
|
113
|
+
*/
|
|
114
|
+
declare function matchScopedQuotaWindow(quota: AccountQuota | undefined, requestedModel: string | undefined, now?: number): AccountQuotaWindow | null;
|
|
115
|
+
/**
|
|
116
|
+
* Split a candidate set by whether each account can still serve the requested
|
|
117
|
+
* model under its model-scoped cap.
|
|
118
|
+
*
|
|
119
|
+
* An account is scoped out only on evidence strong enough to act on: a fresh
|
|
120
|
+
* window, a reset that is actually ticking, a rejected/spent reading, and no
|
|
121
|
+
* paid extra usage to absorb the overflow. Anything weaker leaves the account
|
|
122
|
+
* eligible — a stale or mis-parsed window must never be able to empty the pool.
|
|
123
|
+
*
|
|
124
|
+
* `exhaustion` is populated only when every candidate is scoped out AND the
|
|
125
|
+
* evidence is trustworthy, which is what lets the caller report "switch model"
|
|
126
|
+
* instead of a generic rate limit.
|
|
127
|
+
*/
|
|
128
|
+
declare function evaluateScopedExhaustion(accounts: ProxyPassthroughAccount[], requestedModel: string | undefined, now?: number, policy?: ProxyOveragePolicy): {
|
|
129
|
+
eligible: ProxyPassthroughAccount[];
|
|
130
|
+
exhaustion: AnthropicScopedExhaustion | null;
|
|
131
|
+
};
|
|
91
132
|
/**
|
|
92
133
|
* Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
|
|
93
134
|
* spend the overall weekly allowance that expires SOONEST first, so quota
|
|
@@ -112,7 +153,7 @@ declare function refreshAccountLimits(options?: {
|
|
|
112
153
|
* Cooling/rejected accounts sort last, soonest-back-to-service first, as
|
|
113
154
|
* last resort.
|
|
114
155
|
*/
|
|
115
|
-
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
|
|
156
|
+
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number, requestedModel?: string): ProxyPassthroughAccount[];
|
|
116
157
|
declare function scheduleAdaptiveQuotaRefreshes(accounts: ProxyPassthroughAccount[], orderedAccounts: ProxyPassthroughAccount[], sessionSoftLimit: number, routingMetrics?: ReadonlyMap<string, ProxyAccountSortMetrics>): void;
|
|
117
158
|
declare function scheduleHandoffQuotaRefresh(current: ProxyPassthroughAccount, candidate: ProxyPassthroughAccount | undefined, handoffEpoch: number, sessionSoftLimit: number): void;
|
|
118
159
|
declare function buildRoutingDecision(args: {
|
|
@@ -158,6 +199,8 @@ declare function buildClaudeAnthropicFailureResponse(args: {
|
|
|
158
199
|
body: string;
|
|
159
200
|
contentType?: string;
|
|
160
201
|
} | null;
|
|
202
|
+
entitlementFailure: AnthropicEntitlementFailure | null;
|
|
203
|
+
scopedExhaustion: AnthropicScopedExhaustion | null;
|
|
161
204
|
sawNetworkError: boolean;
|
|
162
205
|
sawTransientFailure: boolean;
|
|
163
206
|
sawRateLimit: boolean;
|
|
@@ -231,6 +274,7 @@ declare function handleAnthropicAuthRetry(args: {
|
|
|
231
274
|
}) => void;
|
|
232
275
|
lastError: unknown;
|
|
233
276
|
authFailureMessage: string | null;
|
|
277
|
+
entitlementFailure: AnthropicEntitlementFailure | null;
|
|
234
278
|
sawRateLimit: boolean;
|
|
235
279
|
sawTransientFailure: boolean;
|
|
236
280
|
sawNetworkError: boolean;
|
|
@@ -270,6 +314,7 @@ declare function handleAnthropicNonOkResponse(args: {
|
|
|
270
314
|
body: string;
|
|
271
315
|
contentType?: string;
|
|
272
316
|
} | null;
|
|
317
|
+
entitlementFailure: AnthropicEntitlementFailure | null;
|
|
273
318
|
}): Promise<AnthropicNonOkResult>;
|
|
274
319
|
/**
|
|
275
320
|
* Detect Anthropic's anti-abuse / request-construction 429.
|
|
@@ -286,6 +331,7 @@ declare function fetchAnthropicAccountResponse(args: {
|
|
|
286
331
|
url: string;
|
|
287
332
|
headers: Record<string, string>;
|
|
288
333
|
finalBodyStr: string;
|
|
334
|
+
requestedModel?: string;
|
|
289
335
|
account: ProxyPassthroughAccount;
|
|
290
336
|
accountState: RuntimeAccountState;
|
|
291
337
|
enabledAccounts: ProxyPassthroughAccount[];
|
|
@@ -341,6 +387,29 @@ export declare function isInvalidRequestError(status: number, errBody: string):
|
|
|
341
387
|
* rather than a terminal client-facing 400.
|
|
342
388
|
*/
|
|
343
389
|
export declare function isSubscriptionBetaRejection(status: number, errBody: string): boolean;
|
|
390
|
+
/**
|
|
391
|
+
* An entitlement rejection: Anthropic refuses THIS credential on organization
|
|
392
|
+
* or plan policy, e.g. `403 permission_error` /
|
|
393
|
+
* "OAuth authentication is currently not allowed for this organization."
|
|
394
|
+
*
|
|
395
|
+
* The taxonomy makes this safe to rotate on: `permission_error` is reserved for
|
|
396
|
+
* credential/organization permission, distinct from `invalid_request_error`
|
|
397
|
+
* (request shape) and `not_found_error`, both already terminal above. No
|
|
398
|
+
* `permission_error` is caused by the request body, so the identical request
|
|
399
|
+
* can succeed on a different account.
|
|
400
|
+
*
|
|
401
|
+
* Deliberately broad — rotation is cheap and reversible. Persisting the block
|
|
402
|
+
* is gated on the narrower {@link isDurableEntitlementBlock}.
|
|
403
|
+
*/
|
|
404
|
+
export declare function isAccountEntitlementError(status: number, errBody: string): boolean;
|
|
405
|
+
/**
|
|
406
|
+
* Whether an entitlement rejection is specific enough to disable the account
|
|
407
|
+
* until someone re-enables it. Narrower than {@link isAccountEntitlementError}
|
|
408
|
+
* because disabling is sticky and user-visible: an unrecognised
|
|
409
|
+
* `permission_error` should rotate through the pool and surface a 403, never
|
|
410
|
+
* durably remove a credential on a guess.
|
|
411
|
+
*/
|
|
412
|
+
export declare function isDurableEntitlementBlock(status: number, errBody: string): boolean;
|
|
344
413
|
/**
|
|
345
414
|
* Backward-compatible alias — delegates to the shared translation engine.
|
|
346
415
|
*/
|
|
@@ -374,7 +443,7 @@ export declare const __testHooks: {
|
|
|
374
443
|
scheduleAdaptiveQuotaRefreshes: typeof scheduleAdaptiveQuotaRefreshes;
|
|
375
444
|
scheduleHandoffQuotaRefresh: typeof scheduleHandoffQuotaRefresh;
|
|
376
445
|
getQuotaRefreshState: (key: string) => import("../../index.js").ProxyQuotaRefreshRuntimeState;
|
|
377
|
-
buildQuotaRoutingDecision: (accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number) => ProxyAccountRoutingDecision | undefined;
|
|
446
|
+
buildQuotaRoutingDecision: (accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number, requestedModel?: string) => ProxyAccountRoutingDecision | undefined;
|
|
378
447
|
buildRoutingDecision: typeof buildRoutingDecision;
|
|
379
448
|
resetEpochToMs: typeof resetEpochToMs;
|
|
380
449
|
seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
|
|
@@ -413,5 +482,10 @@ export declare const __testHooks: {
|
|
|
413
482
|
shouldAttemptClaudeFallback: typeof shouldAttemptClaudeFallback;
|
|
414
483
|
executeClaudeFallbackWithRetry: typeof executeClaudeFallbackWithRetry;
|
|
415
484
|
buildClaudeAnthropicFailureResponse: typeof buildClaudeAnthropicFailureResponse;
|
|
485
|
+
isAccountEntitlementError: typeof isAccountEntitlementError;
|
|
486
|
+
isDurableEntitlementBlock: typeof isDurableEntitlementBlock;
|
|
487
|
+
evaluateScopedExhaustion: typeof evaluateScopedExhaustion;
|
|
488
|
+
matchScopedQuotaWindow: typeof matchScopedQuotaWindow;
|
|
489
|
+
setOveragePolicy: typeof setOveragePolicy;
|
|
416
490
|
};
|
|
417
491
|
export {};
|