@juspay/neurolink 12.24.0 → 12.25.0

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.
@@ -1,7 +1,7 @@
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", "scoped_headroom", "session_reset", "weekly_reset", "weekly_utilization", "scoped_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", "headroom", "session_affinity", "preferred_primary", "spill_inflight", "routing_policy_error"];
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
7
  /**
@@ -26,6 +26,11 @@ export const PROXY_ACCOUNT_ROUTING_REASONS = [
26
26
  "weekly_reset",
27
27
  "weekly_utilization",
28
28
  "scoped_utilization",
29
+ "headroom",
30
+ "session_affinity",
31
+ "preferred_primary",
32
+ "spill_inflight",
33
+ "routing_policy_error",
29
34
  ];
30
35
  export const PROXY_ACCOUNT_TYPES = ["oauth", "api_key"];
31
36
  export const ACCOUNT_COOLING_REASONS = [
@@ -229,6 +229,11 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
229
229
  .digest("hex")
230
230
  .slice(0, 16);
231
231
  const useOverage = routing?.useOverage ?? "auto";
232
+ const accountRanking = routing?.accountRanking ?? "expiry-first";
233
+ const preferPrimary = routing?.preferPrimary ?? false;
234
+ const sessionAffinity = routing?.sessionAffinity ?? false;
235
+ const sessionAffinityIdleTtlMs = routing?.sessionAffinityIdleTtlMs ?? 3_600_000;
236
+ const spillInflight = routing?.spillInflight ?? 0;
232
237
  const fingerprintSource = JSON.stringify({
233
238
  strategy,
234
239
  passthrough: options.passthrough,
@@ -239,6 +244,11 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
239
244
  sessionSoftLimit,
240
245
  sessionResetToleranceMs,
241
246
  useOverage,
247
+ accountRanking,
248
+ preferPrimary,
249
+ sessionAffinity,
250
+ sessionAffinityIdleTtlMs,
251
+ spillInflight,
242
252
  });
243
253
  const configHash = createHash("sha256")
244
254
  .update(fingerprintSource)
@@ -259,6 +269,11 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
259
269
  sessionSoftLimit,
260
270
  sessionResetToleranceMs,
261
271
  useOverage,
272
+ accountRanking,
273
+ preferPrimary,
274
+ sessionAffinity,
275
+ sessionAffinityIdleTtlMs,
276
+ spillInflight,
262
277
  }),
263
278
  configFilePresent,
264
279
  envFilePresent,
@@ -0,0 +1,23 @@
1
+ /** Bind a session to the account that just served it, refreshing its idle clock. */
2
+ export declare function bind(sessionId: string, accountKey: string, now: number): void;
3
+ /**
4
+ * The account a session is bound to, or undefined if unbound or idle-expired.
5
+ * A read never refreshes the binding — only a served request (bind) does,
6
+ * since the idle TTL counts from the session's last served request.
7
+ */
8
+ export declare function get(sessionId: string, now: number, idleTtlMs: number): string | undefined;
9
+ export declare function clear(): void;
10
+ export declare function size(): number;
11
+ /**
12
+ * Sessions whose binding has not idle-expired. Expired entries are dropped
13
+ * here as on read, since otherwise they would linger until the cap evicted
14
+ * them.
15
+ */
16
+ export declare function countActive(now: number, idleTtlMs: number): number;
17
+ export declare const sessionAffinity: Readonly<{
18
+ bind: typeof bind;
19
+ get: typeof get;
20
+ clear: typeof clear;
21
+ size: typeof size;
22
+ countActive: typeof countActive;
23
+ }>;
@@ -0,0 +1,62 @@
1
+ const MAX_BOUND_SESSIONS = 5_000;
2
+ // Per-record shape is an inline type on the Map's generic parameter rather
3
+ // than a named `type` alias: it is a private, single-use implementation
4
+ // detail with no external consumer, so it does not belong in
5
+ // src/lib/types/ (CLAUDE.md rule 2 targets shared type definitions).
6
+ const bindings = new Map();
7
+ /** Bind a session to the account that just served it, refreshing its idle clock. */
8
+ export function bind(sessionId, accountKey, now) {
9
+ // Delete-then-set moves the entry to the end of Map iteration order, which
10
+ // is what makes that order double as least-recently-bound-first for the
11
+ // cap eviction below.
12
+ bindings.delete(sessionId);
13
+ bindings.set(sessionId, { accountKey, lastServedAt: now });
14
+ if (bindings.size > MAX_BOUND_SESSIONS) {
15
+ const oldestKey = bindings.keys().next().value;
16
+ if (oldestKey !== undefined) {
17
+ bindings.delete(oldestKey);
18
+ }
19
+ }
20
+ }
21
+ /**
22
+ * The account a session is bound to, or undefined if unbound or idle-expired.
23
+ * A read never refreshes the binding — only a served request (bind) does,
24
+ * since the idle TTL counts from the session's last served request.
25
+ */
26
+ export function get(sessionId, now, idleTtlMs) {
27
+ const record = bindings.get(sessionId);
28
+ if (!record) {
29
+ return undefined;
30
+ }
31
+ if (now - record.lastServedAt > idleTtlMs) {
32
+ bindings.delete(sessionId);
33
+ return undefined;
34
+ }
35
+ return record.accountKey;
36
+ }
37
+ export function clear() {
38
+ bindings.clear();
39
+ }
40
+ export function size() {
41
+ return bindings.size;
42
+ }
43
+ /**
44
+ * Sessions whose binding has not idle-expired. Expired entries are dropped
45
+ * here as on read, since otherwise they would linger until the cap evicted
46
+ * them.
47
+ */
48
+ export function countActive(now, idleTtlMs) {
49
+ for (const [sessionId, record] of bindings) {
50
+ if (now - record.lastServedAt > idleTtlMs) {
51
+ bindings.delete(sessionId);
52
+ }
53
+ }
54
+ return bindings.size;
55
+ }
56
+ export const sessionAffinity = Object.freeze({
57
+ bind,
58
+ get,
59
+ clear,
60
+ size,
61
+ countActive,
62
+ });
@@ -2,7 +2,7 @@ import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
2
2
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
3
3
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
4
4
  import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
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";
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, ProxyAccountRankingPolicy, ProxyAccountRoutingAffinityEvidence, ProxyAccountRoutingDecision, ProxyAccountRoutingReason, ProxyAccountRoutingSpillEvidence, ProxyAccountSortMetrics, ProxyAffinityPrecedenceSkipReason, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, ProxyRoutingPolicySnapshot, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
6
6
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined, generation?: number): AccountAdmissionLease | undefined;
7
7
  /** Current in-flight request count for one account, unlimited or capped. */
8
8
  declare function getAccountInflight(accountKey: string): number;
@@ -121,9 +121,18 @@ declare function evaluateScopedExhaustion(accounts: ProxyPassthroughAccount[], r
121
121
  eligible: ProxyPassthroughAccount[];
122
122
  exhaustion: AnthropicScopedExhaustion | null;
123
123
  };
124
- declare function orderAccountsByQuotaWithMetrics(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit: number, sessionResetToleranceMs: number, requestedModel?: string): {
124
+ declare function orderAccountsByQuotaWithMetrics(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit: number, sessionResetToleranceMs: number, options?: {
125
+ requestedModel?: string;
126
+ policy?: {
127
+ ranking?: ProxyAccountRankingPolicy;
128
+ affinityKey?: string;
129
+ preferPrimary?: boolean;
130
+ };
131
+ }): {
125
132
  orderedAccounts: ProxyPassthroughAccount[];
126
133
  metricsByKey: Map<string, ProxyAccountSortMetrics>;
134
+ reason: ProxyAccountRoutingReason;
135
+ affinitySkippedReason: ProxyAffinityPrecedenceSkipReason | null;
127
136
  };
128
137
  /**
129
138
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
@@ -164,7 +173,30 @@ declare function buildRoutingDecision(args: {
164
173
  sessionSoftLimit: number;
165
174
  sessionResetToleranceMs: number;
166
175
  rotationOffset: number;
176
+ policySelectionReason?: ProxyAccountRoutingReason;
177
+ policy?: ProxyRoutingPolicySnapshot;
178
+ affinity?: ProxyAccountRoutingAffinityEvidence;
179
+ spill?: ProxyAccountRoutingSpillEvidence;
167
180
  }): ProxyAccountRoutingDecision | undefined;
181
+ declare function selectClaudeProxyAccountOrder(args: {
182
+ enabledAccounts: ProxyPassthroughAccount[];
183
+ accountStrategy: "round-robin" | "fill-first";
184
+ primaryAccountKey: string | undefined;
185
+ quotaRoutingEnabled: boolean;
186
+ sessionSoftLimit: number;
187
+ sessionResetToleranceMs: number;
188
+ requestedModel?: string;
189
+ sessionId?: string;
190
+ ranking: ProxyAccountRankingPolicy;
191
+ preferPrimary: boolean;
192
+ sessionAffinityEnabled: boolean;
193
+ sessionAffinityIdleTtlMs: number;
194
+ spillInflight: number;
195
+ setRoutingDecision: (decision: ProxyAccountRoutingDecision) => void;
196
+ }): {
197
+ orderedAccounts: ProxyPassthroughAccount[];
198
+ metricsByKey: Map<string, ProxyAccountSortMetrics>;
199
+ };
168
200
  declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>): {
169
201
  stream: ReadableStream<Uint8Array>;
170
202
  outcome: Promise<StreamTerminalOutcome>;
@@ -314,6 +346,7 @@ declare function handleAnthropicAuthRetry(args: {
314
346
  sawRateLimit: boolean;
315
347
  sawTransientFailure: boolean;
316
348
  sawNetworkError: boolean;
349
+ bindServedSession?: (servingAccountKey: string) => void;
317
350
  }): Promise<AnthropicAuthRetryResult>;
318
351
  declare function finalizeAnthropicTerminalFetchError(args: {
319
352
  terminalError: NonNullable<AnthropicUpstreamFetchResult["terminalError"]>;
@@ -506,7 +539,14 @@ export declare const __testHooks: {
506
539
  scheduleAdaptiveQuotaRefreshes: typeof scheduleAdaptiveQuotaRefreshes;
507
540
  scheduleHandoffQuotaRefresh: typeof scheduleHandoffQuotaRefresh;
508
541
  getQuotaRefreshState: (key: string) => import("../../index.js").ProxyQuotaRefreshRuntimeState;
509
- buildQuotaRoutingDecision: (accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number, requestedModel?: string) => ProxyAccountRoutingDecision | undefined;
542
+ buildQuotaRoutingDecision: (accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number, options?: {
543
+ requestedModel?: string;
544
+ policy?: {
545
+ ranking?: ProxyAccountRankingPolicy;
546
+ affinityKey?: string;
547
+ preferPrimary?: boolean;
548
+ };
549
+ }) => ProxyAccountRoutingDecision | undefined;
510
550
  buildRoutingDecision: typeof buildRoutingDecision;
511
551
  resetEpochToMs: typeof resetEpochToMs;
512
552
  seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
@@ -520,6 +560,11 @@ export declare const __testHooks: {
520
560
  bodyStr: string;
521
561
  sessionId?: string;
522
562
  };
563
+ extractSnapshotBodySessionIdForTests: (body: unknown) => string | undefined;
564
+ selectClaudeProxyAccountOrderForTests: typeof selectClaudeProxyAccountOrder;
565
+ setForceSpillThrowForTests: (value: boolean) => void;
566
+ setForceAffinityLookupThrowForTests: (value: boolean) => void;
567
+ setForceBindRecheckThrowForTests: (value: boolean) => void;
523
568
  isAntiAbuseConstruction429: typeof isAntiAbuseConstruction429;
524
569
  fetchAnthropicAccountResponse: typeof fetchAnthropicAccountResponse;
525
570
  finalizeAnthropicTerminalFetchError: typeof finalizeAnthropicTerminalFetchError;