@juspay/neurolink 12.23.0 → 12.23.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.
@@ -0,0 +1,22 @@
1
+ import type { ProxyAccountRoutingReason, ProxyAccountSortMetrics, ProxyPassthroughAccount } from "../types/index.js";
2
+ /**
3
+ * The pre-existing "spend the soonest-expiring weekly allowance first" order.
4
+ * Moved verbatim from claudeProxyRoutes.ts — availability → quota-evidence
5
+ * quality → session saturation → model-scoped saturation → soonest weekly
6
+ * reset → soonest session-reset bucket → model-scoped utilization → highest
7
+ * weekly utilization → configured primary → insertion order.
8
+ */
9
+ export declare function compareExpiryFirst(a: ProxyPassthroughAccount, b: ProxyPassthroughAccount, metricsByKey: ReadonlyMap<string, ProxyAccountSortMetrics>, primaryKey: string | undefined): [number, ProxyAccountRoutingReason];
10
+ /**
11
+ * Orders accounts by the configured ranking and reports the reason that
12
+ * placed rank 0. PR1 ships only `expiry-first`; PR2 (Task 11) adds
13
+ * `headroom-first` plus the affinity/prefer-primary precedence layer.
14
+ */
15
+ export declare function rankAccounts(args: {
16
+ accounts: ProxyPassthroughAccount[];
17
+ metricsByKey: ReadonlyMap<string, ProxyAccountSortMetrics>;
18
+ primaryKey: string | undefined;
19
+ }): {
20
+ orderedAccounts: ProxyPassthroughAccount[];
21
+ reason: ProxyAccountRoutingReason;
22
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The pre-existing "spend the soonest-expiring weekly allowance first" order.
3
+ * Moved verbatim from claudeProxyRoutes.ts — availability → quota-evidence
4
+ * quality → session saturation → model-scoped saturation → soonest weekly
5
+ * reset → soonest session-reset bucket → model-scoped utilization → highest
6
+ * weekly utilization → configured primary → insertion order.
7
+ */
8
+ export function compareExpiryFirst(a, b, metricsByKey, primaryKey) {
9
+ const ma = metricsByKey.get(a.key);
10
+ const mb = metricsByKey.get(b.key);
11
+ if (!ma || !mb) {
12
+ return [0, "insertion_order"];
13
+ }
14
+ if (ma.usable !== mb.usable) {
15
+ return [ma.usable ? -1 : 1, "availability"];
16
+ }
17
+ if (!ma.usable && !mb.usable) {
18
+ const au = ma.coolingUntil || Number.POSITIVE_INFINITY;
19
+ const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
20
+ return [
21
+ au === bu ? 0 : au - bu,
22
+ au === bu ? "insertion_order" : "cooldown_recovery",
23
+ ];
24
+ }
25
+ if (ma.quotaEvidenceRank !== mb.quotaEvidenceRank) {
26
+ return [ma.quotaEvidenceRank - mb.quotaEvidenceRank, "quota_evidence"];
27
+ }
28
+ if (ma.saturated !== mb.saturated) {
29
+ return [ma.saturated ? 1 : -1, "session_headroom"];
30
+ }
31
+ // Per-model headroom, after overall session capacity: an account whose cap
32
+ // for THIS model is nearly spent is demoted even when its 5h/7d are healthy.
33
+ // No-op when neither account reports a scoped window for the model.
34
+ if (ma.scopedSaturated !== mb.scopedSaturated) {
35
+ return [ma.scopedSaturated ? 1 : -1, "scoped_headroom"];
36
+ }
37
+ if (ma.saturated && mb.saturated) {
38
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
39
+ return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
40
+ }
41
+ if (ma.weeklyReset !== mb.weeklyReset) {
42
+ return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
43
+ }
44
+ }
45
+ else {
46
+ if (ma.weeklyReset !== mb.weeklyReset) {
47
+ return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
48
+ }
49
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
50
+ return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
51
+ }
52
+ }
53
+ // Fill-first within the per-model allowance: finish off the account closest
54
+ // to spending its cap for this model before opening a fresher one. Ranked
55
+ // above overall weekly utilization because it is the tighter constraint.
56
+ // Both sides must actually report a scoped window. Comparing a real
57
+ // utilization against the "absent" sentinel would rank the account that has a
58
+ // window above one that does not — and since only the account serving a model
59
+ // gets that model's window, it would funnel all of a model's traffic onto
60
+ // whichever account happened to serve it first.
61
+ if (ma.scopedUsed !== null &&
62
+ mb.scopedUsed !== null &&
63
+ ma.scopedUsedForSort !== mb.scopedUsedForSort) {
64
+ return [mb.scopedUsedForSort - ma.scopedUsedForSort, "scoped_utilization"];
65
+ }
66
+ if (ma.weeklyUsedForSort !== mb.weeklyUsedForSort) {
67
+ return [mb.weeklyUsedForSort - ma.weeklyUsedForSort, "weekly_utilization"];
68
+ }
69
+ if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
70
+ return [a.key === primaryKey ? -1 : 1, "configured_primary"];
71
+ }
72
+ return [0, "insertion_order"];
73
+ }
74
+ /**
75
+ * Orders accounts by the configured ranking and reports the reason that
76
+ * placed rank 0. PR1 ships only `expiry-first`; PR2 (Task 11) adds
77
+ * `headroom-first` plus the affinity/prefer-primary precedence layer.
78
+ */
79
+ export function rankAccounts(args) {
80
+ const { accounts, metricsByKey, primaryKey } = args;
81
+ const orderedAccounts = [...accounts].sort((a, b) => compareExpiryFirst(a, b, metricsByKey, primaryKey)[0]);
82
+ const reason = orderedAccounts.length < 2
83
+ ? "single_account"
84
+ : compareExpiryFirst(orderedAccounts[0], orderedAccounts[1], metricsByKey, primaryKey)[1];
85
+ return { orderedAccounts, reason };
86
+ }
@@ -213,6 +213,36 @@ const CODEX_REASONING_EFFORTS = [
213
213
  function isCodexReasoningEffort(value) {
214
214
  return CODEX_REASONING_EFFORTS.some((effort) => effort === value);
215
215
  }
216
+ function kebabToCamelRoutingKey(kebabKey) {
217
+ return kebabKey.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
218
+ }
219
+ /**
220
+ * Read one of the eight routing keys that predate #1787's five newer keys,
221
+ * accepting either spelling and treating a `null` value under either
222
+ * spelling as "unset" so it falls back to the caller's default instead of
223
+ * failing validation.
224
+ *
225
+ * `??` already treats a `null` kebab value the same as an absent one and
226
+ * falls through to the camel spelling — that precedence must not change (a
227
+ * `null` kebab key must not mask a non-null camel value) — so the only gap
228
+ * is a combined value that still resolves to `null` (camel-only `null`, or
229
+ * both spellings `null`): that case is mapped to `undefined` here so every
230
+ * existing `!== undefined` guard and default keeps working unchanged.
231
+ *
232
+ * `wasNull` is true exactly when the resolved value became `undefined`
233
+ * *because* one of the spellings was explicitly `null` — never merely
234
+ * because both were absent — so callers can warn once per null key without
235
+ * warning on ordinary omission.
236
+ */
237
+ function readLegacyRoutingKey(routing, kebabKey) {
238
+ const camelKey = kebabToCamelRoutingKey(kebabKey);
239
+ const rawKebab = routing[kebabKey];
240
+ const rawCamel = routing[camelKey];
241
+ const combined = rawKebab ?? rawCamel;
242
+ const value = combined === null ? undefined : combined;
243
+ const wasNull = value === undefined && (rawKebab === null || rawCamel === null);
244
+ return { value, wasNull };
245
+ }
216
246
  /**
217
247
  * Validate the shape of a parsed proxy config.
218
248
  * Returns an array of human-readable error strings (empty = valid).
@@ -239,7 +269,7 @@ export function validateProxyConfig(config) {
239
269
  }
240
270
  if (hasRouting) {
241
271
  const routing = cfg.routing;
242
- const rawFallback = routing["fallback-chain"] ?? routing.fallbackChain;
272
+ const rawFallback = readLegacyRoutingKey(routing, "fallback-chain").value;
243
273
  if (Array.isArray(rawFallback)) {
244
274
  rawFallback.forEach((entry, index) => {
245
275
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
@@ -261,7 +291,7 @@ export function validateProxyConfig(config) {
261
291
  }
262
292
  });
263
293
  }
264
- const rawAccountAllowlist = routing["account-allowlist"] ?? routing.accountAllowlist;
294
+ const rawAccountAllowlist = readLegacyRoutingKey(routing, "account-allowlist").value;
265
295
  if (rawAccountAllowlist !== undefined) {
266
296
  if (!Array.isArray(rawAccountAllowlist)) {
267
297
  errors.push("routing.account-allowlist must be an array of non-empty strings");
@@ -274,7 +304,7 @@ export function validateProxyConfig(config) {
274
304
  });
275
305
  }
276
306
  }
277
- const rawQuotaRouting = routing["quota-routing"] ?? routing.quotaRouting;
307
+ const rawQuotaRouting = readLegacyRoutingKey(routing, "quota-routing").value;
278
308
  const normalizedQuotaRouting = typeof rawQuotaRouting === "string"
279
309
  ? rawQuotaRouting.trim().toLowerCase()
280
310
  : undefined;
@@ -287,14 +317,14 @@ export function validateProxyConfig(config) {
287
317
  // Without this a typo (`use-overage: nevr`) loads cleanly and silently
288
318
  // falls back to "auto" — the operator asked to block paid extra usage and
289
319
  // gets provider-driven overage instead.
290
- const rawUseOverage = routing["use-overage"] ?? routing.useOverage;
320
+ const rawUseOverage = readLegacyRoutingKey(routing, "use-overage").value;
291
321
  if (rawUseOverage !== undefined &&
292
322
  !["auto", "always", "never"].includes(typeof rawUseOverage === "string"
293
323
  ? rawUseOverage.trim().toLowerCase()
294
324
  : "")) {
295
325
  errors.push("routing.use-overage must be auto, always, or never");
296
326
  }
297
- const rawAutoFallback = routing["auto-fallback"] ?? routing.autoFallback;
327
+ const rawAutoFallback = readLegacyRoutingKey(routing, "auto-fallback").value;
298
328
  const normalizedAutoFallback = typeof rawAutoFallback === "string"
299
329
  ? rawAutoFallback.trim().toLowerCase()
300
330
  : undefined;
@@ -304,7 +334,7 @@ export function validateProxyConfig(config) {
304
334
  normalizedAutoFallback !== "false") {
305
335
  errors.push("routing.auto-fallback must be a boolean");
306
336
  }
307
- const rawMaxInflight = routing["max-inflight-per-account"] ?? routing.maxInflightPerAccount;
337
+ const rawMaxInflight = readLegacyRoutingKey(routing, "max-inflight-per-account").value;
308
338
  if (rawMaxInflight !== undefined &&
309
339
  (typeof rawMaxInflight !== "number" ||
310
340
  !Number.isInteger(rawMaxInflight) ||
@@ -312,7 +342,7 @@ export function validateProxyConfig(config) {
312
342
  rawMaxInflight > MAX_MAX_INFLIGHT_PER_ACCOUNT)) {
313
343
  errors.push("routing.max-inflight-per-account must be an integer between 1 and 20");
314
344
  }
315
- const rawSessionSoftLimit = routing["session-soft-limit"] ?? routing.sessionSoftLimit;
345
+ const rawSessionSoftLimit = readLegacyRoutingKey(routing, "session-soft-limit").value;
316
346
  if (rawSessionSoftLimit !== undefined) {
317
347
  const sessionSoftLimit = Number(rawSessionSoftLimit);
318
348
  if (!Number.isFinite(sessionSoftLimit) ||
@@ -321,7 +351,7 @@ export function validateProxyConfig(config) {
321
351
  errors.push("routing.session-soft-limit must be a number in (0, 1]");
322
352
  }
323
353
  }
324
- const rawSessionResetToleranceMs = routing["session-reset-tolerance-ms"] ?? routing.sessionResetToleranceMs;
354
+ const rawSessionResetToleranceMs = readLegacyRoutingKey(routing, "session-reset-tolerance-ms").value;
325
355
  if (rawSessionResetToleranceMs !== undefined) {
326
356
  const sessionResetToleranceMs = Number(rawSessionResetToleranceMs);
327
357
  if (!Number.isInteger(sessionResetToleranceMs) ||
@@ -439,7 +469,11 @@ function parseRoutingConfig(raw) {
439
469
  .filter((m) => m !== null);
440
470
  }
441
471
  // Fallback chain (accept kebab-case or camelCase)
442
- const rawFallback = (raw["fallback-chain"] ?? raw.fallbackChain);
472
+ const fallbackChainRead = readLegacyRoutingKey(raw, "fallback-chain");
473
+ if (fallbackChainRead.wasNull) {
474
+ logger.warn("[proxy-config] routing.fallback-chain is null; using the default");
475
+ }
476
+ const rawFallback = fallbackChainRead.value;
443
477
  if (Array.isArray(rawFallback)) {
444
478
  result.fallbackChain = rawFallback
445
479
  .filter((e) => e !== null && typeof e === "object")
@@ -469,7 +503,11 @@ function parseRoutingConfig(raw) {
469
503
  if (Array.isArray(rawPassthrough)) {
470
504
  result.passthroughModels = rawPassthrough.map(String);
471
505
  }
472
- const rawQuotaRouting = raw["quota-routing"] ?? raw.quotaRouting;
506
+ const quotaRoutingRead = readLegacyRoutingKey(raw, "quota-routing");
507
+ if (quotaRoutingRead.wasNull) {
508
+ logger.warn("[proxy-config] routing.quota-routing is null; using the default");
509
+ }
510
+ const rawQuotaRouting = quotaRoutingRead.value;
473
511
  if (rawQuotaRouting !== undefined) {
474
512
  if (typeof rawQuotaRouting === "boolean") {
475
513
  result.quotaRouting = rawQuotaRouting;
@@ -482,7 +520,11 @@ function parseRoutingConfig(raw) {
482
520
  logger.warn(`[proxy-config] Ignoring routing.quotaRouting: expected boolean, got ${typeof rawQuotaRouting}`);
483
521
  }
484
522
  }
485
- const rawUseOverage = raw["use-overage"] ?? raw.useOverage;
523
+ const useOverageRead = readLegacyRoutingKey(raw, "use-overage");
524
+ if (useOverageRead.wasNull) {
525
+ logger.warn("[proxy-config] routing.use-overage is null; using the default");
526
+ }
527
+ const rawUseOverage = useOverageRead.value;
486
528
  if (rawUseOverage !== undefined) {
487
529
  const normalized = typeof rawUseOverage === "string"
488
530
  ? rawUseOverage.trim().toLowerCase()
@@ -496,7 +538,11 @@ function parseRoutingConfig(raw) {
496
538
  logger.warn(`[proxy-config] Ignoring routing.useOverage: expected auto|always|never, got ${String(rawUseOverage)}`);
497
539
  }
498
540
  }
499
- const rawAutoFallback = raw["auto-fallback"] ?? raw.autoFallback;
541
+ const autoFallbackRead = readLegacyRoutingKey(raw, "auto-fallback");
542
+ if (autoFallbackRead.wasNull) {
543
+ logger.warn("[proxy-config] routing.auto-fallback is null; using the default");
544
+ }
545
+ const rawAutoFallback = autoFallbackRead.value;
500
546
  if (rawAutoFallback !== undefined) {
501
547
  if (typeof rawAutoFallback === "boolean") {
502
548
  result.autoFallback = rawAutoFallback;
@@ -509,7 +555,11 @@ function parseRoutingConfig(raw) {
509
555
  logger.warn(`[proxy-config] Ignoring routing.autoFallback: expected boolean, got ${typeof rawAutoFallback}`);
510
556
  }
511
557
  }
512
- const rawMaxInflight = raw["max-inflight-per-account"] ?? raw.maxInflightPerAccount;
558
+ const maxInflightRead = readLegacyRoutingKey(raw, "max-inflight-per-account");
559
+ if (maxInflightRead.wasNull) {
560
+ logger.warn("[proxy-config] routing.max-inflight-per-account is null; using the default");
561
+ }
562
+ const rawMaxInflight = maxInflightRead.value;
513
563
  if (rawMaxInflight !== undefined) {
514
564
  if (typeof rawMaxInflight === "number" &&
515
565
  Number.isInteger(rawMaxInflight) &&
@@ -521,7 +571,11 @@ function parseRoutingConfig(raw) {
521
571
  logger.warn(`[proxy-config] Ignoring routing.maxInflightPerAccount: expected integer between 1 and 20, got ${String(rawMaxInflight)}`);
522
572
  }
523
573
  }
524
- const rawSessionSoftLimit = raw["session-soft-limit"] ?? raw.sessionSoftLimit;
574
+ const sessionSoftLimitRead = readLegacyRoutingKey(raw, "session-soft-limit");
575
+ if (sessionSoftLimitRead.wasNull) {
576
+ logger.warn("[proxy-config] routing.session-soft-limit is null; using the default");
577
+ }
578
+ const rawSessionSoftLimit = sessionSoftLimitRead.value;
525
579
  if (rawSessionSoftLimit !== undefined) {
526
580
  const sessionSoftLimit = Number(rawSessionSoftLimit);
527
581
  if (Number.isFinite(sessionSoftLimit) &&
@@ -533,7 +587,11 @@ function parseRoutingConfig(raw) {
533
587
  logger.warn(`[proxy-config] Ignoring routing.sessionSoftLimit: expected number in (0, 1], got ${String(rawSessionSoftLimit)}`);
534
588
  }
535
589
  }
536
- const rawSessionResetToleranceMs = raw["session-reset-tolerance-ms"] ?? raw.sessionResetToleranceMs;
590
+ const sessionResetToleranceMsRead = readLegacyRoutingKey(raw, "session-reset-tolerance-ms");
591
+ if (sessionResetToleranceMsRead.wasNull) {
592
+ logger.warn("[proxy-config] routing.session-reset-tolerance-ms is null; using the default");
593
+ }
594
+ const rawSessionResetToleranceMs = sessionResetToleranceMsRead.value;
537
595
  if (rawSessionResetToleranceMs !== undefined) {
538
596
  const sessionResetToleranceMs = Number(rawSessionResetToleranceMs);
539
597
  if (Number.isInteger(sessionResetToleranceMs) &&
@@ -558,8 +616,11 @@ function parseRoutingConfig(raw) {
558
616
  `string, got ${typeof rawPrimary}`);
559
617
  }
560
618
  }
561
- const rawAccountAllowlist = (raw["account-allowlist"] ??
562
- raw.accountAllowlist);
619
+ const accountAllowlistRead = readLegacyRoutingKey(raw, "account-allowlist");
620
+ if (accountAllowlistRead.wasNull) {
621
+ logger.warn("[proxy-config] routing.account-allowlist is null; using the default");
622
+ }
623
+ const rawAccountAllowlist = accountAllowlistRead.value;
563
624
  if (Array.isArray(rawAccountAllowlist)) {
564
625
  result.accountAllowlist = [
565
626
  ...new Set(rawAccountAllowlist.map((entry) => String(entry).trim())),
@@ -3,10 +3,12 @@ import { ProxyTracer } from "../../proxy/proxyTracer.js";
3
3
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
4
4
  import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
5
5
  import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamBody, AnthropicUpstreamBodyBuilder, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
6
- declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
7
- declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
8
- declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
9
- declare function acquireFirstAvailableAccountAdmission(accountKeys: string[], capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<{
6
+ declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined, generation?: number): AccountAdmissionLease | undefined;
7
+ /** Current in-flight request count for one account, unlimited or capped. */
8
+ declare function getAccountInflight(accountKey: string): number;
9
+ declare function enqueueAccountAdmission(accountKey: string, capacity: number, generation?: number): QueuedAccountAdmission;
10
+ declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number, generation?: number): Promise<AccountAdmissionLease | undefined>;
11
+ declare function acquireFirstAvailableAccountAdmission(accountKeys: string[], capacity: number, abortSignal?: AbortSignal, timeoutMs?: number, generation?: number): Promise<{
10
12
  accountKey: string;
11
13
  lease: AccountAdmissionLease;
12
14
  } | undefined>;
@@ -119,6 +121,10 @@ declare function evaluateScopedExhaustion(accounts: ProxyPassthroughAccount[], r
119
121
  eligible: ProxyPassthroughAccount[];
120
122
  exhaustion: AnthropicScopedExhaustion | null;
121
123
  };
124
+ declare function orderAccountsByQuotaWithMetrics(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit: number, sessionResetToleranceMs: number, requestedModel?: string): {
125
+ orderedAccounts: ProxyPassthroughAccount[];
126
+ metricsByKey: Map<string, ProxyAccountSortMetrics>;
127
+ };
122
128
  /**
123
129
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
124
130
  * spend the overall weekly allowance that expires SOONEST first, so quota
@@ -496,6 +502,7 @@ export declare const __testHooks: {
496
502
  getStreamFailureDetails: typeof getStreamFailureDetails;
497
503
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
498
504
  orderAccountsByQuota: typeof orderAccountsByQuota;
505
+ orderAccountsByQuotaWithMetrics: typeof orderAccountsByQuotaWithMetrics;
499
506
  scheduleAdaptiveQuotaRefreshes: typeof scheduleAdaptiveQuotaRefreshes;
500
507
  scheduleHandoffQuotaRefresh: typeof scheduleHandoffQuotaRefresh;
501
508
  getQuotaRefreshState: (key: string) => import("../../index.js").ProxyQuotaRefreshRuntimeState;
@@ -526,6 +533,7 @@ export declare const __testHooks: {
526
533
  acquireFirstAvailableAccountAdmission: typeof acquireFirstAvailableAccountAdmission;
527
534
  tryAcquireAccountAdmission: typeof tryAcquireAccountAdmission;
528
535
  enqueueAccountAdmission: typeof enqueueAccountAdmission;
536
+ getAccountInflight: typeof getAccountInflight;
529
537
  getAccountAdmissionSnapshot: (accountKey: string) => {
530
538
  active: number;
531
539
  waiting: number;
@@ -24,6 +24,7 @@ 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
28
  import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
28
29
  import { buildClaudeError, ClaudeStreamSerializer, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
29
30
  import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
@@ -261,9 +262,6 @@ const transientCooldownAdmissionSchedules = new Map();
261
262
  * make room for another stream on the same account.
262
263
  */
263
264
  const accountAdmissionStates = new Map();
264
- const unlimitedAccountAdmissionLease = {
265
- release: () => undefined,
266
- };
267
265
  function getAccountAdmissionState(accountKey) {
268
266
  let state = accountAdmissionStates.get(accountKey);
269
267
  if (!state) {
@@ -282,6 +280,32 @@ function drainAccountAdmissionWaiters(accountKey, state) {
282
280
  waiter.resolve(createAccountAdmissionLease(accountKey, state));
283
281
  }
284
282
  }
283
+ /**
284
+ * Admits, in FIFO order and ignoring the capacity each one queued with, every
285
+ * waiter that queued under an older runtime-config generation than the
286
+ * uncapped request now acquiring. Each request reads the cap from the snapshot
287
+ * it arrived with, so only a NEWER uncapped snapshot shows that a reload
288
+ * removed the waiter's cap — and uncapped arrivals count toward `active`, so
289
+ * that waiter would otherwise never drain. An uncapped request from an older
290
+ * snapshot (one that predates a reload adding the cap) or from the same one
291
+ * leaves waiters queued: their cap is still current. With no generation there
292
+ * is no runtime config store, the cap cannot change, and nothing is admitted.
293
+ */
294
+ function admitWaitersOfRemovedCap(accountKey, state, generation) {
295
+ if (generation === undefined) {
296
+ return;
297
+ }
298
+ const queuedUnderOlderSnapshot = (waiter) => waiter.generation !== undefined && waiter.generation < generation;
299
+ const admitted = state.waiters.filter(queuedUnderOlderSnapshot);
300
+ if (admitted.length === 0) {
301
+ return;
302
+ }
303
+ state.waiters = state.waiters.filter((waiter) => !queuedUnderOlderSnapshot(waiter));
304
+ for (const waiter of admitted) {
305
+ state.active += 1;
306
+ waiter.resolve(createAccountAdmissionLease(accountKey, state));
307
+ }
308
+ }
285
309
  function createAccountAdmissionLease(accountKey, state) {
286
310
  let released = false;
287
311
  return {
@@ -301,18 +325,24 @@ function discardAccountAdmissionState(accountKey, state) {
301
325
  accountAdmissionStates.delete(accountKey);
302
326
  }
303
327
  }
304
- function tryAcquireAccountAdmission(accountKey, capacity) {
328
+ function tryAcquireAccountAdmission(accountKey, capacity, generation) {
305
329
  const normalizedCapacity = normalizeMaxInflightPerAccount(capacity);
330
+ const state = getAccountAdmissionState(accountKey);
306
331
  if (normalizedCapacity === undefined) {
307
- return unlimitedAccountAdmissionLease;
332
+ admitWaitersOfRemovedCap(accountKey, state, generation);
333
+ state.active += 1;
334
+ return createAccountAdmissionLease(accountKey, state);
308
335
  }
309
- const state = getAccountAdmissionState(accountKey);
310
336
  if (state.waiters.length > 0 || state.active >= normalizedCapacity) {
311
337
  return undefined;
312
338
  }
313
339
  state.active += 1;
314
340
  return createAccountAdmissionLease(accountKey, state);
315
341
  }
342
+ /** Current in-flight request count for one account, unlimited or capped. */
343
+ function getAccountInflight(accountKey) {
344
+ return accountAdmissionStates.get(accountKey)?.active ?? 0;
345
+ }
316
346
  function isAccountAdmissionAvailable(accountKey, capacity) {
317
347
  const normalizedCapacity = normalizeMaxInflightPerAccount(capacity);
318
348
  if (normalizedCapacity === undefined) {
@@ -321,7 +351,7 @@ function isAccountAdmissionAvailable(accountKey, capacity) {
321
351
  const state = accountAdmissionStates.get(accountKey);
322
352
  return (!state || (state.waiters.length === 0 && state.active < normalizedCapacity));
323
353
  }
324
- function enqueueAccountAdmission(accountKey, capacity) {
354
+ function enqueueAccountAdmission(accountKey, capacity, generation) {
325
355
  // Validate BEFORE getAccountAdmissionState(), which inserts into the map as a
326
356
  // side effect. Throwing after it would strand an empty entry for an account
327
357
  // that never got admitted — and the throw path never calls
@@ -339,6 +369,7 @@ function enqueueAccountAdmission(accountKey, capacity) {
339
369
  });
340
370
  const waiter = {
341
371
  capacity: normalizedCapacity,
372
+ generation,
342
373
  resolve: (lease) => {
343
374
  queued = false;
344
375
  grantedLease = lease;
@@ -370,8 +401,8 @@ function enqueueAccountAdmission(accountKey, capacity) {
370
401
  },
371
402
  };
372
403
  }
373
- async function acquireAccountAdmission(accountKey, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS) {
374
- const queued = enqueueAccountAdmission(accountKey, capacity);
404
+ async function acquireAccountAdmission(accountKey, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS, generation) {
405
+ const queued = enqueueAccountAdmission(accountKey, capacity, generation);
375
406
  try {
376
407
  return await withTimeout(raceWithAbort(queued.promise, abortSignal), timeoutMs, `Account admission for ${accountKey} timed out after ${timeoutMs}ms`);
377
408
  }
@@ -383,8 +414,8 @@ async function acquireAccountAdmission(accountKey, capacity, abortSignal, timeou
383
414
  return undefined;
384
415
  }
385
416
  }
386
- async function acquireFirstAvailableAccountAdmission(accountKeys, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS) {
387
- const queuedAdmissions = [...new Set(accountKeys)].map((accountKey) => enqueueAccountAdmission(accountKey, capacity));
417
+ async function acquireFirstAvailableAccountAdmission(accountKeys, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS, generation) {
418
+ const queuedAdmissions = [...new Set(accountKeys)].map((accountKey) => enqueueAccountAdmission(accountKey, capacity, generation));
388
419
  let winnerKey;
389
420
  try {
390
421
  return await withTimeout(raceWithAbort(Promise.race(queuedAdmissions.map((queued) => queued.promise.then((lease) => {
@@ -1475,81 +1506,17 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
1475
1506
  weeklyUsedForSort: weeklyUsed ?? -1,
1476
1507
  };
1477
1508
  }
1478
- function compareAccountRoutingFactors(a, b, metricsByKey, primaryKey) {
1479
- const ma = metricsByKey.get(a.key);
1480
- const mb = metricsByKey.get(b.key);
1481
- if (!ma || !mb) {
1482
- return [0, "insertion_order"];
1483
- }
1484
- if (ma.usable !== mb.usable) {
1485
- return [ma.usable ? -1 : 1, "availability"];
1486
- }
1487
- if (!ma.usable && !mb.usable) {
1488
- const au = ma.coolingUntil || Number.POSITIVE_INFINITY;
1489
- const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
1490
- return [
1491
- au === bu ? 0 : au - bu,
1492
- au === bu ? "insertion_order" : "cooldown_recovery",
1493
- ];
1494
- }
1495
- if (ma.quotaEvidenceRank !== mb.quotaEvidenceRank) {
1496
- return [ma.quotaEvidenceRank - mb.quotaEvidenceRank, "quota_evidence"];
1497
- }
1498
- if (ma.saturated !== mb.saturated) {
1499
- return [ma.saturated ? 1 : -1, "session_headroom"];
1500
- }
1501
- // Per-model headroom, after overall session capacity: an account whose cap
1502
- // for THIS model is nearly spent is demoted even when its 5h/7d are healthy.
1503
- // No-op when neither account reports a scoped window for the model.
1504
- if (ma.scopedSaturated !== mb.scopedSaturated) {
1505
- return [ma.scopedSaturated ? 1 : -1, "scoped_headroom"];
1506
- }
1507
- if (ma.saturated && mb.saturated) {
1508
- if (ma.sessionResetBucket !== mb.sessionResetBucket) {
1509
- return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
1510
- }
1511
- if (ma.weeklyReset !== mb.weeklyReset) {
1512
- return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
1513
- }
1514
- }
1515
- else {
1516
- if (ma.weeklyReset !== mb.weeklyReset) {
1517
- return [ma.weeklyReset - mb.weeklyReset, "weekly_reset"];
1518
- }
1519
- if (ma.sessionResetBucket !== mb.sessionResetBucket) {
1520
- return [ma.sessionResetBucket - mb.sessionResetBucket, "session_reset"];
1521
- }
1522
- }
1523
- // Fill-first within the per-model allowance: finish off the account closest
1524
- // to spending its cap for this model before opening a fresher one. Ranked
1525
- // above overall weekly utilization because it is the tighter constraint.
1526
- // Both sides must actually report a scoped window. Comparing a real
1527
- // utilization against the "absent" sentinel would rank the account that has a
1528
- // window above one that does not — and since only the account serving a model
1529
- // gets that model's window, it would funnel all of a model's traffic onto
1530
- // whichever account happened to serve it first.
1531
- if (ma.scopedUsed !== null &&
1532
- mb.scopedUsed !== null &&
1533
- ma.scopedUsedForSort !== mb.scopedUsedForSort) {
1534
- return [mb.scopedUsedForSort - ma.scopedUsedForSort, "scoped_utilization"];
1535
- }
1536
- if (ma.weeklyUsedForSort !== mb.weeklyUsedForSort) {
1537
- return [mb.weeklyUsedForSort - ma.weeklyUsedForSort, "weekly_utilization"];
1538
- }
1539
- if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
1540
- return [a.key === primaryKey ? -1 : 1, "configured_primary"];
1541
- }
1542
- return [0, "insertion_order"];
1543
- }
1544
1509
  function orderAccountsByQuotaWithMetrics(accounts, now, primaryKey, sessionSoftLimit, sessionResetToleranceMs, requestedModel) {
1545
1510
  const metricsByKey = new Map(accounts.map((account) => [
1546
1511
  account.key,
1547
1512
  accountSortMetrics(account.key, now, sessionSoftLimit, sessionResetToleranceMs, requestedModel),
1548
1513
  ]));
1549
- return {
1550
- orderedAccounts: [...accounts].sort((a, b) => compareAccountRoutingFactors(a, b, metricsByKey, primaryKey)[0]),
1514
+ const { orderedAccounts } = rankAccounts({
1515
+ accounts,
1551
1516
  metricsByKey,
1552
- };
1517
+ primaryKey,
1518
+ });
1519
+ return { orderedAccounts, metricsByKey };
1553
1520
  }
1554
1521
  /**
1555
1522
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
@@ -1706,7 +1673,7 @@ function buildRoutingDecision(args) {
1706
1673
  }
1707
1674
  else if (quotaOrdered) {
1708
1675
  mode = "quota";
1709
- selectionReason = compareAccountRoutingFactors(orderedAccounts[0], orderedAccounts[1], metricsByKey, primaryKey)[1];
1676
+ selectionReason = compareExpiryFirst(orderedAccounts[0], orderedAccounts[1], metricsByKey, primaryKey)[1];
1710
1677
  }
1711
1678
  else if (strategy === "round-robin") {
1712
1679
  mode = "round_robin";
@@ -6783,7 +6750,7 @@ function buildDeferredClaudeAccountFailureResponse(args) {
6783
6750
  return buildLoggedClaudeError(failure.status, failure.message, failure.errorType);
6784
6751
  }
6785
6752
  async function handleAnthropicRoutedClaudeRequest(args) {
6786
- const { ctx, body, modelRouter, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
6753
+ const { ctx, body, modelRouter, configGeneration, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
6787
6754
  const parsedRequest = parseClaudeRequest(body);
6788
6755
  const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedRequest);
6789
6756
  const hasConfiguredFallback = configuredFallbackPlan.attempts
@@ -6931,7 +6898,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6931
6898
  if (accountAdmissionCapacity !== undefined &&
6932
6899
  effectiveAccounts.length > 0 &&
6933
6900
  effectiveAccounts.every((account) => !isAccountAdmissionAvailable(account.key, accountAdmissionCapacity))) {
6934
- queuedAccountAdmission = await acquireFirstAvailableAccountAdmission(effectiveAccounts.map((account) => account.key), accountAdmissionCapacity, ctx.abortSignal);
6901
+ queuedAccountAdmission = await acquireFirstAvailableAccountAdmission(effectiveAccounts.map((account) => account.key), accountAdmissionCapacity, ctx.abortSignal, MAX_TRANSIENT_QUEUE_WAIT_MS, configGeneration);
6935
6902
  }
6936
6903
  accountLoop: for (const [accountIndex, account,] of effectiveAccounts.entries()) {
6937
6904
  const hasNextAccount = accountIndex < effectiveAccounts.length - 1;
@@ -7016,7 +6983,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7016
6983
  queuedAccountAdmission = undefined;
7017
6984
  }
7018
6985
  else {
7019
- admissionLease = tryAcquireAccountAdmission(account.key, accountAdmissionCapacity);
6986
+ admissionLease = tryAcquireAccountAdmission(account.key, accountAdmissionCapacity, configGeneration);
7020
6987
  if (admissionLease && queuedAccountAdmission) {
7021
6988
  // A preferred account became available while the race was being
7022
6989
  // established; release the lower-priority reservation.
@@ -7636,6 +7603,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
7636
7603
  ctx,
7637
7604
  body,
7638
7605
  modelRouter: requestModelRouter,
7606
+ configGeneration: requestRouting.generation,
7639
7607
  tracer,
7640
7608
  requestStartTime,
7641
7609
  accountStrategy: requestRouting.strategy,
@@ -8850,6 +8818,7 @@ export const __testHooks = {
8850
8818
  getStreamFailureDetails,
8851
8819
  trackUpstreamReadableStream,
8852
8820
  orderAccountsByQuota,
8821
+ orderAccountsByQuotaWithMetrics,
8853
8822
  scheduleAdaptiveQuotaRefreshes,
8854
8823
  scheduleHandoffQuotaRefresh,
8855
8824
  getQuotaRefreshState: (key) => accountQuotaRefreshCoordinator.getState(key),
@@ -8910,6 +8879,7 @@ export const __testHooks = {
8910
8879
  acquireFirstAvailableAccountAdmission,
8911
8880
  tryAcquireAccountAdmission,
8912
8881
  enqueueAccountAdmission,
8882
+ getAccountInflight,
8913
8883
  getAccountAdmissionSnapshot: (accountKey) => {
8914
8884
  const state = accountAdmissionStates.get(accountKey);
8915
8885
  return state
@@ -1100,6 +1100,9 @@ export type QueuedAccountAdmission = {
1100
1100
  /** One queued request waiting for per-account admission capacity. */
1101
1101
  export type AccountAdmissionWaiter = {
1102
1102
  capacity: number;
1103
+ /** Runtime-config generation of the snapshot the request queued under;
1104
+ * absent when no runtime config store publishes one. */
1105
+ generation?: number;
1103
1106
  resolve: (lease: AccountAdmissionLease) => void;
1104
1107
  };
1105
1108
  /** In-process request admission state for an OAuth account. */