@juspay/neurolink 10.4.2 → 10.4.4
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/auth/tokenStore.d.ts +7 -0
- package/dist/auth/tokenStore.js +54 -0
- package/dist/browser/neurolink.min.js +377 -380
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +290 -63
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/lib/auth/tokenStore.d.ts +7 -0
- package/dist/lib/auth/tokenStore.js +54 -0
- package/dist/lib/proxy/proxyAnalysis.js +136 -12
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
- package/dist/lib/proxy/rawStreamCapture.js +47 -1
- package/dist/lib/proxy/requestLogger.d.ts +2 -0
- package/dist/lib/proxy/requestLogger.js +72 -13
- package/dist/lib/proxy/rollingProxyServer.js +79 -8
- package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
- package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
- package/dist/lib/proxy/runtimeConfig.js +20 -5
- package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
- package/dist/lib/proxy/sseInterceptor.js +47 -1
- package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
- package/dist/lib/proxy/tokenRefresh.js +70 -15
- package/dist/lib/proxy/usageStats.d.ts +25 -3
- package/dist/lib/proxy/usageStats.js +546 -55
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/lib/types/proxy.d.ts +90 -1
- package/dist/proxy/proxyAnalysis.js +136 -12
- package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/proxy/proxyTranslationEngine.js +56 -12
- package/dist/proxy/rawStreamCapture.js +47 -1
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +72 -13
- package/dist/proxy/rollingProxyServer.js +79 -8
- package/dist/proxy/rollingWorkerProcess.js +20 -15
- package/dist/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/proxy/runtimeConfig.d.ts +1 -0
- package/dist/proxy/runtimeConfig.js +20 -5
- package/dist/proxy/socketWorkerRuntime.js +41 -13
- package/dist/proxy/sseInterceptor.js +47 -1
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +70 -15
- package/dist/proxy/usageStats.d.ts +25 -3
- package/dist/proxy/usageStats.js +546 -55
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/types/proxy.d.ts +90 -1
- package/package.json +1 -1
|
@@ -671,6 +671,29 @@ export type AnthropicUpstreamFetchResult = {
|
|
|
671
671
|
sawNetworkError: boolean;
|
|
672
672
|
upstreamSpan?: Span;
|
|
673
673
|
};
|
|
674
|
+
export type ProxyTerminalErrorCategory = "authentication" | "client_cancelled" | "fallback_exhausted" | "invalid_request" | "proxy_error" | "rate_limit" | "stream_error" | "unclassified" | "upstream_error" | "other";
|
|
675
|
+
/** Compact, redacted terminal failure retained independently of body logs. */
|
|
676
|
+
export type ProxyTerminalErrorSummary = {
|
|
677
|
+
id: string;
|
|
678
|
+
at: number;
|
|
679
|
+
status: number;
|
|
680
|
+
category: ProxyTerminalErrorCategory;
|
|
681
|
+
requestId?: string;
|
|
682
|
+
account?: string;
|
|
683
|
+
accountType?: string;
|
|
684
|
+
errorType?: string;
|
|
685
|
+
errorCode?: string;
|
|
686
|
+
terminalOutcome?: string;
|
|
687
|
+
message?: string;
|
|
688
|
+
};
|
|
689
|
+
/** Optional terminal context supplied when a final request error is recorded. */
|
|
690
|
+
export type ProxyTerminalErrorDetails = {
|
|
691
|
+
requestId?: string;
|
|
692
|
+
errorType?: string;
|
|
693
|
+
errorCode?: string;
|
|
694
|
+
terminalOutcome?: string;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
674
697
|
export type AccountStats = {
|
|
675
698
|
label: string;
|
|
676
699
|
type: string;
|
|
@@ -700,6 +723,19 @@ export type ProxyStats = {
|
|
|
700
723
|
totalQuotaRateLimits: number;
|
|
701
724
|
accounts: Record<string, AccountStats>;
|
|
702
725
|
};
|
|
726
|
+
/** Bounded terminal-error state stored separately from counters and body logs. */
|
|
727
|
+
export type ProxyTerminalErrorJournal = {
|
|
728
|
+
startedAt: number;
|
|
729
|
+
totalErrors: number;
|
|
730
|
+
counts: Record<ProxyTerminalErrorCategory, number>;
|
|
731
|
+
recent: ProxyTerminalErrorSummary[];
|
|
732
|
+
};
|
|
733
|
+
export type ProxyUsageStatsSnapshot = {
|
|
734
|
+
stats: ProxyStats;
|
|
735
|
+
statsVersion: number;
|
|
736
|
+
terminalErrors: ProxyTerminalErrorJournal;
|
|
737
|
+
terminalErrorsVersion: number;
|
|
738
|
+
};
|
|
703
739
|
/** Durability and reconciliation state for the proxy usage counters. */
|
|
704
740
|
export type ProxyStatsPersistenceStatus = {
|
|
705
741
|
enabled: boolean;
|
|
@@ -712,6 +748,14 @@ export type ProxyStatsPersistenceStatus = {
|
|
|
712
748
|
lastReconciledAt: number | null;
|
|
713
749
|
lastRecoveryAt: number | null;
|
|
714
750
|
lastError: string | null;
|
|
751
|
+
terminalErrorsFilePath?: string | null;
|
|
752
|
+
terminalErrorsRevision?: number;
|
|
753
|
+
terminalErrorsPending?: number;
|
|
754
|
+
terminalErrorsInFlight?: number;
|
|
755
|
+
terminalErrorsUnpersisted?: number;
|
|
756
|
+
terminalErrorsLastFlushedAt?: number | null;
|
|
757
|
+
terminalErrorsLastRecoveryAt?: number | null;
|
|
758
|
+
terminalErrorsLastError?: string | null;
|
|
715
759
|
};
|
|
716
760
|
/** Versioned on-disk snapshot shared by overlapping proxy workers. */
|
|
717
761
|
export type PersistedProxyStatsSnapshot = {
|
|
@@ -720,6 +764,13 @@ export type PersistedProxyStatsSnapshot = {
|
|
|
720
764
|
updatedAt: number;
|
|
721
765
|
stats: ProxyStats;
|
|
722
766
|
};
|
|
767
|
+
/** Versioned terminal-error snapshot isolated from rolling-worker counter writes. */
|
|
768
|
+
export type PersistedProxyTerminalErrorSnapshot = {
|
|
769
|
+
schemaVersion: 1;
|
|
770
|
+
revision: number;
|
|
771
|
+
updatedAt: number;
|
|
772
|
+
journal: ProxyTerminalErrorJournal;
|
|
773
|
+
};
|
|
723
774
|
/** Ownership metadata for the proxy statistics cross-process lock. */
|
|
724
775
|
export type ProxyStatsLockOwner = {
|
|
725
776
|
token: string;
|
|
@@ -729,6 +780,7 @@ export type ProxyStatsLockOwner = {
|
|
|
729
780
|
/** Construction options for an isolated proxy statistics store. */
|
|
730
781
|
export type ProxyUsageStatsStoreOptions = {
|
|
731
782
|
filePath?: string;
|
|
783
|
+
terminalErrorsFilePath?: string;
|
|
732
784
|
flushIntervalMs?: number;
|
|
733
785
|
lockTimeoutMs?: number;
|
|
734
786
|
staleLockMs?: number;
|
|
@@ -1121,6 +1173,7 @@ export type ProxyAnalysisAccount = {
|
|
|
1121
1173
|
unclassifiedRateLimits: number;
|
|
1122
1174
|
};
|
|
1123
1175
|
/** Offline report generated from proxy request, attempt, and lifecycle logs. */
|
|
1176
|
+
export type ProxyAnalysisStreamName = "lifecycle" | "requests" | "attempts" | "debug";
|
|
1124
1177
|
export type ProxyAnalysisReport = {
|
|
1125
1178
|
generatedAt: string;
|
|
1126
1179
|
since: string;
|
|
@@ -1129,6 +1182,7 @@ export type ProxyAnalysisReport = {
|
|
|
1129
1182
|
lifecycle: number;
|
|
1130
1183
|
requests: number;
|
|
1131
1184
|
attempts: number;
|
|
1185
|
+
debug: number;
|
|
1132
1186
|
};
|
|
1133
1187
|
coverage: {
|
|
1134
1188
|
lifecycle: boolean;
|
|
@@ -1143,6 +1197,20 @@ export type ProxyAnalysisReport = {
|
|
|
1143
1197
|
unsupportedLifecycleLines: number;
|
|
1144
1198
|
lifecycleSequenceGaps: number;
|
|
1145
1199
|
lifecycleSequenceDuplicates: number;
|
|
1200
|
+
streams: Record<ProxyAnalysisStreamName, {
|
|
1201
|
+
observedFrom: string | null;
|
|
1202
|
+
observedTo: string | null;
|
|
1203
|
+
startsAtOrBeforeRequestedWindow: boolean;
|
|
1204
|
+
}>;
|
|
1205
|
+
bodyArtifacts: {
|
|
1206
|
+
capturesIndexed: number;
|
|
1207
|
+
artifactsReferenced: number;
|
|
1208
|
+
artifactsPresent: number;
|
|
1209
|
+
artifactsMissing: number;
|
|
1210
|
+
invalidPaths: number;
|
|
1211
|
+
writeFailures: number;
|
|
1212
|
+
truncatedCaptures: number;
|
|
1213
|
+
};
|
|
1146
1214
|
};
|
|
1147
1215
|
lifecycle: {
|
|
1148
1216
|
accepted: number;
|
|
@@ -1392,6 +1460,7 @@ export type StoredBodyArtifact = {
|
|
|
1392
1460
|
storedFileBytes?: number;
|
|
1393
1461
|
redactedBody?: string;
|
|
1394
1462
|
bodyTruncated?: boolean;
|
|
1463
|
+
bodyWriteFailed?: boolean;
|
|
1395
1464
|
};
|
|
1396
1465
|
/** File the proxy logger tracks for rotation and cleanup. */
|
|
1397
1466
|
export type ManagedLogFile = {
|
|
@@ -1653,6 +1722,11 @@ export type ProxyWorkerStatusMessage = {
|
|
|
1653
1722
|
generation: number;
|
|
1654
1723
|
pid: number;
|
|
1655
1724
|
socketId: string;
|
|
1725
|
+
} | {
|
|
1726
|
+
type: "proxy-worker:replacement-requested";
|
|
1727
|
+
generation: number;
|
|
1728
|
+
pid: number;
|
|
1729
|
+
reason: "environment";
|
|
1656
1730
|
};
|
|
1657
1731
|
export type ProxyWorkerSocketMessage = {
|
|
1658
1732
|
type: "proxy-worker:socket";
|
|
@@ -1661,6 +1735,8 @@ export type ProxyWorkerSocketMessage = {
|
|
|
1661
1735
|
};
|
|
1662
1736
|
export type ProxyWorkerIpcMessage = ProxyWorkerControlMessage | ProxyWorkerStatusMessage | ProxyWorkerSocketMessage;
|
|
1663
1737
|
export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy" | "end" | "pause" | "resume" | "once">;
|
|
1738
|
+
/** A transferred socket whose temporary handoff listeners can be detached. */
|
|
1739
|
+
export type DetachableTransferableProxySocket = TransferableProxySocket & Pick<import("node:net").Socket, "off">;
|
|
1664
1740
|
export type RollingWorkerHandle = {
|
|
1665
1741
|
pid: number;
|
|
1666
1742
|
sendControl: (message: ProxyWorkerControlMessage) => void;
|
|
@@ -1714,6 +1790,11 @@ export type RollingWorkerSupervisorOptions = {
|
|
|
1714
1790
|
socketQueueTimeoutMs?: number;
|
|
1715
1791
|
shutdownTimeoutMs?: number;
|
|
1716
1792
|
onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
|
|
1793
|
+
onReplacementRequested?: (request: {
|
|
1794
|
+
generation: number;
|
|
1795
|
+
pid: number;
|
|
1796
|
+
reason: "environment";
|
|
1797
|
+
}) => void;
|
|
1717
1798
|
log?: (message: string) => void;
|
|
1718
1799
|
};
|
|
1719
1800
|
export type RollingProxyServerOptions = {
|
|
@@ -1854,6 +1935,7 @@ export type ProxyRuntimeConfigReloadResult = {
|
|
|
1854
1935
|
applied: boolean;
|
|
1855
1936
|
changed: boolean;
|
|
1856
1937
|
generation: number;
|
|
1938
|
+
environmentChanged?: boolean;
|
|
1857
1939
|
error?: string;
|
|
1858
1940
|
};
|
|
1859
1941
|
/** Safe runtime configuration diagnostics exposed through proxy status. */
|
|
@@ -1921,6 +2003,11 @@ export type StatusStats = {
|
|
|
1921
2003
|
totalRateLimits: number;
|
|
1922
2004
|
totalTransientRateLimits?: number;
|
|
1923
2005
|
totalQuotaRateLimits?: number;
|
|
2006
|
+
terminalErrors?: ProxyTerminalErrorJournal;
|
|
2007
|
+
lastTerminalError?: ProxyTerminalErrorSummary | null;
|
|
2008
|
+
terminalErrorDetailsComparable?: boolean;
|
|
2009
|
+
terminalErrorDetailsMissing?: number;
|
|
2010
|
+
terminalErrorDetailsExcess?: number;
|
|
1924
2011
|
accounts?: {
|
|
1925
2012
|
label: string;
|
|
1926
2013
|
type: string;
|
|
@@ -1933,7 +2020,9 @@ export type StatusStats = {
|
|
|
1933
2020
|
transientRateLimits?: number;
|
|
1934
2021
|
quotaRateLimits?: number;
|
|
1935
2022
|
cooling: boolean;
|
|
1936
|
-
|
|
2023
|
+
allowed?: boolean;
|
|
2024
|
+
expired?: boolean;
|
|
2025
|
+
status?: "active" | "cooling" | "disabled" | "expired" | "excluded" | "removed" | "internal" | "unattributed";
|
|
1937
2026
|
}[];
|
|
1938
2027
|
persistence?: ProxyStatsPersistenceStatus;
|
|
1939
2028
|
};
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
7
7
|
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
8
8
|
const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
9
|
+
const DEBUG_FILE_PATTERN = /^proxy-debug-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
10
|
+
const ARTIFACT_STAT_CONCURRENCY = 64;
|
|
9
11
|
const LIFECYCLE_EVENTS = new Set([
|
|
10
12
|
"request_accepted",
|
|
11
13
|
"response_headers",
|
|
12
14
|
"response_first_chunk",
|
|
13
15
|
"request_terminal",
|
|
14
16
|
]);
|
|
17
|
+
function isContainedPath(root, candidate) {
|
|
18
|
+
const candidateRelative = relative(root, candidate);
|
|
19
|
+
return (candidateRelative.length > 0 &&
|
|
20
|
+
!isAbsolute(candidateRelative) &&
|
|
21
|
+
candidateRelative !== ".." &&
|
|
22
|
+
!candidateRelative.startsWith(`..${sep}`));
|
|
23
|
+
}
|
|
24
|
+
async function inspectBodyArtifact(artifactPath, canonicalBodiesRoot) {
|
|
25
|
+
if (!canonicalBodiesRoot) {
|
|
26
|
+
return "missing";
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const canonicalArtifactPath = await realpath(artifactPath);
|
|
30
|
+
if (!isContainedPath(canonicalBodiesRoot, canonicalArtifactPath)) {
|
|
31
|
+
return "invalid";
|
|
32
|
+
}
|
|
33
|
+
return (await stat(canonicalArtifactPath)).isFile() ? "present" : "missing";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "missing";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
15
39
|
function increment(counter, key) {
|
|
16
40
|
counter[key] = (counter[key] ?? 0) + 1;
|
|
17
41
|
}
|
|
@@ -192,6 +216,7 @@ async function discoverLogFiles(logsDir) {
|
|
|
192
216
|
lifecycleFiles: matching(LIFECYCLE_FILE_PATTERN),
|
|
193
217
|
requestFiles: matching(REQUEST_FILE_PATTERN),
|
|
194
218
|
attemptFiles: matching(ATTEMPT_FILE_PATTERN),
|
|
219
|
+
debugFiles: matching(DEBUG_FILE_PATTERN),
|
|
195
220
|
};
|
|
196
221
|
}
|
|
197
222
|
/** Analyze local proxy logs without reading request or response body artifacts. */
|
|
@@ -199,7 +224,24 @@ export async function analyzeProxyLogs(options) {
|
|
|
199
224
|
const nowMs = options?.nowMs ?? Date.now();
|
|
200
225
|
const sinceMs = parseSince(options?.since ?? "24h", nowMs);
|
|
201
226
|
const logsDir = resolve(options?.logsDir ?? join(homedir(), ".neurolink", "logs"));
|
|
202
|
-
const { lifecycleFiles, requestFiles, attemptFiles } = await discoverLogFiles(logsDir);
|
|
227
|
+
const { lifecycleFiles, requestFiles, attemptFiles, debugFiles } = await discoverLogFiles(logsDir);
|
|
228
|
+
const observedRanges = {
|
|
229
|
+
lifecycle: { from: null, to: null },
|
|
230
|
+
requests: { from: null, to: null },
|
|
231
|
+
attempts: { from: null, to: null },
|
|
232
|
+
debug: { from: null, to: null },
|
|
233
|
+
};
|
|
234
|
+
const observeTimestamp = (stream, record) => {
|
|
235
|
+
const timestamp = Date.parse(String(record.timestamp ?? ""));
|
|
236
|
+
if (!Number.isFinite(timestamp)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const range = observedRanges[stream];
|
|
240
|
+
range.from =
|
|
241
|
+
range.from === null ? timestamp : Math.min(range.from, timestamp);
|
|
242
|
+
range.to = range.to === null ? timestamp : Math.max(range.to, timestamp);
|
|
243
|
+
return timestamp;
|
|
244
|
+
};
|
|
203
245
|
let linesRead = 0;
|
|
204
246
|
let malformedLines = 0;
|
|
205
247
|
let unsupportedLifecycleLines = 0;
|
|
@@ -216,8 +258,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
216
258
|
const sequences = new Map();
|
|
217
259
|
for (const filePath of lifecycleFiles) {
|
|
218
260
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
219
|
-
const timestamp =
|
|
220
|
-
if (
|
|
261
|
+
const timestamp = observeTimestamp("lifecycle", record);
|
|
262
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
221
263
|
return;
|
|
222
264
|
}
|
|
223
265
|
const event = stringValue(record.event);
|
|
@@ -298,8 +340,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
298
340
|
let unclassifiedRateLimits = 0;
|
|
299
341
|
for (const filePath of attemptFiles) {
|
|
300
342
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
301
|
-
const timestamp =
|
|
302
|
-
if (
|
|
343
|
+
const timestamp = observeTimestamp("attempts", record);
|
|
344
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
303
345
|
return;
|
|
304
346
|
}
|
|
305
347
|
const requestId = stringValue(record.requestId);
|
|
@@ -360,8 +402,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
360
402
|
const terminalStreamErrors = new Set();
|
|
361
403
|
for (const filePath of requestFiles) {
|
|
362
404
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
363
|
-
const timestamp =
|
|
364
|
-
if (
|
|
405
|
+
const timestamp = observeTimestamp("requests", record);
|
|
406
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
365
407
|
return;
|
|
366
408
|
}
|
|
367
409
|
const requestId = stringValue(record.requestId);
|
|
@@ -391,6 +433,70 @@ export async function analyzeProxyLogs(options) {
|
|
|
391
433
|
malformedLines += 1;
|
|
392
434
|
});
|
|
393
435
|
}
|
|
436
|
+
let capturesIndexed = 0;
|
|
437
|
+
let truncatedCaptures = 0;
|
|
438
|
+
let writeFailures = 0;
|
|
439
|
+
let invalidPaths = 0;
|
|
440
|
+
const referencedArtifacts = new Set();
|
|
441
|
+
const bodiesRoot = resolve(logsDir, "bodies");
|
|
442
|
+
for (const filePath of debugFiles) {
|
|
443
|
+
linesRead += await readJsonLines(filePath, (record) => {
|
|
444
|
+
const timestamp = observeTimestamp("debug", record);
|
|
445
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (record.type !== "body_capture") {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
capturesIndexed += 1;
|
|
452
|
+
truncatedCaptures += record.bodyTruncated === true ? 1 : 0;
|
|
453
|
+
writeFailures += record.bodyWriteFailed === true ? 1 : 0;
|
|
454
|
+
const bodyPath = stringValue(record.bodyPath);
|
|
455
|
+
if (!bodyPath) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (bodyPath.includes("\0") || bodyPath.split(/[\\/]/).includes("..")) {
|
|
459
|
+
invalidPaths += 1;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const resolvedBodyPath = resolve(logsDir, bodyPath);
|
|
463
|
+
if (!isContainedPath(bodiesRoot, resolvedBodyPath)) {
|
|
464
|
+
invalidPaths += 1;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
referencedArtifacts.add(resolvedBodyPath);
|
|
468
|
+
}, () => {
|
|
469
|
+
malformedLines += 1;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const artifactPaths = [...referencedArtifacts];
|
|
473
|
+
let canonicalBodiesRoot = null;
|
|
474
|
+
let bodiesRootUnsafe = false;
|
|
475
|
+
try {
|
|
476
|
+
const bodiesRootStat = await lstat(bodiesRoot);
|
|
477
|
+
if (bodiesRootStat.isDirectory() && !bodiesRootStat.isSymbolicLink()) {
|
|
478
|
+
canonicalBodiesRoot = await realpath(bodiesRoot);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
bodiesRootUnsafe = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// A missing body directory means every lexically valid reference is absent.
|
|
486
|
+
}
|
|
487
|
+
let artifactsPresent = 0;
|
|
488
|
+
let artifactsMissing = 0;
|
|
489
|
+
for (let offset = 0; offset < artifactPaths.length; offset += ARTIFACT_STAT_CONCURRENCY) {
|
|
490
|
+
const presence = await Promise.all(artifactPaths
|
|
491
|
+
.slice(offset, offset + ARTIFACT_STAT_CONCURRENCY)
|
|
492
|
+
.map((artifactPath) => bodiesRootUnsafe
|
|
493
|
+
? Promise.resolve("invalid")
|
|
494
|
+
: inspectBodyArtifact(artifactPath, canonicalBodiesRoot)));
|
|
495
|
+
artifactsPresent += presence.filter((value) => value === "present").length;
|
|
496
|
+
artifactsMissing += presence.filter((value) => value === "missing").length;
|
|
497
|
+
invalidPaths += presence.filter((value) => value === "invalid").length;
|
|
498
|
+
}
|
|
499
|
+
const artifactsReferenced = artifactsPresent + artifactsMissing;
|
|
394
500
|
const finalSummary = summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts);
|
|
395
501
|
return {
|
|
396
502
|
generatedAt: new Date(nowMs).toISOString(),
|
|
@@ -400,11 +506,12 @@ export async function analyzeProxyLogs(options) {
|
|
|
400
506
|
lifecycle: lifecycleFiles.length,
|
|
401
507
|
requests: requestFiles.length,
|
|
402
508
|
attempts: attemptFiles.length,
|
|
509
|
+
debug: debugFiles.length,
|
|
403
510
|
},
|
|
404
511
|
coverage: {
|
|
405
|
-
lifecycle:
|
|
406
|
-
finalRequests:
|
|
407
|
-
attempts:
|
|
512
|
+
lifecycle: accepted.size + headers.size + firstChunks.size + terminal.size > 0,
|
|
513
|
+
finalRequests: finalRequests.size > 0 || terminalStreamErrors.size > 0,
|
|
514
|
+
attempts: totalAttempts > 0,
|
|
408
515
|
attemptLatency: attemptLatency.length > 0,
|
|
409
516
|
cacheUsage: finalSummary.cache.requestsWithUsage > 0,
|
|
410
517
|
},
|
|
@@ -414,6 +521,23 @@ export async function analyzeProxyLogs(options) {
|
|
|
414
521
|
unsupportedLifecycleLines,
|
|
415
522
|
lifecycleSequenceGaps,
|
|
416
523
|
lifecycleSequenceDuplicates,
|
|
524
|
+
streams: Object.fromEntries(Object.entries(observedRanges).map(([stream, range]) => [
|
|
525
|
+
stream,
|
|
526
|
+
{
|
|
527
|
+
observedFrom: range.from === null ? null : new Date(range.from).toISOString(),
|
|
528
|
+
observedTo: range.to === null ? null : new Date(range.to).toISOString(),
|
|
529
|
+
startsAtOrBeforeRequestedWindow: range.from !== null && range.from <= sinceMs,
|
|
530
|
+
},
|
|
531
|
+
])),
|
|
532
|
+
bodyArtifacts: {
|
|
533
|
+
capturesIndexed,
|
|
534
|
+
artifactsReferenced,
|
|
535
|
+
artifactsPresent,
|
|
536
|
+
artifactsMissing,
|
|
537
|
+
invalidPaths,
|
|
538
|
+
writeFailures,
|
|
539
|
+
truncatedCaptures,
|
|
540
|
+
},
|
|
417
541
|
},
|
|
418
542
|
lifecycle: {
|
|
419
543
|
accepted: accepted.size,
|
|
@@ -77,6 +77,7 @@ export declare function handleTranslatedJsonRequest(args: {
|
|
|
77
77
|
attempts: ProxyTranslationAttempt[];
|
|
78
78
|
tracer?: ProxyTracer;
|
|
79
79
|
requestStartTime: number;
|
|
80
|
+
terminalFailureStatus?: number;
|
|
80
81
|
}): Promise<unknown>;
|
|
81
82
|
/**
|
|
82
83
|
* Build the /v1/models response in OpenAI list format.
|
|
@@ -247,6 +247,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
247
247
|
let keepAliveTimer;
|
|
248
248
|
let cancelled = false;
|
|
249
249
|
let succeeded = false;
|
|
250
|
+
let streamInterruptedAfterOutput = false;
|
|
250
251
|
let translatedModel;
|
|
251
252
|
let finalStreamError = "No translation providers succeeded";
|
|
252
253
|
let upstreamIterator;
|
|
@@ -267,6 +268,9 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
267
268
|
}, KEEPALIVE_INTERVAL_MS);
|
|
268
269
|
try {
|
|
269
270
|
for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex++) {
|
|
271
|
+
if (cancelled) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
270
274
|
const attempt = attempts[attemptIndex];
|
|
271
275
|
lastAttemptLabel = attempt.label ?? "translation";
|
|
272
276
|
logger.always(`[proxy:${format}] attempt ${attemptIndex + 1}/${attempts.length}: ${attempt.label}`);
|
|
@@ -295,10 +299,14 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
}
|
|
302
|
+
if (cancelled) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
298
305
|
const toolCalls = streamResult.toolCalls ?? [];
|
|
299
306
|
if (!hasTranslatedOutput(collectedText, toolCalls)) {
|
|
300
307
|
finalStreamError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
301
308
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
309
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
302
310
|
continue;
|
|
303
311
|
}
|
|
304
312
|
if (!cancelled && toolCalls.length) {
|
|
@@ -331,7 +339,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
331
339
|
tracer?.recordMetrics();
|
|
332
340
|
translatedModel = streamResult.model;
|
|
333
341
|
succeeded = true;
|
|
334
|
-
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
catch (streamErr) {
|
|
@@ -343,6 +350,8 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
343
350
|
? streamErr.message
|
|
344
351
|
: String(streamErr);
|
|
345
352
|
if (collectedText.trim().length > 0) {
|
|
353
|
+
streamInterruptedAfterOutput = true;
|
|
354
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
346
355
|
logger.always(`${tag} mid-stream error: ${finalStreamError}`);
|
|
347
356
|
for (const frame of serializer.emitError(`Upstream stream interrupted: ${finalStreamError}`)) {
|
|
348
357
|
controller.enqueue(encoder.encode(frame));
|
|
@@ -354,7 +363,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
354
363
|
}
|
|
355
364
|
}
|
|
356
365
|
// All attempts exhausted
|
|
357
|
-
recordFinalError(500, lastAttemptLabel, "translation");
|
|
358
366
|
if (!cancelled) {
|
|
359
367
|
logger.always(`${tag} all translation attempts failed: ${finalStreamError}`);
|
|
360
368
|
for (const frame of serializer.emitError(finalStreamError)) {
|
|
@@ -372,12 +380,38 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
372
380
|
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
373
381
|
tracer.setModelSubstitution(requestModel, translatedModel);
|
|
374
382
|
}
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
const terminalStatus = cancelled
|
|
384
|
+
? 499
|
|
385
|
+
: succeeded
|
|
386
|
+
? 200
|
|
387
|
+
: streamInterruptedAfterOutput
|
|
388
|
+
? 502
|
|
389
|
+
: 500;
|
|
390
|
+
const terminalErrorType = cancelled
|
|
391
|
+
? "client_cancelled"
|
|
392
|
+
: streamInterruptedAfterOutput
|
|
393
|
+
? "stream_error"
|
|
394
|
+
: succeeded
|
|
395
|
+
? undefined
|
|
396
|
+
: "generation_error";
|
|
397
|
+
const terminalErrorMessage = cancelled
|
|
398
|
+
? "Client cancelled the streaming response"
|
|
399
|
+
: succeeded
|
|
400
|
+
? undefined
|
|
401
|
+
: finalStreamError;
|
|
402
|
+
if (terminalErrorType && terminalErrorMessage) {
|
|
403
|
+
tracer?.setError(terminalErrorType, terminalErrorMessage.slice(0, 500));
|
|
404
|
+
recordFinalError(terminalStatus, lastAttemptLabel, "translation", {
|
|
405
|
+
requestId: ctx.requestId,
|
|
406
|
+
errorType: terminalErrorType,
|
|
407
|
+
terminalOutcome: terminalErrorType,
|
|
408
|
+
message: terminalErrorMessage,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
377
413
|
}
|
|
378
|
-
|
|
379
|
-
// responseStatus below (success path is 200, exhausted-attempts path is 500).
|
|
380
|
-
tracer?.end(succeeded ? 200 : 500, Date.now() - requestStartTime);
|
|
414
|
+
tracer?.end(terminalStatus, Date.now() - requestStartTime);
|
|
381
415
|
const traceCtx = tracer?.getTraceContext();
|
|
382
416
|
logRequest({
|
|
383
417
|
timestamp: new Date().toISOString(),
|
|
@@ -389,8 +423,12 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
389
423
|
toolCount: Object.keys(parsed.tools).length,
|
|
390
424
|
account: "translation",
|
|
391
425
|
accountType: "translation",
|
|
392
|
-
responseStatus:
|
|
426
|
+
responseStatus: terminalStatus,
|
|
393
427
|
responseTimeMs: Date.now() - requestStartTime,
|
|
428
|
+
...(terminalErrorType ? { errorType: terminalErrorType } : {}),
|
|
429
|
+
...(terminalErrorMessage
|
|
430
|
+
? { errorMessage: terminalErrorMessage.slice(0, 500) }
|
|
431
|
+
: {}),
|
|
394
432
|
...(traceCtx?.traceId ? { traceId: traceCtx.traceId } : {}),
|
|
395
433
|
...(traceCtx?.spanId ? { spanId: traceCtx.spanId } : {}),
|
|
396
434
|
});
|
|
@@ -424,7 +462,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
424
462
|
* Handles a translated non-streaming request for either Claude or OpenAI format.
|
|
425
463
|
*/
|
|
426
464
|
export async function handleTranslatedJsonRequest(args) {
|
|
427
|
-
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, } = args;
|
|
465
|
+
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, terminalFailureStatus = 500, } = args;
|
|
428
466
|
const tag = logTag(format);
|
|
429
467
|
let lastAttemptError = "No translation providers succeeded";
|
|
430
468
|
let lastAttemptLabel = "translation";
|
|
@@ -448,6 +486,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
448
486
|
if (!hasTranslatedOutput(collectedText, streamResult.toolCalls)) {
|
|
449
487
|
lastAttemptError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
450
488
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
489
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
451
490
|
continue;
|
|
452
491
|
}
|
|
453
492
|
const internal = {
|
|
@@ -505,9 +544,14 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
505
544
|
recordAttemptError(lastAttemptLabel, "translation", 500);
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
|
-
recordFinalError(
|
|
547
|
+
recordFinalError(terminalFailureStatus, lastAttemptLabel, "translation", {
|
|
548
|
+
requestId: ctx.requestId,
|
|
549
|
+
errorType: "generation_error",
|
|
550
|
+
terminalOutcome: "handler_error",
|
|
551
|
+
message: lastAttemptError,
|
|
552
|
+
});
|
|
509
553
|
tracer?.setError("generation_error", lastAttemptError.slice(0, 500));
|
|
510
|
-
tracer?.end(
|
|
554
|
+
tracer?.end(terminalFailureStatus, Date.now() - requestStartTime);
|
|
511
555
|
const traceCtx = tracer?.getTraceContext();
|
|
512
556
|
logRequest({
|
|
513
557
|
timestamp: new Date().toISOString(),
|
|
@@ -519,7 +563,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
519
563
|
toolCount: Object.keys(parsed.tools).length,
|
|
520
564
|
account: "translation",
|
|
521
565
|
accountType: "translation",
|
|
522
|
-
responseStatus:
|
|
566
|
+
responseStatus: terminalFailureStatus,
|
|
523
567
|
responseTimeMs: Date.now() - requestStartTime,
|
|
524
568
|
errorType: "generation_error",
|
|
525
569
|
errorMessage: lastAttemptError.slice(0, 500),
|
|
@@ -60,7 +60,11 @@ export function createRawStreamCapture() {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
const innerWriter = transform.writable.getWriter();
|
|
63
|
+
let writableController;
|
|
63
64
|
const writable = new WritableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
writableController = controller;
|
|
67
|
+
},
|
|
64
68
|
write(chunk) {
|
|
65
69
|
return innerWriter.write(chunk);
|
|
66
70
|
},
|
|
@@ -72,9 +76,51 @@ export function createRawStreamCapture() {
|
|
|
72
76
|
return innerWriter.abort(reason);
|
|
73
77
|
},
|
|
74
78
|
});
|
|
79
|
+
// A downstream reader cancellation errors the TransformStream's writable
|
|
80
|
+
// side, but this wrapper otherwise hides that state from the upstream pipe.
|
|
81
|
+
// Mirror it onto the wrapper so cancellation reaches the source reader.
|
|
82
|
+
void innerWriter.closed.catch((reason) => {
|
|
83
|
+
settle();
|
|
84
|
+
try {
|
|
85
|
+
writableController.error(reason);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The wrapper may already be closing or aborted.
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const innerReader = transform.readable.getReader();
|
|
92
|
+
const readable = new ReadableStream({
|
|
93
|
+
async pull(controller) {
|
|
94
|
+
try {
|
|
95
|
+
const { done, value } = await innerReader.read();
|
|
96
|
+
if (done) {
|
|
97
|
+
controller.close();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
controller.enqueue(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
controller.error(error);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async cancel(reason) {
|
|
108
|
+
settle();
|
|
109
|
+
try {
|
|
110
|
+
writableController.error(reason);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// The wrapper may already be closing or aborted.
|
|
114
|
+
}
|
|
115
|
+
await Promise.allSettled([
|
|
116
|
+
innerReader.cancel(reason),
|
|
117
|
+
innerWriter.abort(reason),
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
75
121
|
return {
|
|
76
122
|
stream: {
|
|
77
|
-
readable
|
|
123
|
+
readable,
|
|
78
124
|
writable,
|
|
79
125
|
},
|
|
80
126
|
capture,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Useful for debugging and auditing proxy traffic.
|
|
7
7
|
*/
|
|
8
8
|
import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry } from "../types/index.js";
|
|
9
|
+
/** Wait, up to a bounded deadline, for admitted request/body writes to settle. */
|
|
10
|
+
export declare function flushRequestLogs(timeoutMs?: number): Promise<void>;
|
|
9
11
|
export declare function initRequestLogger(enabled?: boolean, customLogsDir?: string): void;
|
|
10
12
|
export declare function logRequest(entry: RequestLogEntry): Promise<void>;
|
|
11
13
|
/**
|