@juspay/neurolink 9.93.0 → 9.93.2
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/browser/neurolink.min.js +379 -382
- package/dist/cli/commands/proxy.d.ts +3 -2
- package/dist/cli/commands/proxy.js +224 -66
- package/dist/cli/commands/proxyAnalyze.d.ts +3 -0
- package/dist/cli/commands/proxyAnalyze.js +147 -0
- package/dist/cli/parser.js +3 -1
- package/dist/cli/utils/serverUtils.js +4 -1
- package/dist/lib/proxy/globalInstaller.js +1 -1
- package/dist/lib/proxy/openaiFormat.js +1 -0
- package/dist/lib/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/lib/proxy/proxyAnalysis.js +454 -0
- package/dist/lib/proxy/proxyHealth.d.ts +4 -0
- package/dist/lib/proxy/proxyHealth.js +21 -0
- package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
- package/dist/lib/proxy/sseInterceptor.js +7 -6
- package/dist/lib/proxy/streamOutcome.d.ts +3 -1
- package/dist/lib/proxy/streamOutcome.js +77 -0
- package/dist/lib/proxy/updateCoordinator.d.ts +7 -0
- package/dist/lib/proxy/updateCoordinator.js +60 -0
- package/dist/lib/proxy/updateState.d.ts +4 -0
- package/dist/lib/proxy/updateState.js +51 -0
- package/dist/lib/proxy/workerLog.d.ts +6 -0
- package/dist/lib/proxy/workerLog.js +42 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +114 -17
- package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/lib/types/cli.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +187 -0
- package/dist/proxy/globalInstaller.js +1 -1
- package/dist/proxy/openaiFormat.js +1 -0
- package/dist/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/proxy/proxyAnalysis.js +453 -0
- package/dist/proxy/proxyHealth.d.ts +4 -0
- package/dist/proxy/proxyHealth.js +21 -0
- package/dist/proxy/sseInterceptor.d.ts +9 -1
- package/dist/proxy/sseInterceptor.js +7 -6
- package/dist/proxy/streamOutcome.d.ts +3 -1
- package/dist/proxy/streamOutcome.js +77 -0
- package/dist/proxy/updateCoordinator.d.ts +7 -0
- package/dist/proxy/updateCoordinator.js +59 -0
- package/dist/proxy/updateState.d.ts +4 -0
- package/dist/proxy/updateState.js +51 -0
- package/dist/proxy/workerLog.d.ts +6 -0
- package/dist/proxy/workerLog.js +41 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +114 -17
- package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/proxy.d.ts +187 -0
- package/package.json +4 -1
package/dist/cli/parser.js
CHANGED
|
@@ -14,6 +14,7 @@ import { ragCommand } from "./commands/rag.js";
|
|
|
14
14
|
import { ObservabilityCommandFactory } from "./commands/observability.js";
|
|
15
15
|
import { TelemetryCommandFactory } from "./commands/telemetry.js";
|
|
16
16
|
import { proxyStartCommand, proxyStatusCommand, proxyTelemetryCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
|
|
17
|
+
import { proxyAnalyzeCommand } from "./commands/proxyAnalyze.js";
|
|
17
18
|
import { EvaluateCommandFactory } from "./commands/evaluate.js";
|
|
18
19
|
import { TaskCommandFactory } from "./commands/task.js";
|
|
19
20
|
import { AutoresearchCommandFactory } from "./commands/autoresearch.js";
|
|
@@ -200,12 +201,13 @@ export function initializeCliParser() {
|
|
|
200
201
|
builder: (yargs) => yargs
|
|
201
202
|
.command(proxyStartCommand)
|
|
202
203
|
.command(proxyStatusCommand)
|
|
204
|
+
.command(proxyAnalyzeCommand)
|
|
203
205
|
.command(proxyTelemetryCommand)
|
|
204
206
|
.command(proxySetupCommand)
|
|
205
207
|
.command(proxyGuardCommand)
|
|
206
208
|
.command(proxyInstallCommand)
|
|
207
209
|
.command(proxyUninstallCommand)
|
|
208
|
-
.demandCommand(1, "Please specify a proxy subcommand: start, status, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
210
|
+
.demandCommand(1, "Please specify a proxy subcommand: start, status, analyze, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
209
211
|
handler: () => { },
|
|
210
212
|
})
|
|
211
213
|
// Evaluate Command Group - Using EvaluateCommandFactory
|
|
@@ -112,7 +112,10 @@ export class StateFileManager {
|
|
|
112
112
|
if (!fs.existsSync(dir)) {
|
|
113
113
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
114
114
|
}
|
|
115
|
-
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2)
|
|
115
|
+
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), {
|
|
116
|
+
mode: 0o600,
|
|
117
|
+
});
|
|
118
|
+
fs.chmodSync(this.filePath, 0o600);
|
|
116
119
|
}
|
|
117
120
|
/**
|
|
118
121
|
* Load state from the state file
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
7
|
+
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
8
|
+
const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
9
|
+
const LIFECYCLE_EVENTS = new Set([
|
|
10
|
+
"request_accepted",
|
|
11
|
+
"response_headers",
|
|
12
|
+
"response_first_chunk",
|
|
13
|
+
"request_terminal",
|
|
14
|
+
]);
|
|
15
|
+
function increment(counter, key) {
|
|
16
|
+
counter[key] = (counter[key] ?? 0) + 1;
|
|
17
|
+
}
|
|
18
|
+
function finiteNumber(value) {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
20
|
+
}
|
|
21
|
+
function stringValue(value) {
|
|
22
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
23
|
+
}
|
|
24
|
+
function percentile(sorted, fraction) {
|
|
25
|
+
if (sorted.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const index = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
|
|
29
|
+
return Number(sorted[index].toFixed(3));
|
|
30
|
+
}
|
|
31
|
+
function summarizeLatency(values) {
|
|
32
|
+
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
33
|
+
return {
|
|
34
|
+
count: sorted.length,
|
|
35
|
+
p50: percentile(sorted, 0.5),
|
|
36
|
+
p95: percentile(sorted, 0.95),
|
|
37
|
+
p99: percentile(sorted, 0.99),
|
|
38
|
+
max: sorted.length ? Number(sorted[sorted.length - 1].toFixed(3)) : null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseSince(value, nowMs) {
|
|
42
|
+
const relative = /^(\d+(?:\.\d+)?)(m|h|d|w)$/i.exec(value.trim());
|
|
43
|
+
if (relative) {
|
|
44
|
+
const amount = Number(relative[1]);
|
|
45
|
+
const unitMs = {
|
|
46
|
+
m: 60_000,
|
|
47
|
+
h: 3_600_000,
|
|
48
|
+
d: 86_400_000,
|
|
49
|
+
w: 604_800_000,
|
|
50
|
+
};
|
|
51
|
+
return nowMs - amount * unitMs[relative[2].toLowerCase()];
|
|
52
|
+
}
|
|
53
|
+
const parsed = Date.parse(value);
|
|
54
|
+
if (!Number.isFinite(parsed)) {
|
|
55
|
+
throw new Error(`Invalid --since value "${value}". Use an ISO timestamp or a duration such as 6h, 1d, or 1w.`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
async function readJsonLines(filePath, onRecord, onMalformed) {
|
|
60
|
+
let linesRead = 0;
|
|
61
|
+
const lines = createInterface({
|
|
62
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
63
|
+
crlfDelay: Infinity,
|
|
64
|
+
});
|
|
65
|
+
for await (const line of lines) {
|
|
66
|
+
if (!line.trim()) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
linesRead += 1;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(line);
|
|
72
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
73
|
+
onMalformed();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
onRecord(parsed);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
onMalformed();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return linesRead;
|
|
83
|
+
}
|
|
84
|
+
function accountEntry(accounts, account, accountType) {
|
|
85
|
+
const key = `${accountType}:${account}`;
|
|
86
|
+
const existing = accounts.get(key);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
const created = {
|
|
91
|
+
account,
|
|
92
|
+
accountType,
|
|
93
|
+
attempts: 0,
|
|
94
|
+
attemptErrors: 0,
|
|
95
|
+
finalRequests: 0,
|
|
96
|
+
finalErrors: 0,
|
|
97
|
+
transientRateLimits: 0,
|
|
98
|
+
quotaRateLimits: 0,
|
|
99
|
+
unclassifiedRateLimits: 0,
|
|
100
|
+
};
|
|
101
|
+
accounts.set(key, created);
|
|
102
|
+
return created;
|
|
103
|
+
}
|
|
104
|
+
function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts) {
|
|
105
|
+
let success = 0;
|
|
106
|
+
let errors = 0;
|
|
107
|
+
let finalRateLimits = 0;
|
|
108
|
+
let recoveredAfterRetry = 0;
|
|
109
|
+
let requestsWithUsage = 0;
|
|
110
|
+
let requestsWithCacheRead = 0;
|
|
111
|
+
let cacheReadTokens = 0;
|
|
112
|
+
let cacheCreationTokens = 0;
|
|
113
|
+
let inputTokens = 0;
|
|
114
|
+
const finalRequestLatency = [];
|
|
115
|
+
const singleAttemptDelta = [];
|
|
116
|
+
const errorTypes = {};
|
|
117
|
+
const errorCodes = {};
|
|
118
|
+
for (const [requestId, request] of finalRequests) {
|
|
119
|
+
const failed = request.status >= 400 || terminalStreamErrors.has(requestId);
|
|
120
|
+
if (failed) {
|
|
121
|
+
errors += 1;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
success += 1;
|
|
125
|
+
}
|
|
126
|
+
finalRateLimits += request.status === 429 ? 1 : 0;
|
|
127
|
+
const accountStats = accountEntry(accounts, request.account, request.accountType);
|
|
128
|
+
accountStats.finalRequests += 1;
|
|
129
|
+
accountStats.finalErrors += failed ? 1 : 0;
|
|
130
|
+
if (failed) {
|
|
131
|
+
increment(errorTypes, terminalStreamErrors.has(requestId)
|
|
132
|
+
? "stream_error"
|
|
133
|
+
: (request.errorType ?? `http_${request.status}`));
|
|
134
|
+
if (request.errorCode) {
|
|
135
|
+
increment(errorCodes, request.errorCode);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (request.durationMs !== null && request.durationMs >= 0) {
|
|
139
|
+
finalRequestLatency.push(request.durationMs);
|
|
140
|
+
}
|
|
141
|
+
const requestAttempts = attemptsByRequest.get(requestId);
|
|
142
|
+
recoveredAfterRetry += !failed && requestAttempts?.hadError ? 1 : 0;
|
|
143
|
+
if (requestAttempts?.count === 1 &&
|
|
144
|
+
requestAttempts.durationCount === 1 &&
|
|
145
|
+
request.durationMs !== null &&
|
|
146
|
+
Number.isFinite(requestAttempts.totalDurationMs)) {
|
|
147
|
+
singleAttemptDelta.push(request.durationMs - requestAttempts.totalDurationMs);
|
|
148
|
+
}
|
|
149
|
+
if (request.inputTokens !== null ||
|
|
150
|
+
request.cacheReadTokens !== null ||
|
|
151
|
+
request.cacheCreationTokens !== null) {
|
|
152
|
+
requestsWithUsage += 1;
|
|
153
|
+
inputTokens += request.inputTokens ?? 0;
|
|
154
|
+
cacheReadTokens += request.cacheReadTokens ?? 0;
|
|
155
|
+
cacheCreationTokens += request.cacheCreationTokens ?? 0;
|
|
156
|
+
requestsWithCacheRead += (request.cacheReadTokens ?? 0) > 0 ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
requestTotals: {
|
|
161
|
+
completed: finalRequests.size,
|
|
162
|
+
success,
|
|
163
|
+
errors,
|
|
164
|
+
finalRateLimits,
|
|
165
|
+
recoveredAfterRetry,
|
|
166
|
+
errorTypes,
|
|
167
|
+
errorCodes,
|
|
168
|
+
},
|
|
169
|
+
cache: {
|
|
170
|
+
requestsWithUsage,
|
|
171
|
+
requestsWithCacheRead,
|
|
172
|
+
cacheReadTokens,
|
|
173
|
+
cacheCreationTokens,
|
|
174
|
+
inputTokens,
|
|
175
|
+
requestHitRate: requestsWithUsage > 0
|
|
176
|
+
? Number((requestsWithCacheRead / requestsWithUsage).toFixed(4))
|
|
177
|
+
: null,
|
|
178
|
+
},
|
|
179
|
+
finalRequestLatency,
|
|
180
|
+
singleAttemptDelta,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function discoverLogFiles(logsDir) {
|
|
184
|
+
const entries = await readdir(logsDir, { withFileTypes: true }).catch((error) => {
|
|
185
|
+
throw new Error(`Unable to read proxy logs at ${logsDir}: ${error.message}`);
|
|
186
|
+
});
|
|
187
|
+
const matching = (pattern) => entries
|
|
188
|
+
.filter((entry) => entry.isFile() && pattern.test(entry.name))
|
|
189
|
+
.map((entry) => join(logsDir, entry.name))
|
|
190
|
+
.sort();
|
|
191
|
+
return {
|
|
192
|
+
lifecycleFiles: matching(LIFECYCLE_FILE_PATTERN),
|
|
193
|
+
requestFiles: matching(REQUEST_FILE_PATTERN),
|
|
194
|
+
attemptFiles: matching(ATTEMPT_FILE_PATTERN),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Analyze local proxy logs without reading request or response body artifacts. */
|
|
198
|
+
export async function analyzeProxyLogs(options) {
|
|
199
|
+
const nowMs = options?.nowMs ?? Date.now();
|
|
200
|
+
const sinceMs = parseSince(options?.since ?? "24h", nowMs);
|
|
201
|
+
const logsDir = resolve(options?.logsDir ?? join(homedir(), ".neurolink", "logs"));
|
|
202
|
+
const { lifecycleFiles, requestFiles, attemptFiles } = await discoverLogFiles(logsDir);
|
|
203
|
+
let linesRead = 0;
|
|
204
|
+
let malformedLines = 0;
|
|
205
|
+
let unsupportedLifecycleLines = 0;
|
|
206
|
+
const accepted = new Set();
|
|
207
|
+
const headers = new Set();
|
|
208
|
+
const firstChunks = new Set();
|
|
209
|
+
const terminal = new Set();
|
|
210
|
+
const terminalOutcomes = {};
|
|
211
|
+
const lifecycleErrorTypes = {};
|
|
212
|
+
const lifecycleErrorCodes = {};
|
|
213
|
+
const headersLatency = [];
|
|
214
|
+
const firstChunkLatency = [];
|
|
215
|
+
const terminalLatency = [];
|
|
216
|
+
const sequences = new Map();
|
|
217
|
+
for (const filePath of lifecycleFiles) {
|
|
218
|
+
linesRead += await readJsonLines(filePath, (record) => {
|
|
219
|
+
const timestamp = Date.parse(String(record.timestamp ?? ""));
|
|
220
|
+
if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const event = stringValue(record.event);
|
|
224
|
+
const requestId = stringValue(record.requestId);
|
|
225
|
+
if (record.schemaVersion !== 1 ||
|
|
226
|
+
!event ||
|
|
227
|
+
!LIFECYCLE_EVENTS.has(event) ||
|
|
228
|
+
!requestId) {
|
|
229
|
+
unsupportedLifecycleLines += 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const processId = stringValue(record.processInstanceId);
|
|
233
|
+
const sequence = finiteNumber(record.sequence);
|
|
234
|
+
if (processId && sequence !== null && Number.isInteger(sequence)) {
|
|
235
|
+
const values = sequences.get(processId) ?? [];
|
|
236
|
+
values.push(sequence);
|
|
237
|
+
sequences.set(processId, values);
|
|
238
|
+
}
|
|
239
|
+
const elapsed = finiteNumber(record.elapsedMs);
|
|
240
|
+
if (event === "request_accepted") {
|
|
241
|
+
accepted.add(requestId);
|
|
242
|
+
}
|
|
243
|
+
else if (event === "response_headers") {
|
|
244
|
+
headers.add(requestId);
|
|
245
|
+
if (elapsed !== null && elapsed >= 0) {
|
|
246
|
+
headersLatency.push(elapsed);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (event === "response_first_chunk") {
|
|
250
|
+
firstChunks.add(requestId);
|
|
251
|
+
if (elapsed !== null && elapsed >= 0) {
|
|
252
|
+
firstChunkLatency.push(elapsed);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
terminal.add(requestId);
|
|
257
|
+
if (elapsed !== null && elapsed >= 0) {
|
|
258
|
+
terminalLatency.push(elapsed);
|
|
259
|
+
}
|
|
260
|
+
increment(terminalOutcomes, stringValue(record.terminalOutcome) ?? "unknown");
|
|
261
|
+
const errorType = stringValue(record.errorType);
|
|
262
|
+
const errorCode = stringValue(record.errorCode);
|
|
263
|
+
if (errorType) {
|
|
264
|
+
increment(lifecycleErrorTypes, errorType);
|
|
265
|
+
}
|
|
266
|
+
if (errorCode) {
|
|
267
|
+
increment(lifecycleErrorCodes, errorCode);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}, () => {
|
|
271
|
+
malformedLines += 1;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
let lifecycleSequenceGaps = 0;
|
|
275
|
+
let lifecycleSequenceDuplicates = 0;
|
|
276
|
+
for (const values of sequences.values()) {
|
|
277
|
+
values.sort((a, b) => a - b);
|
|
278
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
279
|
+
const difference = values[index] - values[index - 1];
|
|
280
|
+
if (difference === 0) {
|
|
281
|
+
lifecycleSequenceDuplicates += 1;
|
|
282
|
+
}
|
|
283
|
+
else if (difference > 1) {
|
|
284
|
+
lifecycleSequenceGaps += difference - 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const accounts = new Map();
|
|
289
|
+
const attemptsByRequest = new Map();
|
|
290
|
+
const attemptLatency = [];
|
|
291
|
+
let totalAttempts = 0;
|
|
292
|
+
let totalAttemptErrors = 0;
|
|
293
|
+
const attemptErrorTypes = {};
|
|
294
|
+
const attemptErrorCodes = {};
|
|
295
|
+
let attemptRateLimits = 0;
|
|
296
|
+
let transientRateLimits = 0;
|
|
297
|
+
let quotaRateLimits = 0;
|
|
298
|
+
let unclassifiedRateLimits = 0;
|
|
299
|
+
for (const filePath of attemptFiles) {
|
|
300
|
+
linesRead += await readJsonLines(filePath, (record) => {
|
|
301
|
+
const timestamp = Date.parse(String(record.timestamp ?? ""));
|
|
302
|
+
if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const requestId = stringValue(record.requestId);
|
|
306
|
+
const status = finiteNumber(record.responseStatus);
|
|
307
|
+
const duration = finiteNumber(record.attemptDurationMs);
|
|
308
|
+
if (!requestId || status === null) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const account = stringValue(record.account) ?? "unknown";
|
|
312
|
+
const accountType = stringValue(record.accountType) ?? "unknown";
|
|
313
|
+
const accountStats = accountEntry(accounts, account, accountType);
|
|
314
|
+
totalAttempts += 1;
|
|
315
|
+
accountStats.attempts += 1;
|
|
316
|
+
const hadError = status >= 400 || !!stringValue(record.errorType);
|
|
317
|
+
if (hadError) {
|
|
318
|
+
totalAttemptErrors += 1;
|
|
319
|
+
accountStats.attemptErrors += 1;
|
|
320
|
+
increment(attemptErrorTypes, stringValue(record.errorType) ?? `http_${status}`);
|
|
321
|
+
const errorCode = stringValue(record.errorCode);
|
|
322
|
+
if (errorCode) {
|
|
323
|
+
increment(attemptErrorCodes, errorCode);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const requestAttempts = attemptsByRequest.get(requestId) ?? {
|
|
327
|
+
count: 0,
|
|
328
|
+
hadError: false,
|
|
329
|
+
totalDurationMs: 0,
|
|
330
|
+
durationCount: 0,
|
|
331
|
+
};
|
|
332
|
+
requestAttempts.count += 1;
|
|
333
|
+
requestAttempts.hadError = requestAttempts.hadError || hadError;
|
|
334
|
+
if (duration !== null && duration >= 0) {
|
|
335
|
+
requestAttempts.totalDurationMs += duration;
|
|
336
|
+
requestAttempts.durationCount += 1;
|
|
337
|
+
attemptLatency.push(duration);
|
|
338
|
+
}
|
|
339
|
+
attemptsByRequest.set(requestId, requestAttempts);
|
|
340
|
+
if (status === 429) {
|
|
341
|
+
attemptRateLimits += 1;
|
|
342
|
+
if (record.rateLimitKind === "transient") {
|
|
343
|
+
transientRateLimits += 1;
|
|
344
|
+
accountStats.transientRateLimits += 1;
|
|
345
|
+
}
|
|
346
|
+
else if (record.rateLimitKind === "quota") {
|
|
347
|
+
quotaRateLimits += 1;
|
|
348
|
+
accountStats.quotaRateLimits += 1;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
unclassifiedRateLimits += 1;
|
|
352
|
+
accountStats.unclassifiedRateLimits += 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}, () => {
|
|
356
|
+
malformedLines += 1;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const finalRequests = new Map();
|
|
360
|
+
const terminalStreamErrors = new Set();
|
|
361
|
+
for (const filePath of requestFiles) {
|
|
362
|
+
linesRead += await readJsonLines(filePath, (record) => {
|
|
363
|
+
const timestamp = Date.parse(String(record.timestamp ?? ""));
|
|
364
|
+
if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const requestId = stringValue(record.requestId);
|
|
368
|
+
if (!requestId) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (finiteNumber(record.terminalStatus) !== null) {
|
|
372
|
+
terminalStreamErrors.add(requestId);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const status = finiteNumber(record.responseStatus);
|
|
376
|
+
if (status === null || !stringValue(record.method)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
finalRequests.set(requestId, {
|
|
380
|
+
status,
|
|
381
|
+
durationMs: finiteNumber(record.responseTimeMs),
|
|
382
|
+
account: stringValue(record.account) ?? "unknown",
|
|
383
|
+
accountType: stringValue(record.accountType) ?? "unknown",
|
|
384
|
+
inputTokens: finiteNumber(record.inputTokens),
|
|
385
|
+
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
386
|
+
cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
|
|
387
|
+
errorType: stringValue(record.errorType),
|
|
388
|
+
errorCode: stringValue(record.errorCode),
|
|
389
|
+
});
|
|
390
|
+
}, () => {
|
|
391
|
+
malformedLines += 1;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const finalSummary = summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts);
|
|
395
|
+
return {
|
|
396
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
397
|
+
since: new Date(sinceMs).toISOString(),
|
|
398
|
+
logsDir,
|
|
399
|
+
files: {
|
|
400
|
+
lifecycle: lifecycleFiles.length,
|
|
401
|
+
requests: requestFiles.length,
|
|
402
|
+
attempts: attemptFiles.length,
|
|
403
|
+
},
|
|
404
|
+
coverage: {
|
|
405
|
+
lifecycle: lifecycleFiles.length > 0,
|
|
406
|
+
finalRequests: requestFiles.length > 0,
|
|
407
|
+
attempts: attemptFiles.length > 0,
|
|
408
|
+
attemptLatency: attemptLatency.length > 0,
|
|
409
|
+
cacheUsage: finalSummary.cache.requestsWithUsage > 0,
|
|
410
|
+
},
|
|
411
|
+
dataQuality: {
|
|
412
|
+
linesRead,
|
|
413
|
+
malformedLines,
|
|
414
|
+
unsupportedLifecycleLines,
|
|
415
|
+
lifecycleSequenceGaps,
|
|
416
|
+
lifecycleSequenceDuplicates,
|
|
417
|
+
},
|
|
418
|
+
lifecycle: {
|
|
419
|
+
accepted: accepted.size,
|
|
420
|
+
headers: headers.size,
|
|
421
|
+
firstChunks: firstChunks.size,
|
|
422
|
+
terminal: terminal.size,
|
|
423
|
+
unsettled: [...accepted].filter((requestId) => !terminal.has(requestId))
|
|
424
|
+
.length,
|
|
425
|
+
terminalOutcomes,
|
|
426
|
+
errorTypes: lifecycleErrorTypes,
|
|
427
|
+
errorCodes: lifecycleErrorCodes,
|
|
428
|
+
},
|
|
429
|
+
requests: finalSummary.requestTotals,
|
|
430
|
+
attempts: {
|
|
431
|
+
total: totalAttempts,
|
|
432
|
+
errors: totalAttemptErrors,
|
|
433
|
+
errorTypes: attemptErrorTypes,
|
|
434
|
+
errorCodes: attemptErrorCodes,
|
|
435
|
+
},
|
|
436
|
+
rateLimits: {
|
|
437
|
+
attemptRateLimits,
|
|
438
|
+
transient: transientRateLimits,
|
|
439
|
+
quota: quotaRateLimits,
|
|
440
|
+
unclassified: unclassifiedRateLimits,
|
|
441
|
+
},
|
|
442
|
+
latencyMs: {
|
|
443
|
+
headers: summarizeLatency(headersLatency),
|
|
444
|
+
firstChunk: summarizeLatency(firstChunkLatency),
|
|
445
|
+
terminal: summarizeLatency(terminalLatency),
|
|
446
|
+
finalRequest: summarizeLatency(finalSummary.finalRequestLatency),
|
|
447
|
+
attempt: summarizeLatency(attemptLatency),
|
|
448
|
+
singleAttemptDelta: summarizeLatency(finalSummary.singleAttemptDelta),
|
|
449
|
+
},
|
|
450
|
+
cache: finalSummary.cache,
|
|
451
|
+
accounts: [...accounts.values()].sort((left, right) => right.attempts - left.attempts),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=proxyAnalysis.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ProxyHealthResponse, ProxyReadinessState } from "../types/index.js";
|
|
2
2
|
export declare function createProxyReadinessState(startTimeMs?: number): ProxyReadinessState;
|
|
3
3
|
export declare function markProxyReady(state: ProxyReadinessState, readyAtMs?: number): void;
|
|
4
|
+
/** Stop admitting new inference requests while existing responses drain. */
|
|
5
|
+
export declare function markProxyDrainingForUpdate(state: ProxyReadinessState): boolean;
|
|
6
|
+
/** Reopen inference admission after a deferred or failed update attempt. */
|
|
7
|
+
export declare function resumeProxyConnections(state: ProxyReadinessState): boolean;
|
|
4
8
|
export declare function buildProxyHealthResponse(state: ProxyReadinessState, options: {
|
|
5
9
|
strategy: string;
|
|
6
10
|
passthrough: boolean;
|
|
@@ -3,19 +3,40 @@ export function createProxyReadinessState(startTimeMs = Date.now()) {
|
|
|
3
3
|
startTimeMs,
|
|
4
4
|
acceptingConnections: false,
|
|
5
5
|
ready: false,
|
|
6
|
+
drainingForUpdate: false,
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
export function markProxyReady(state, readyAtMs = Date.now()) {
|
|
9
10
|
state.acceptingConnections = true;
|
|
10
11
|
state.ready = true;
|
|
12
|
+
state.drainingForUpdate = false;
|
|
11
13
|
state.readyAtMs = readyAtMs;
|
|
12
14
|
}
|
|
15
|
+
/** Stop admitting new inference requests while existing responses drain. */
|
|
16
|
+
export function markProxyDrainingForUpdate(state) {
|
|
17
|
+
if (!state.ready) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
state.acceptingConnections = false;
|
|
21
|
+
state.drainingForUpdate = true;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
/** Reopen inference admission after a deferred or failed update attempt. */
|
|
25
|
+
export function resumeProxyConnections(state) {
|
|
26
|
+
if (!state.ready) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
state.acceptingConnections = true;
|
|
30
|
+
state.drainingForUpdate = false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
13
33
|
export function buildProxyHealthResponse(state, options) {
|
|
14
34
|
const now = options.now ?? Date.now();
|
|
15
35
|
return {
|
|
16
36
|
status: state.ready ? "ok" : "starting",
|
|
17
37
|
ready: state.ready,
|
|
18
38
|
acceptingConnections: state.acceptingConnections,
|
|
39
|
+
drainingForUpdate: state.drainingForUpdate,
|
|
19
40
|
strategy: options.strategy,
|
|
20
41
|
passthrough: options.passthrough,
|
|
21
42
|
version: options.version,
|
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
* upstreamResponse.body.pipeThrough(stream).pipeTo(clientWritable);
|
|
16
16
|
* const data = await telemetry; // resolves on stream end
|
|
17
17
|
*/
|
|
18
|
-
import type { SSEInterceptorOptions, SSEInterceptorResult } from "../types/index.js";
|
|
18
|
+
import type { SSEInterceptorOptions, SSEInterceptorResult, ParsedSSEBuffer } from "../types/index.js";
|
|
19
|
+
/**
|
|
20
|
+
* Incrementally parse SSE events from a growing buffer of text.
|
|
21
|
+
*
|
|
22
|
+
* SSE events are separated by a blank line (`\n\n`). Each event consists of
|
|
23
|
+
* field lines (`event: ...`, `data: ...`). We consume complete events and
|
|
24
|
+
* return them, leaving any trailing partial event in the buffer.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractSSEEvents(buffer: string): ParsedSSEBuffer;
|
|
19
27
|
/**
|
|
20
28
|
* Create an SSE interceptor that extracts telemetry from an Anthropic
|
|
21
29
|
* streaming response while passing all bytes through unmodified.
|
|
@@ -35,22 +35,23 @@ const TRUNCATION_MARKER = "...[TRUNCATED]";
|
|
|
35
35
|
* field lines (`event: ...`, `data: ...`). We consume complete events and
|
|
36
36
|
* return them, leaving any trailing partial event in the buffer.
|
|
37
37
|
*/
|
|
38
|
-
function extractSSEEvents(buffer) {
|
|
38
|
+
export function extractSSEEvents(buffer) {
|
|
39
39
|
const events = [];
|
|
40
40
|
// Split on double-newline boundaries. The last segment may be an
|
|
41
41
|
// incomplete event if the chunk was split mid-event.
|
|
42
42
|
let cursor = 0;
|
|
43
43
|
while (cursor < buffer.length) {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
44
|
+
const boundaryMatch = /\r\n\r\n|\n\n|\r\r/.exec(buffer.slice(cursor));
|
|
45
|
+
if (!boundaryMatch || boundaryMatch.index === undefined) {
|
|
46
46
|
// No more complete events — everything from cursor onward is partial.
|
|
47
47
|
break;
|
|
48
48
|
}
|
|
49
|
+
const boundary = cursor + boundaryMatch.index;
|
|
49
50
|
const rawBlock = buffer.slice(cursor, boundary);
|
|
50
|
-
cursor = boundary +
|
|
51
|
+
cursor = boundary + boundaryMatch[0].length;
|
|
51
52
|
let eventType = "";
|
|
52
53
|
let dataValue = "";
|
|
53
|
-
const lines = rawBlock.split(
|
|
54
|
+
const lines = rawBlock.split(/\r\n|\n|\r/);
|
|
54
55
|
for (const line of lines) {
|
|
55
56
|
if (line.startsWith("event: ")) {
|
|
56
57
|
eventType = line.slice(7).trim();
|
|
@@ -336,7 +337,7 @@ function processEvent(acc, event) {
|
|
|
336
337
|
const message = nestedError?.message ?? payload.message;
|
|
337
338
|
acc.streamErrorMessage =
|
|
338
339
|
typeof message === "string" && message.trim()
|
|
339
|
-
? message.trim()
|
|
340
|
+
? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
|
|
340
341
|
: truncateString(event.data, MAX_EVENT_DATA_BYTES);
|
|
341
342
|
}
|
|
342
343
|
switch (event.event) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { StreamTerminalOutcome, StreamTerminalOutcomeTracker } from "../types/index.js";
|
|
1
|
+
import type { AnthropicStreamPreflightResult, StreamTerminalOutcome, StreamTerminalOutcomeTracker } from "../types/index.js";
|
|
2
|
+
/** Hold only pre-commit SSE frames so immediate upstream errors remain retryable. */
|
|
3
|
+
export declare function preflightAnthropicStream(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<AnthropicStreamPreflightResult>;
|
|
2
4
|
export declare function createStreamTerminalOutcomeTracker(): StreamTerminalOutcomeTracker;
|
|
3
5
|
export declare function mergeStreamTerminalOutcome(outcome: StreamTerminalOutcome, sseErrorMessage?: string): StreamTerminalOutcome;
|