@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.
- package/CHANGELOG.md +12 -0
- package/dist/analytics/pricing.d.ts +4 -1
- package/dist/analytics/pricing.js +6 -2
- package/dist/browser/neurolink.min.js +406 -406
- package/dist/cli/commands/proxyAnalyze.js +12 -0
- package/dist/core/analytics.js +8 -2
- package/dist/core/baseProvider.js +9 -7
- package/dist/core/modules/GenerationHandler.d.ts +8 -0
- package/dist/core/modules/GenerationHandler.js +28 -38
- package/dist/core/modules/TelemetryHandler.d.ts +3 -9
- package/dist/core/modules/TelemetryHandler.js +17 -12
- package/dist/lib/analytics/pricing.d.ts +4 -1
- package/dist/lib/analytics/pricing.js +6 -2
- package/dist/lib/core/analytics.js +8 -2
- package/dist/lib/core/baseProvider.js +9 -7
- package/dist/lib/core/modules/GenerationHandler.d.ts +8 -0
- package/dist/lib/core/modules/GenerationHandler.js +28 -38
- package/dist/lib/core/modules/TelemetryHandler.d.ts +3 -9
- package/dist/lib/core/modules/TelemetryHandler.js +17 -12
- package/dist/lib/neurolink.js +18 -1
- package/dist/lib/providers/amazonBedrock.js +68 -6
- package/dist/lib/providers/anthropic.js +50 -16
- package/dist/lib/providers/googleAiStudio.js +29 -4
- package/dist/lib/providers/googleNativeGemini3.js +10 -0
- package/dist/lib/providers/googleVertex.js +150 -25
- package/dist/lib/providers/litellm.js +13 -2
- package/dist/lib/providers/openAI.js +13 -2
- package/dist/lib/providers/openaiChatCompletionsBase.js +45 -10
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +2 -14
- package/dist/lib/providers/openaiChatCompletionsClient.js +20 -1
- package/dist/lib/providers/sagemaker/language-model.js +5 -3
- package/dist/lib/providers/sagemaker/parsers.js +18 -9
- package/dist/lib/providers/sagemaker/streaming.d.ts +9 -0
- package/dist/lib/providers/sagemaker/streaming.js +64 -6
- package/dist/lib/proxy/accountCooldown.js +2 -7
- package/dist/lib/proxy/proxyAnalysis.js +197 -0
- package/dist/lib/proxy/requestLogger.js +11 -0
- package/dist/lib/proxy/routingEvidence.d.ts +6 -0
- package/dist/lib/proxy/routingEvidence.js +33 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +257 -87
- package/dist/lib/types/common.d.ts +3 -0
- package/dist/lib/types/openaiCompatible.d.ts +8 -7
- package/dist/lib/types/providers.d.ts +6 -0
- package/dist/lib/types/proxy.d.ts +97 -2
- package/dist/lib/utils/pricing.js +15 -3
- package/dist/lib/utils/tokenUtils.js +38 -3
- package/dist/neurolink.js +18 -1
- package/dist/providers/amazonBedrock.js +68 -6
- package/dist/providers/anthropic.js +50 -16
- package/dist/providers/googleAiStudio.js +29 -4
- package/dist/providers/googleNativeGemini3.js +10 -0
- package/dist/providers/googleVertex.js +150 -25
- package/dist/providers/litellm.js +13 -2
- package/dist/providers/openAI.js +13 -2
- package/dist/providers/openaiChatCompletionsBase.js +45 -10
- package/dist/providers/openaiChatCompletionsClient.d.ts +2 -14
- package/dist/providers/openaiChatCompletionsClient.js +20 -1
- package/dist/providers/sagemaker/language-model.js +5 -3
- package/dist/providers/sagemaker/parsers.js +18 -9
- package/dist/providers/sagemaker/streaming.d.ts +9 -0
- package/dist/providers/sagemaker/streaming.js +64 -6
- package/dist/proxy/accountCooldown.js +2 -7
- package/dist/proxy/proxyAnalysis.js +197 -0
- package/dist/proxy/requestLogger.js +11 -0
- package/dist/proxy/routingEvidence.d.ts +6 -0
- package/dist/proxy/routingEvidence.js +32 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +257 -87
- package/dist/types/common.d.ts +3 -0
- package/dist/types/openaiCompatible.d.ts +8 -7
- package/dist/types/providers.d.ts +6 -0
- package/dist/types/proxy.d.ts +97 -2
- package/dist/utils/pricing.js +15 -3
- package/dist/utils/tokenUtils.js +38 -3
- 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;
|