@juspay/neurolink 10.6.5 → 10.7.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.
@@ -42,6 +42,17 @@ function printAnalysis(report) {
42
42
  logger.always(chalk.yellow(" Rate limits: unavailable"));
43
43
  }
44
44
  logger.always("");
45
+ logger.always(chalk.bold(" Account Routing"));
46
+ if (report.coverage.routingDecisions) {
47
+ logger.always(` Decisions: ${report.routing.records.length}, modes: ${JSON.stringify(report.routing.modes)}`);
48
+ logger.always(` Selection reasons: ${JSON.stringify(report.routing.selectionReasons)}`);
49
+ logger.always(` Initial accounts: ${JSON.stringify(report.routing.initialAccounts)}`);
50
+ logger.always(` Final account changed after retry: ${report.routing.finalAccountChanges}, outside candidate set: ${report.routing.finalOutsideCandidateSet}`);
51
+ }
52
+ else {
53
+ logger.always(chalk.yellow(" Routing decisions: unavailable (legacy or no logs)"));
54
+ }
55
+ logger.always("");
45
56
  logger.always(chalk.bold(" Latency (ms)"));
46
57
  const latencyHeader = `${"METRIC".padEnd(22)} ${"COUNT".padStart(7)} ${"P50".padStart(9)} ${"P95".padStart(9)} ${"P99".padStart(9)} ${"MAX".padStart(9)}`;
47
58
  logger.always(` ${chalk.gray(latencyHeader)}`);
@@ -68,6 +79,7 @@ function printAnalysis(report) {
68
79
  logger.always("");
69
80
  logger.always(chalk.bold(" Data Quality"));
70
81
  logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates`);
82
+ logger.always(` Routing decisions: ${report.dataQuality.routingDecisions.valid} valid, ${report.dataQuality.routingDecisions.invalid} invalid, ${report.dataQuality.routingDecisions.absent} absent`);
71
83
  for (const [stream, range] of Object.entries(report.dataQuality.streams)) {
72
84
  if (range.observedFrom) {
73
85
  logger.always(` ${stream}: ${range.observedFrom} to ${range.observedTo}${range.startsAtOrBeforeRequestedWindow ? "" : chalk.yellow(" (starts after requested window)")}`);
@@ -2,15 +2,10 @@ import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { AsyncMutex } from "../utils/asyncMutex.js";
5
+ import { ACCOUNT_COOLING_REASONS } from "./routingEvidence.js";
5
6
  import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
6
7
  const COOLDOWN_FILE = "account-cooldowns.json";
7
- const VALID_REASONS = new Set([
8
- "weekly",
9
- "session",
10
- "unified",
11
- "transient",
12
- "auth",
13
- ]);
8
+ const VALID_REASONS = new Set(ACCOUNT_COOLING_REASONS);
14
9
  let customCooldownFilePath = null;
15
10
  let cacheLoaded = false;
16
11
  let cacheLoadPromise = null;
@@ -3,6 +3,7 @@ import { lstat, readdir, realpath, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { createInterface } from "node:readline";
5
5
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES, } from "./routingEvidence.js";
6
7
  const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
7
8
  const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
8
9
  const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
@@ -14,6 +15,11 @@ const LIFECYCLE_EVENTS = new Set([
14
15
  "response_first_chunk",
15
16
  "request_terminal",
16
17
  ]);
18
+ const ROUTING_STRATEGIES = new Set(PROXY_ACCOUNT_ROUTING_STRATEGIES);
19
+ const ROUTING_MODES = new Set(PROXY_ACCOUNT_ROUTING_MODES);
20
+ const ROUTING_REASONS = new Set(PROXY_ACCOUNT_ROUTING_REASONS);
21
+ const ROUTING_ACCOUNT_TYPES = new Set(PROXY_ACCOUNT_TYPES);
22
+ const COOLING_REASONS = new Set(ACCOUNT_COOLING_REASONS);
17
23
  function isContainedPath(root, candidate) {
18
24
  const candidateRelative = relative(root, candidate);
19
25
  return (candidateRelative.length > 0 &&
@@ -45,6 +51,130 @@ function finiteNumber(value) {
45
51
  function stringValue(value) {
46
52
  return typeof value === "string" && value.length > 0 ? value : null;
47
53
  }
54
+ function isNullableString(value) {
55
+ return value === null || (typeof value === "string" && value.length > 0);
56
+ }
57
+ function isNullableFiniteNumber(value) {
58
+ return value === null || finiteNumber(value) !== null;
59
+ }
60
+ function routingCandidateValue(value) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
62
+ return null;
63
+ }
64
+ const candidate = value;
65
+ const requiredBooleanFields = [
66
+ "configuredPrimary",
67
+ "usable",
68
+ "saturated",
69
+ "quotaObserved",
70
+ "coolingActive",
71
+ ];
72
+ const requiredNullableNumberFields = [
73
+ "quotaLastUpdated",
74
+ "quotaAgeMs",
75
+ "coolingUntil",
76
+ "sessionUsed",
77
+ "sessionResetAt",
78
+ "sessionResetBucket",
79
+ "weeklyUsed",
80
+ "weeklyResetAt",
81
+ ];
82
+ const requiredNullableStringFields = [
83
+ "unifiedStatus",
84
+ "overageStatus",
85
+ "sessionStatus",
86
+ "weeklyStatus",
87
+ ];
88
+ if (!stringValue(candidate.account) ||
89
+ typeof candidate.accountType !== "string" ||
90
+ !ROUTING_ACCOUNT_TYPES.has(candidate.accountType) ||
91
+ !Number.isInteger(candidate.sourceIndex) ||
92
+ candidate.sourceIndex < 0 ||
93
+ !Number.isInteger(candidate.rank) ||
94
+ candidate.rank < 0 ||
95
+ requiredBooleanFields.some((field) => typeof candidate[field] !== "boolean") ||
96
+ requiredNullableNumberFields.some((field) => !isNullableFiniteNumber(candidate[field])) ||
97
+ requiredNullableStringFields.some((field) => !isNullableString(candidate[field])) ||
98
+ !(candidate.coolingReason === null ||
99
+ (typeof candidate.coolingReason === "string" &&
100
+ COOLING_REASONS.has(candidate.coolingReason)))) {
101
+ return null;
102
+ }
103
+ return {
104
+ account: candidate.account,
105
+ accountType: candidate.accountType,
106
+ sourceIndex: candidate.sourceIndex,
107
+ rank: candidate.rank,
108
+ configuredPrimary: candidate.configuredPrimary,
109
+ usable: candidate.usable,
110
+ saturated: candidate.saturated,
111
+ quotaObserved: candidate.quotaObserved,
112
+ quotaLastUpdated: candidate.quotaLastUpdated,
113
+ quotaAgeMs: candidate.quotaAgeMs,
114
+ coolingActive: candidate.coolingActive,
115
+ coolingReason: candidate.coolingReason,
116
+ coolingUntil: candidate.coolingUntil,
117
+ unifiedStatus: candidate.unifiedStatus,
118
+ overageStatus: candidate.overageStatus,
119
+ sessionStatus: candidate.sessionStatus,
120
+ sessionUsed: candidate.sessionUsed,
121
+ sessionResetAt: candidate.sessionResetAt,
122
+ sessionResetBucket: candidate.sessionResetBucket,
123
+ weeklyStatus: candidate.weeklyStatus,
124
+ weeklyUsed: candidate.weeklyUsed,
125
+ weeklyResetAt: candidate.weeklyResetAt,
126
+ };
127
+ }
128
+ function routingDecisionValue(value) {
129
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
130
+ return null;
131
+ }
132
+ const decision = value;
133
+ const candidates = Array.isArray(decision.candidates)
134
+ ? decision.candidates.map(routingCandidateValue)
135
+ : [];
136
+ if (decision.schemaVersion !== 1 ||
137
+ !stringValue(decision.evaluatedAt) ||
138
+ !Number.isFinite(Date.parse(String(decision.evaluatedAt))) ||
139
+ typeof decision.strategy !== "string" ||
140
+ !ROUTING_STRATEGIES.has(decision.strategy) ||
141
+ typeof decision.mode !== "string" ||
142
+ !ROUTING_MODES.has(decision.mode) ||
143
+ typeof decision.selectionReason !== "string" ||
144
+ !ROUTING_REASONS.has(decision.selectionReason) ||
145
+ typeof decision.quotaRoutingEnabled !== "boolean" ||
146
+ typeof decision.quotaInputsUsed !== "boolean" ||
147
+ finiteNumber(decision.sessionSoftLimit) === null ||
148
+ decision.sessionSoftLimit <= 0 ||
149
+ decision.sessionSoftLimit > 1 ||
150
+ !Number.isInteger(decision.sessionResetToleranceMs) ||
151
+ decision.sessionResetToleranceMs <= 0 ||
152
+ !isNullableString(decision.configuredPrimaryAccount) ||
153
+ typeof decision.configuredPrimaryMatched !== "boolean" ||
154
+ !Number.isInteger(decision.rotationOffset) ||
155
+ decision.rotationOffset < 0 ||
156
+ !stringValue(decision.initialAccount) ||
157
+ candidates.length === 0 ||
158
+ candidates.some((candidate) => candidate === null)) {
159
+ return null;
160
+ }
161
+ return {
162
+ schemaVersion: 1,
163
+ evaluatedAt: decision.evaluatedAt,
164
+ strategy: decision.strategy,
165
+ mode: decision.mode,
166
+ selectionReason: decision.selectionReason,
167
+ quotaRoutingEnabled: decision.quotaRoutingEnabled,
168
+ quotaInputsUsed: decision.quotaInputsUsed,
169
+ sessionSoftLimit: decision.sessionSoftLimit,
170
+ sessionResetToleranceMs: decision.sessionResetToleranceMs,
171
+ configuredPrimaryAccount: decision.configuredPrimaryAccount,
172
+ configuredPrimaryMatched: decision.configuredPrimaryMatched,
173
+ rotationOffset: decision.rotationOffset,
174
+ initialAccount: decision.initialAccount,
175
+ candidates: candidates,
176
+ };
177
+ }
48
178
  function percentile(sorted, fraction) {
49
179
  if (sorted.length === 0) {
50
180
  return null;
@@ -204,6 +334,47 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
204
334
  singleAttemptDelta,
205
335
  };
206
336
  }
337
+ function summarizeRouting(finalRequests) {
338
+ const modes = {};
339
+ const selectionReasons = {};
340
+ const initialAccounts = {};
341
+ const records = [];
342
+ let finalAccountChanges = 0;
343
+ let finalOutsideCandidateSet = 0;
344
+ for (const [requestId, request] of finalRequests) {
345
+ const decision = request.routingDecision;
346
+ if (!decision) {
347
+ continue;
348
+ }
349
+ increment(modes, decision.mode);
350
+ increment(selectionReasons, decision.selectionReason);
351
+ increment(initialAccounts, decision.initialAccount);
352
+ const finalCandidate = decision.candidates.some((candidate) => candidate.account === request.account &&
353
+ candidate.accountType === request.accountType);
354
+ if (!finalCandidate) {
355
+ finalOutsideCandidateSet += 1;
356
+ }
357
+ else if (decision.initialAccount !== request.account) {
358
+ finalAccountChanges += 1;
359
+ }
360
+ records.push({
361
+ requestId,
362
+ timestamp: request.timestamp,
363
+ responseStatus: request.status,
364
+ finalAccount: request.account,
365
+ finalAccountType: request.accountType,
366
+ decision,
367
+ });
368
+ }
369
+ return {
370
+ modes,
371
+ selectionReasons,
372
+ initialAccounts,
373
+ finalAccountChanges,
374
+ finalOutsideCandidateSet,
375
+ records,
376
+ };
377
+ }
207
378
  async function discoverLogFiles(logsDir) {
208
379
  const entries = await readdir(logsDir, { withFileTypes: true }).catch((error) => {
209
380
  throw new Error(`Unable to read proxy logs at ${logsDir}: ${error.message}`);
@@ -400,6 +571,9 @@ export async function analyzeProxyLogs(options) {
400
571
  }
401
572
  const finalRequests = new Map();
402
573
  const terminalStreamErrors = new Set();
574
+ let validRoutingDecisions = 0;
575
+ let invalidRoutingDecisions = 0;
576
+ let absentRoutingDecisions = 0;
403
577
  for (const filePath of requestFiles) {
404
578
  linesRead += await readJsonLines(filePath, (record) => {
405
579
  const timestamp = observeTimestamp("requests", record);
@@ -418,7 +592,21 @@ export async function analyzeProxyLogs(options) {
418
592
  if (status === null || !stringValue(record.method)) {
419
593
  return;
420
594
  }
595
+ const hasRoutingDecision = Object.prototype.hasOwnProperty.call(record, "routingDecision");
596
+ const routingDecision = hasRoutingDecision
597
+ ? routingDecisionValue(record.routingDecision)
598
+ : null;
599
+ if (routingDecision) {
600
+ validRoutingDecisions += 1;
601
+ }
602
+ else if (hasRoutingDecision) {
603
+ invalidRoutingDecisions += 1;
604
+ }
605
+ else {
606
+ absentRoutingDecisions += 1;
607
+ }
421
608
  finalRequests.set(requestId, {
609
+ timestamp: new Date(timestamp).toISOString(),
422
610
  status,
423
611
  durationMs: finiteNumber(record.responseTimeMs),
424
612
  account: stringValue(record.account) ?? "unknown",
@@ -428,6 +616,7 @@ export async function analyzeProxyLogs(options) {
428
616
  cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
429
617
  errorType: stringValue(record.errorType),
430
618
  errorCode: stringValue(record.errorCode),
619
+ routingDecision,
431
620
  });
432
621
  }, () => {
433
622
  malformedLines += 1;
@@ -498,6 +687,7 @@ export async function analyzeProxyLogs(options) {
498
687
  }
499
688
  const artifactsReferenced = artifactsPresent + artifactsMissing;
500
689
  const finalSummary = summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts);
690
+ const routingSummary = summarizeRouting(finalRequests);
501
691
  return {
502
692
  generatedAt: new Date(nowMs).toISOString(),
503
693
  since: new Date(sinceMs).toISOString(),
@@ -514,6 +704,7 @@ export async function analyzeProxyLogs(options) {
514
704
  attempts: totalAttempts > 0,
515
705
  attemptLatency: attemptLatency.length > 0,
516
706
  cacheUsage: finalSummary.cache.requestsWithUsage > 0,
707
+ routingDecisions: routingSummary.records.length > 0,
517
708
  },
518
709
  dataQuality: {
519
710
  linesRead,
@@ -538,6 +729,11 @@ export async function analyzeProxyLogs(options) {
538
729
  writeFailures,
539
730
  truncatedCaptures,
540
731
  },
732
+ routingDecisions: {
733
+ valid: validRoutingDecisions,
734
+ invalid: invalidRoutingDecisions,
735
+ absent: absentRoutingDecisions,
736
+ },
541
737
  },
542
738
  lifecycle: {
543
739
  accepted: accepted.size,
@@ -572,6 +768,7 @@ export async function analyzeProxyLogs(options) {
572
768
  singleAttemptDelta: summarizeLatency(finalSummary.singleAttemptDelta),
573
769
  },
574
770
  cache: finalSummary.cache,
771
+ routing: routingSummary,
575
772
  accounts: [...accounts.values()].sort((left, right) => right.attempts - left.attempts),
576
773
  };
577
774
  }
@@ -236,6 +236,17 @@ function emitOtlpLogRecord(entry) {
236
236
  // Account info
237
237
  "account.name": entry.account,
238
238
  "account.type": entry.accountType,
239
+ // Compact routing summary. Full candidate evidence stays in JSONL.
240
+ ...(entry.routingDecision && {
241
+ "routing.mode": entry.routingDecision.mode,
242
+ "routing.strategy": entry.routingDecision.strategy,
243
+ "routing.selection_reason": entry.routingDecision.selectionReason,
244
+ "routing.initial_account": entry.routingDecision.initialAccount,
245
+ "routing.candidate_count": entry.routingDecision.candidates.length,
246
+ "routing.final_account_changed": entry.routingDecision.initialAccount !== entry.account &&
247
+ entry.routingDecision.candidates.some((candidate) => candidate.account === entry.account &&
248
+ candidate.accountType === entry.accountType),
249
+ }),
239
250
  // Token usage (when available)
240
251
  ...(entry.inputTokens !== undefined && {
241
252
  "ai.input_tokens": entry.inputTokens,
@@ -0,0 +1,6 @@
1
+ /** Schema-v1 routing evidence values shared by emitters and offline readers. */
2
+ export declare const PROXY_ACCOUNT_ROUTING_STRATEGIES: readonly ["round-robin", "fill-first"];
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_probe", "session_headroom", "session_reset", "weekly_reset", "weekly_utilization"];
5
+ export declare const PROXY_ACCOUNT_TYPES: readonly ["oauth", "api_key"];
6
+ export declare const ACCOUNT_COOLING_REASONS: readonly ["weekly", "session", "unified", "transient", "auth"];
@@ -0,0 +1,33 @@
1
+ /** Schema-v1 routing evidence values shared by emitters and offline readers. */
2
+ export const PROXY_ACCOUNT_ROUTING_STRATEGIES = [
3
+ "round-robin",
4
+ "fill-first",
5
+ ];
6
+ export const PROXY_ACCOUNT_ROUTING_MODES = [
7
+ "quota",
8
+ "primary",
9
+ "round_robin",
10
+ "single_account",
11
+ ];
12
+ export const PROXY_ACCOUNT_ROUTING_REASONS = [
13
+ "single_account",
14
+ "round_robin",
15
+ "configured_primary",
16
+ "insertion_order",
17
+ "availability",
18
+ "cooldown_recovery",
19
+ "quota_probe",
20
+ "session_headroom",
21
+ "session_reset",
22
+ "weekly_reset",
23
+ "weekly_utilization",
24
+ ];
25
+ export const PROXY_ACCOUNT_TYPES = ["oauth", "api_key"];
26
+ export const ACCOUNT_COOLING_REASONS = [
27
+ "weekly",
28
+ "session",
29
+ "unified",
30
+ "transient",
31
+ "auth",
32
+ ];
33
+ //# sourceMappingURL=routingEvidence.js.map
@@ -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, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
16
  /** Resolve the configured primary's stable key to its current index in the
17
17
  * request's enabledAccounts list. Returns 0 (insertion-order fallback) when
18
18
  * no key is configured or the key cannot be matched (account disabled/
@@ -279,6 +279,7 @@ export declare const __testHooks: {
279
279
  getStreamFailureDetails: typeof getStreamFailureDetails;
280
280
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
281
281
  orderAccountsByQuota: typeof orderAccountsByQuota;
282
+ buildQuotaRoutingDecision: (accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number) => ProxyAccountRoutingDecision;
282
283
  resetEpochToMs: typeof resetEpochToMs;
283
284
  seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
284
285
  reconcileEligibleAccountRuntimeState: typeof reconcileEligibleAccountRuntimeState;