@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/auth/codexOAuth.d.ts +67 -0
  3. package/dist/auth/codexOAuth.js +202 -0
  4. package/dist/auth/index.d.ts +1 -0
  5. package/dist/auth/index.js +4 -0
  6. package/dist/browser/neurolink.min.js +419 -419
  7. package/dist/cli/commands/auth.d.ts +27 -8
  8. package/dist/cli/commands/auth.js +425 -6
  9. package/dist/cli/commands/proxy.js +230 -5
  10. package/dist/cli/factories/authCommandFactory.d.ts +8 -0
  11. package/dist/cli/factories/authCommandFactory.js +74 -1
  12. package/dist/lib/auth/codexOAuth.d.ts +67 -0
  13. package/dist/lib/auth/codexOAuth.js +203 -0
  14. package/dist/lib/auth/index.d.ts +1 -0
  15. package/dist/lib/auth/index.js +4 -0
  16. package/dist/lib/providers/openaiChatCompletionsBase.js +26 -14
  17. package/dist/lib/proxy/accountCooldown.js +35 -2
  18. package/dist/lib/proxy/accountQuota.d.ts +29 -3
  19. package/dist/lib/proxy/accountQuota.js +203 -12
  20. package/dist/lib/proxy/accountUsage.js +15 -2
  21. package/dist/lib/proxy/codexAccountUsage.d.ts +26 -0
  22. package/dist/lib/proxy/codexAccountUsage.js +174 -0
  23. package/dist/lib/proxy/proxyAnalysis.js +12 -1
  24. package/dist/lib/proxy/proxyConfig.js +24 -0
  25. package/dist/lib/proxy/routingEvidence.d.ts +12 -1
  26. package/dist/lib/proxy/routingEvidence.js +23 -0
  27. package/dist/lib/proxy/runtimeConfig.js +3 -0
  28. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +79 -5
  29. package/dist/lib/server/routes/claudeProxyRoutes.js +653 -72
  30. package/dist/lib/server/routes/codexProxyRoutes.d.ts +64 -0
  31. package/dist/lib/server/routes/codexProxyRoutes.js +454 -0
  32. package/dist/lib/types/cli.d.ts +7 -1
  33. package/dist/lib/types/codex.d.ts +95 -0
  34. package/dist/lib/types/codex.js +15 -0
  35. package/dist/lib/types/index.d.ts +1 -0
  36. package/dist/lib/types/index.js +1 -0
  37. package/dist/lib/types/proxy.d.ts +83 -0
  38. package/dist/lib/types/subscription.d.ts +13 -0
  39. package/dist/providers/openaiChatCompletionsBase.js +26 -14
  40. package/dist/proxy/accountCooldown.js +35 -2
  41. package/dist/proxy/accountQuota.d.ts +29 -3
  42. package/dist/proxy/accountQuota.js +203 -12
  43. package/dist/proxy/accountUsage.js +15 -2
  44. package/dist/proxy/codexAccountUsage.d.ts +26 -0
  45. package/dist/proxy/codexAccountUsage.js +173 -0
  46. package/dist/proxy/proxyAnalysis.js +12 -1
  47. package/dist/proxy/proxyConfig.js +24 -0
  48. package/dist/proxy/routingEvidence.d.ts +12 -1
  49. package/dist/proxy/routingEvidence.js +23 -0
  50. package/dist/proxy/runtimeConfig.js +3 -0
  51. package/dist/server/routes/claudeProxyRoutes.d.ts +79 -5
  52. package/dist/server/routes/claudeProxyRoutes.js +653 -72
  53. package/dist/server/routes/codexProxyRoutes.d.ts +64 -0
  54. package/dist/server/routes/codexProxyRoutes.js +453 -0
  55. package/dist/types/cli.d.ts +7 -1
  56. package/dist/types/codex.d.ts +95 -0
  57. package/dist/types/codex.js +14 -0
  58. package/dist/types/index.d.ts +1 -0
  59. package/dist/types/index.js +1 -0
  60. package/dist/types/proxy.d.ts +83 -0
  61. package/dist/types/subscription.d.ts +13 -0
  62. package/package.json +3 -1
@@ -0,0 +1,174 @@
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
+ }
174
+ //# sourceMappingURL=codexAccountUsage.js.map
@@ -100,7 +100,13 @@ function routingCandidateValue(value) {
100
100
  "sessionStatus",
101
101
  "weeklyStatus",
102
102
  ];
103
- const optionalNullableStringFields = ["fallbackStatus", "upgradePaths"];
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,4 +35,25 @@ 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
+ };
36
59
  //# sourceMappingURL=routingEvidence.js.map
@@ -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("../../types/proxy.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 {};