@juspay/neurolink 9.92.3 → 9.93.1
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 +5 -3
- package/dist/cli/commands/proxy.js +360 -69
- 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/lib/proxy/proxyActivity.d.ts +2 -2
- package/dist/lib/proxy/proxyActivity.js +49 -9
- 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/proxyLifecycle.d.ts +8 -0
- package/dist/lib/proxy/proxyLifecycle.js +333 -0
- package/dist/lib/proxy/requestLogger.js +6 -0
- package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
- package/dist/lib/proxy/sseInterceptor.js +6 -5
- 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/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +137 -35
- package/dist/lib/types/cli.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +269 -0
- package/dist/proxy/proxyActivity.d.ts +2 -2
- package/dist/proxy/proxyActivity.js +49 -9
- 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/proxyLifecycle.d.ts +8 -0
- package/dist/proxy/proxyLifecycle.js +332 -0
- package/dist/proxy/requestLogger.js +6 -0
- package/dist/proxy/sseInterceptor.d.ts +9 -1
- package/dist/proxy/sseInterceptor.js +6 -5
- 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/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +137 -35
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/proxy.d.ts +269 -0
- package/package.json +4 -1
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
const PROXY_RESPONSE_CANCEL_TIMEOUT_MS = 1_000;
|
|
1
4
|
let activeRequests = 0;
|
|
2
5
|
let lastActivityAtMs = null;
|
|
3
6
|
function touchActivity() {
|
|
4
7
|
lastActivityAtMs = Date.now();
|
|
5
8
|
}
|
|
9
|
+
function safelyNotifyObserver(callback) {
|
|
10
|
+
try {
|
|
11
|
+
callback?.();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
// Observability must never alter response handling.
|
|
15
|
+
if (logger.shouldLog("debug")) {
|
|
16
|
+
logger.debug("[proxy] response lifecycle observer failed", {
|
|
17
|
+
error: error instanceof Error ? error.message : String(error),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
/** Track one client-facing proxy request until its response body settles. */
|
|
7
23
|
export function beginProxyRequest() {
|
|
8
24
|
activeRequests += 1;
|
|
@@ -33,35 +49,59 @@ export function isProxyActivityQuiet(snapshot, quietThresholdMs, nowMs = Date.no
|
|
|
33
49
|
return nowMs - snapshot.lastActivityAt.getTime() >= quietThresholdMs;
|
|
34
50
|
}
|
|
35
51
|
/** Keep activity open until the response body completes, errors, or is cancelled. */
|
|
36
|
-
export function trackProxyResponse(response, finishRequest) {
|
|
52
|
+
export function trackProxyResponse(response, finishRequest, observer) {
|
|
37
53
|
if (!response.body) {
|
|
38
54
|
finishRequest();
|
|
55
|
+
safelyNotifyObserver(() => observer?.onTerminal?.({
|
|
56
|
+
outcome: "bodyless",
|
|
57
|
+
observedBodyBytes: 0,
|
|
58
|
+
responseChunks: 0,
|
|
59
|
+
}));
|
|
39
60
|
return response;
|
|
40
61
|
}
|
|
41
62
|
const reader = response.body.getReader();
|
|
63
|
+
let observedBodyBytes = 0;
|
|
64
|
+
let responseChunks = 0;
|
|
65
|
+
let settled = false;
|
|
66
|
+
const settle = (outcome) => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
finishRequest();
|
|
72
|
+
safelyNotifyObserver(() => observer?.onTerminal?.({
|
|
73
|
+
outcome,
|
|
74
|
+
observedBodyBytes,
|
|
75
|
+
responseChunks,
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
42
78
|
const trackedBody = new ReadableStream({
|
|
43
79
|
async pull(controller) {
|
|
44
80
|
try {
|
|
45
81
|
const { value, done } = await reader.read();
|
|
46
82
|
if (done) {
|
|
47
|
-
|
|
83
|
+
settle("completed");
|
|
48
84
|
controller.close();
|
|
49
85
|
return;
|
|
50
86
|
}
|
|
51
87
|
controller.enqueue(value);
|
|
88
|
+
observedBodyBytes += value.byteLength;
|
|
89
|
+
responseChunks += 1;
|
|
90
|
+
if (responseChunks === 1) {
|
|
91
|
+
safelyNotifyObserver(() => observer?.onFirstChunk?.({
|
|
92
|
+
observedBodyBytes,
|
|
93
|
+
responseChunks: 1,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
52
96
|
}
|
|
53
97
|
catch (error) {
|
|
54
|
-
|
|
98
|
+
settle("stream_error");
|
|
55
99
|
controller.error(error);
|
|
56
100
|
}
|
|
57
101
|
},
|
|
58
102
|
async cancel(reason) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
finally {
|
|
63
|
-
finishRequest();
|
|
64
|
-
}
|
|
103
|
+
settle("client_cancelled");
|
|
104
|
+
await withTimeout(reader.cancel(reason), PROXY_RESPONSE_CANCEL_TIMEOUT_MS, "Timed out cancelling the upstream proxy response");
|
|
65
105
|
},
|
|
66
106
|
});
|
|
67
107
|
return new Response(trackedBody, {
|
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
}
|
|
@@ -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,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProxyLifecycleEventInput, ProxyLifecycleLoggerOptions, ProxyLifecycleLoggerSnapshot } from "../types/index.js";
|
|
2
|
+
export declare function hashProxyLifecycleSessionId(sessionId: string | undefined): string | undefined;
|
|
3
|
+
export declare function configureProxyLifecycleLogger(options: ProxyLifecycleLoggerOptions): void;
|
|
4
|
+
/** Enqueue fixed-size lifecycle metadata without awaiting filesystem work. */
|
|
5
|
+
export declare function logProxyLifecycleEvent(input: ProxyLifecycleEventInput): void;
|
|
6
|
+
export declare function flushProxyLifecycleEvents(): Promise<void>;
|
|
7
|
+
export declare function getProxyLifecycleLoggerSnapshot(): ProxyLifecycleLoggerSnapshot;
|
|
8
|
+
export declare function resetProxyLifecycleLoggerForTests(): void;
|