@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/auth/tokenStore.d.ts +7 -0
  3. package/dist/auth/tokenStore.js +54 -0
  4. package/dist/browser/neurolink.min.js +377 -380
  5. package/dist/cli/commands/proxy.d.ts +2 -1
  6. package/dist/cli/commands/proxy.js +290 -63
  7. package/dist/cli/commands/proxyAnalyze.js +10 -1
  8. package/dist/lib/auth/tokenStore.d.ts +7 -0
  9. package/dist/lib/auth/tokenStore.js +54 -0
  10. package/dist/lib/proxy/proxyAnalysis.js +136 -12
  11. package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
  12. package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
  13. package/dist/lib/proxy/rawStreamCapture.js +47 -1
  14. package/dist/lib/proxy/requestLogger.d.ts +2 -0
  15. package/dist/lib/proxy/requestLogger.js +72 -13
  16. package/dist/lib/proxy/rollingProxyServer.js +79 -8
  17. package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
  18. package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
  19. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  20. package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
  21. package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
  22. package/dist/lib/proxy/runtimeConfig.js +20 -5
  23. package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
  24. package/dist/lib/proxy/sseInterceptor.js +47 -1
  25. package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
  26. package/dist/lib/proxy/tokenRefresh.js +70 -15
  27. package/dist/lib/proxy/usageStats.d.ts +25 -3
  28. package/dist/lib/proxy/usageStats.js +546 -55
  29. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
  30. package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
  31. package/dist/lib/types/proxy.d.ts +90 -1
  32. package/dist/proxy/proxyAnalysis.js +136 -12
  33. package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
  34. package/dist/proxy/proxyTranslationEngine.js +56 -12
  35. package/dist/proxy/rawStreamCapture.js +47 -1
  36. package/dist/proxy/requestLogger.d.ts +2 -0
  37. package/dist/proxy/requestLogger.js +72 -13
  38. package/dist/proxy/rollingProxyServer.js +79 -8
  39. package/dist/proxy/rollingWorkerProcess.js +20 -15
  40. package/dist/proxy/rollingWorkerProtocol.js +3 -0
  41. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  42. package/dist/proxy/rollingWorkerSupervisor.js +31 -11
  43. package/dist/proxy/runtimeConfig.d.ts +1 -0
  44. package/dist/proxy/runtimeConfig.js +20 -5
  45. package/dist/proxy/socketWorkerRuntime.js +41 -13
  46. package/dist/proxy/sseInterceptor.js +47 -1
  47. package/dist/proxy/tokenRefresh.d.ts +6 -0
  48. package/dist/proxy/tokenRefresh.js +70 -15
  49. package/dist/proxy/usageStats.d.ts +25 -3
  50. package/dist/proxy/usageStats.js +546 -55
  51. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
  52. package/dist/server/routes/claudeProxyRoutes.js +261 -297
  53. package/dist/types/proxy.d.ts +90 -1
  54. 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
- status?: "active" | "cooling" | "disabled" | "excluded" | "removed";
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 = Date.parse(String(record.timestamp ?? ""));
220
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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 = Date.parse(String(record.timestamp ?? ""));
302
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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 = Date.parse(String(record.timestamp ?? ""));
364
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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: lifecycleFiles.length > 0,
406
- finalRequests: requestFiles.length > 0,
407
- attempts: attemptFiles.length > 0,
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
- if (!succeeded) {
376
- tracer?.setError("generation_error", finalStreamError.slice(0, 500));
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
- // Use the real outcome status so trace data matches the logged
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: succeeded ? 200 : 500,
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(500, lastAttemptLabel, "translation");
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(500, Date.now() - requestStartTime);
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: 500,
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: transform.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
  /**