@juspay/neurolink 10.6.4 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/analytics/pricing.d.ts +4 -1
  3. package/dist/analytics/pricing.js +6 -2
  4. package/dist/browser/neurolink.min.js +406 -406
  5. package/dist/cli/commands/proxyAnalyze.js +12 -0
  6. package/dist/core/analytics.js +8 -2
  7. package/dist/core/baseProvider.js +9 -7
  8. package/dist/core/modules/GenerationHandler.d.ts +8 -0
  9. package/dist/core/modules/GenerationHandler.js +28 -38
  10. package/dist/core/modules/TelemetryHandler.d.ts +3 -9
  11. package/dist/core/modules/TelemetryHandler.js +17 -12
  12. package/dist/lib/analytics/pricing.d.ts +4 -1
  13. package/dist/lib/analytics/pricing.js +6 -2
  14. package/dist/lib/core/analytics.js +8 -2
  15. package/dist/lib/core/baseProvider.js +9 -7
  16. package/dist/lib/core/modules/GenerationHandler.d.ts +8 -0
  17. package/dist/lib/core/modules/GenerationHandler.js +28 -38
  18. package/dist/lib/core/modules/TelemetryHandler.d.ts +3 -9
  19. package/dist/lib/core/modules/TelemetryHandler.js +17 -12
  20. package/dist/lib/neurolink.js +18 -1
  21. package/dist/lib/providers/amazonBedrock.js +68 -6
  22. package/dist/lib/providers/anthropic.js +50 -16
  23. package/dist/lib/providers/googleAiStudio.js +29 -4
  24. package/dist/lib/providers/googleNativeGemini3.js +10 -0
  25. package/dist/lib/providers/googleVertex.js +150 -25
  26. package/dist/lib/providers/litellm.js +13 -2
  27. package/dist/lib/providers/openAI.js +13 -2
  28. package/dist/lib/providers/openaiChatCompletionsBase.js +45 -10
  29. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +2 -14
  30. package/dist/lib/providers/openaiChatCompletionsClient.js +20 -1
  31. package/dist/lib/providers/sagemaker/language-model.js +5 -3
  32. package/dist/lib/providers/sagemaker/parsers.js +18 -9
  33. package/dist/lib/providers/sagemaker/streaming.d.ts +9 -0
  34. package/dist/lib/providers/sagemaker/streaming.js +64 -6
  35. package/dist/lib/proxy/accountCooldown.js +2 -7
  36. package/dist/lib/proxy/proxyAnalysis.js +197 -0
  37. package/dist/lib/proxy/requestLogger.js +11 -0
  38. package/dist/lib/proxy/routingEvidence.d.ts +6 -0
  39. package/dist/lib/proxy/routingEvidence.js +33 -0
  40. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  41. package/dist/lib/server/routes/claudeProxyRoutes.js +257 -87
  42. package/dist/lib/types/common.d.ts +3 -0
  43. package/dist/lib/types/openaiCompatible.d.ts +8 -7
  44. package/dist/lib/types/providers.d.ts +6 -0
  45. package/dist/lib/types/proxy.d.ts +97 -2
  46. package/dist/lib/utils/pricing.js +15 -3
  47. package/dist/lib/utils/tokenUtils.js +38 -3
  48. package/dist/neurolink.js +18 -1
  49. package/dist/providers/amazonBedrock.js +68 -6
  50. package/dist/providers/anthropic.js +50 -16
  51. package/dist/providers/googleAiStudio.js +29 -4
  52. package/dist/providers/googleNativeGemini3.js +10 -0
  53. package/dist/providers/googleVertex.js +150 -25
  54. package/dist/providers/litellm.js +13 -2
  55. package/dist/providers/openAI.js +13 -2
  56. package/dist/providers/openaiChatCompletionsBase.js +45 -10
  57. package/dist/providers/openaiChatCompletionsClient.d.ts +2 -14
  58. package/dist/providers/openaiChatCompletionsClient.js +20 -1
  59. package/dist/providers/sagemaker/language-model.js +5 -3
  60. package/dist/providers/sagemaker/parsers.js +18 -9
  61. package/dist/providers/sagemaker/streaming.d.ts +9 -0
  62. package/dist/providers/sagemaker/streaming.js +64 -6
  63. package/dist/proxy/accountCooldown.js +2 -7
  64. package/dist/proxy/proxyAnalysis.js +197 -0
  65. package/dist/proxy/requestLogger.js +11 -0
  66. package/dist/proxy/routingEvidence.d.ts +6 -0
  67. package/dist/proxy/routingEvidence.js +32 -0
  68. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  69. package/dist/server/routes/claudeProxyRoutes.js +257 -87
  70. package/dist/types/common.d.ts +3 -0
  71. package/dist/types/openaiCompatible.d.ts +8 -7
  72. package/dist/types/providers.d.ts +6 -0
  73. package/dist/types/proxy.d.ts +97 -2
  74. package/dist/utils/pricing.js +15 -3
  75. package/dist/utils/tokenUtils.js +38 -3
  76. package/package.json +2 -1
@@ -87,12 +87,12 @@ async function createProtocolSpecificStream(responseStream, parser, capability,
87
87
  if (done) {
88
88
  // Stream ended - send final chunk if needed
89
89
  if (!finalUsage && accumulatedText) {
90
- finalUsage = estimateTokenUsage("", accumulatedText);
90
+ finalUsage = estimateTokenUsage(options.prompt ?? "", accumulatedText);
91
91
  }
92
92
  const finalChunk = {
93
93
  type: "finish",
94
94
  finishReason: "stop",
95
- usage: finalUsage,
95
+ usage: toLanguageModelV2Usage(finalUsage),
96
96
  };
97
97
  controller.enqueue(finalChunk);
98
98
  options.onComplete?.(finalUsage || {
@@ -182,11 +182,11 @@ async function createProtocolSpecificStream(responseStream, parser, capability,
182
182
  if (parser.isComplete(chunk)) {
183
183
  finalUsage =
184
184
  parser.extractUsage(chunk) ||
185
- estimateTokenUsage("", accumulatedText);
185
+ estimateTokenUsage(options.prompt ?? "", accumulatedText);
186
186
  const finalChunk = {
187
187
  type: "finish",
188
188
  finishReason: chunk.finishReason || "stop",
189
- usage: finalUsage,
189
+ usage: toLanguageModelV2Usage(finalUsage),
190
190
  };
191
191
  controller.enqueue(finalChunk);
192
192
  options.onComplete?.(finalUsage);
@@ -271,7 +271,7 @@ async function createSyntheticStreamFromResponse(responseStream, options) {
271
271
  const finalChunk = {
272
272
  type: "finish",
273
273
  finishReason: "stop",
274
- usage,
274
+ usage: toLanguageModelV2Usage(usage),
275
275
  };
276
276
  controller.enqueue(finalChunk);
277
277
  options.onComplete?.(usage);
@@ -305,7 +305,7 @@ export async function createSyntheticStream(text, usage, options = {}) {
305
305
  const finalChunk = {
306
306
  type: "finish",
307
307
  finishReason: "stop",
308
- usage,
308
+ usage: toLanguageModelV2Usage(usage),
309
309
  };
310
310
  controller.enqueue(finalChunk);
311
311
  options.onComplete?.(usage);
@@ -313,6 +313,64 @@ export async function createSyntheticStream(text, usage, options = {}) {
313
313
  },
314
314
  });
315
315
  }
316
+ /**
317
+ * Map the internal SageMakerUsage shape to AI SDK v2 usage keys. Stream
318
+ * finish parts previously carried promptTokens/completionTokens, but the
319
+ * SDK's v2→v3 bridge reads inputTokens/outputTokens/totalTokens — every
320
+ * streamed SageMaker turn resolved to undefined (zero) usage.
321
+ */
322
+ function toLanguageModelV2Usage(usage) {
323
+ const inputTokens = usage?.promptTokens ?? 0;
324
+ const outputTokens = usage?.completionTokens ?? 0;
325
+ return {
326
+ inputTokens,
327
+ outputTokens,
328
+ totalTokens: usage?.total || inputTokens + outputTokens,
329
+ };
330
+ }
331
+ /**
332
+ * Extract REAL token usage from a complete (non-streaming) SageMaker
333
+ * response body when the endpoint reports it. Covers the OpenAI-compatible
334
+ * shape (vLLM / TGI-OpenAI: usage.prompt_tokens/completion_tokens), the
335
+ * generic input_tokens/output_tokens shape, and the HuggingFace TGI shape
336
+ * (details.tokens.input/generated). Returns undefined when no real counts
337
+ * are present so callers can fall back to estimateTokenUsage.
338
+ */
339
+ export function parseUsageFromResponseBody(body) {
340
+ // TGI-style endpoints wrap the response in an array
341
+ // ([{ generated_text, details: { tokens } }]) — normalize to the first
342
+ // item so those endpoints get real usage instead of the char estimate.
343
+ const responseBody = Array.isArray(body) ? body[0] : body;
344
+ if (!responseBody || typeof responseBody !== "object") {
345
+ return undefined;
346
+ }
347
+ const usageRecord = (responseBody.usage ?? responseBody.tokens);
348
+ if (usageRecord && typeof usageRecord === "object") {
349
+ const promptTokens = Number(usageRecord.prompt_tokens ?? usageRecord.input_tokens) || 0;
350
+ const completionTokens = Number(usageRecord.completion_tokens ?? usageRecord.output_tokens) || 0;
351
+ if (promptTokens > 0 || completionTokens > 0) {
352
+ return {
353
+ promptTokens,
354
+ completionTokens,
355
+ total: Number(usageRecord.total_tokens) || promptTokens + completionTokens,
356
+ };
357
+ }
358
+ }
359
+ const details = responseBody.details;
360
+ const tokens = details?.tokens;
361
+ if (tokens && typeof tokens === "object") {
362
+ const promptTokens = Number(tokens.input) || 0;
363
+ const completionTokens = Number(tokens.generated) || 0;
364
+ if (promptTokens > 0 || completionTokens > 0) {
365
+ return {
366
+ promptTokens,
367
+ completionTokens,
368
+ total: Number(tokens.total) || promptTokens + completionTokens,
369
+ };
370
+ }
371
+ }
372
+ return undefined;
373
+ }
316
374
  /**
317
375
  * Estimate token usage from text content
318
376
  *
@@ -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;