@juspay/neurolink 12.12.6 → 12.12.8

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.
@@ -17,11 +17,78 @@ import { isBorrowedRequest } from "./shareContext.js";
17
17
  import { OtelBridge } from "../observability/otelBridge.js";
18
18
  import { SeverityNumber } from "@opentelemetry/api-logs";
19
19
  import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
20
+ import { notifyProxyFinalLog } from "./proxyActivity.js";
20
21
  import { withTimeout } from "../utils/async/withTimeout.js";
21
22
  let logDir = null;
22
23
  let logEnabled = false;
23
24
  const pendingLogOperations = new Set();
24
25
  const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
26
+ const MAX_PENDING_METADATA_RECORDS = 4_096;
27
+ const appendChains = new Map();
28
+ let appendMetadataFile = writeFile;
29
+ const createSinkSnapshot = () => ({
30
+ attempted: 0,
31
+ written: 0,
32
+ inFlight: 0,
33
+ pending: 0,
34
+ dropped: 0,
35
+ writeTimeouts: 0,
36
+ unconfirmedWrites: 0,
37
+ });
38
+ const metadataSinks = {
39
+ requests: createSinkSnapshot(),
40
+ attempts: createSinkSnapshot(),
41
+ debug: createSinkSnapshot(),
42
+ };
43
+ export function getRequestLoggerSnapshot() {
44
+ return {
45
+ enabled: logEnabled,
46
+ requests: { ...metadataSinks.requests },
47
+ attempts: { ...metadataSinks.attempts },
48
+ debug: { ...metadataSinks.debug },
49
+ };
50
+ }
51
+ async function appendMetadataRecord(file, line, kind) {
52
+ const sink = metadataSinks[kind];
53
+ sink.attempted += 1;
54
+ if (sink.pending + sink.inFlight >= MAX_PENDING_METADATA_RECORDS) {
55
+ sink.dropped += 1;
56
+ return;
57
+ }
58
+ sink.pending += 1;
59
+ // writeFile may perform multiple append syscalls for a large record. Order
60
+ // them per destination so concurrent records cannot interleave in this worker.
61
+ const operation = trackLogOperation((appendChains.get(file) ?? Promise.resolve()).then(async () => {
62
+ sink.pending -= 1;
63
+ sink.inFlight += 1;
64
+ const timer = setTimeout(() => {
65
+ sink.writeTimeouts += 1;
66
+ }, REQUEST_LOG_IO_TIMEOUT_MS);
67
+ timer.unref?.();
68
+ try {
69
+ await appendMetadataFile(file, line, { mode: 0o600, flag: "a" });
70
+ sink.written += 1;
71
+ }
72
+ catch (error) {
73
+ // A failed append may have written a prefix; never replay it.
74
+ sink.unconfirmedWrites += 1;
75
+ sink.lastErrorCode =
76
+ error?.code ?? "UNKNOWN";
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ sink.inFlight -= 1;
81
+ }
82
+ }));
83
+ appendChains.set(file, operation);
84
+ void operation.then(() => {
85
+ if (appendChains.get(file) === operation) {
86
+ appendChains.delete(file);
87
+ }
88
+ });
89
+ // Bound the caller's wait, not the lifetime/ownership of the underlying write.
90
+ await withTimeout(operation, REQUEST_LOG_IO_TIMEOUT_MS, "Proxy metadata write remains pending").catch(() => undefined);
91
+ }
25
92
  function trackLogOperation(operation) {
26
93
  pendingLogOperations.add(operation);
27
94
  void operation.then(() => pendingLogOperations.delete(operation), () => pendingLogOperations.delete(operation));
@@ -33,15 +100,7 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_IO_TIMEOUT_MS) {
33
100
  while (pendingLogOperations.size > 0) {
34
101
  const admitted = [...pendingLogOperations];
35
102
  const remainingMs = Math.max(1, deadline - Date.now());
36
- try {
37
- await withTimeout(Promise.allSettled(admitted), remainingMs, `Timed out flushing ${admitted.length} proxy request log operation(s)`);
38
- }
39
- catch (error) {
40
- for (const operation of admitted) {
41
- pendingLogOperations.delete(operation);
42
- }
43
- throw error;
44
- }
103
+ await withTimeout(Promise.allSettled(admitted), remainingMs, `Timed out flushing ${admitted.length} proxy request log operation(s)`);
45
104
  if (Date.now() >= deadline && pendingLogOperations.size > 0) {
46
105
  const remaining = pendingLogOperations.size;
47
106
  throw new Error(`Timed out flushing ${remaining} proxy request log operation(s)`);
@@ -52,6 +111,12 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_IO_TIMEOUT_MS) {
52
111
  export const __requestLoggerTestHooks = {
53
112
  pendingOperationCount: () => pendingLogOperations.size,
54
113
  trackLogOperation,
114
+ setAppendFileForTests: (writer) => {
115
+ appendMetadataFile = writer;
116
+ },
117
+ restoreAppendFileForTests: () => {
118
+ appendMetadataFile = writeFile;
119
+ },
55
120
  };
56
121
  /**
57
122
  * Lazily-resolved LoggerProvider from OTel instrumentation.
@@ -104,6 +169,15 @@ export function initRequestLogger(enabled = true, customLogsDir) {
104
169
  }
105
170
  }
106
171
  export async function logRequest(entry) {
172
+ entry.terminalOutcome ??=
173
+ entry.errorType === "client_cancelled" || entry.responseStatus === 499
174
+ ? "client_cancelled"
175
+ : entry.errorType?.includes("stream")
176
+ ? "stream_error"
177
+ : entry.responseStatus >= 400 || entry.errorType
178
+ ? "handler_error"
179
+ : "completed";
180
+ notifyProxyFinalLog(entry);
107
181
  if (!logEnabled || !logDir) {
108
182
  return;
109
183
  }
@@ -121,11 +195,7 @@ export async function logRequest(entry) {
121
195
  const logFile = join(logDir, `proxy-${new Date().toISOString().split("T")[0]}.jsonl`);
122
196
  const line = JSON.stringify(entry) + "\n";
123
197
  try {
124
- await trackLogOperation(writeFile(logFile, line, {
125
- mode: 0o600,
126
- flag: "a",
127
- signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
128
- }));
198
+ await appendMetadataRecord(logFile, line, "requests");
129
199
  }
130
200
  catch {
131
201
  // Non-fatal — don't crash proxy for logging failures
@@ -153,11 +223,7 @@ export async function logRequestAttempt(entry) {
153
223
  const logFile = join(logDir, `proxy-attempts-${new Date().toISOString().split("T")[0]}.jsonl`);
154
224
  const line = JSON.stringify(entry) + "\n";
155
225
  try {
156
- await trackLogOperation(writeFile(logFile, line, {
157
- mode: 0o600,
158
- flag: "a",
159
- signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
160
- }));
226
+ await appendMetadataRecord(logFile, line, "attempts");
161
227
  }
162
228
  catch {
163
229
  // Non-fatal — don't crash proxy for logging failures
@@ -615,11 +681,7 @@ export async function logBodyCapture(entry) {
615
681
  indexEntry.spanId = traceCtx.spanId;
616
682
  }
617
683
  try {
618
- await trackLogOperation(writeFile(logFile, JSON.stringify(indexEntry) + "\n", {
619
- mode: 0o600,
620
- flag: "a",
621
- signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
622
- }));
684
+ await appendMetadataRecord(logFile, JSON.stringify(indexEntry) + "\n", "debug");
623
685
  }
624
686
  catch {
625
687
  // Non-fatal
@@ -691,11 +753,7 @@ export async function logStreamError(entry) {
691
753
  logEntry.spanId = traceCtx.spanId;
692
754
  }
693
755
  try {
694
- await trackLogOperation(writeFile(logFile, JSON.stringify(logEntry) + "\n", {
695
- mode: 0o600,
696
- flag: "a",
697
- signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
698
- }));
756
+ await appendMetadataRecord(logFile, JSON.stringify(logEntry) + "\n", "requests");
699
757
  }
700
758
  catch {
701
759
  // Non-fatal — don't crash proxy for logging failures
@@ -50,20 +50,18 @@ export function extractSSEEvents(buffer) {
50
50
  const rawBlock = buffer.slice(cursor, boundary);
51
51
  cursor = boundary + boundaryMatch[0].length;
52
52
  let eventType = "";
53
- let dataValue = "";
53
+ const dataLines = [];
54
54
  const lines = rawBlock.split(/\r\n|\n|\r/);
55
55
  for (const line of lines) {
56
- if (line.startsWith("event: ")) {
57
- eventType = line.slice(7).trim();
58
- }
59
- else if (line.startsWith("data: ")) {
60
- dataValue = line.slice(6);
56
+ if (line.startsWith("event:")) {
57
+ eventType = line.slice(6).trim();
61
58
  }
62
59
  else if (line.startsWith("data:")) {
63
- // handle `data:` with no space (edge case)
64
- dataValue = line.slice(5);
60
+ const value = line.slice(5);
61
+ dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
65
62
  }
66
63
  }
64
+ const dataValue = dataLines.join("\n");
67
65
  if (eventType || dataValue) {
68
66
  events.push({ event: eventType, data: dataValue });
69
67
  }
@@ -75,6 +73,7 @@ export function extractSSEEvents(buffer) {
75
73
  // ---------------------------------------------------------------------------
76
74
  function createAccumulator(captureRawText) {
77
75
  return {
76
+ messageStopReceived: false,
78
77
  messageId: "",
79
78
  model: "",
80
79
  inputTokens: 0,
@@ -171,6 +170,8 @@ function finalize(acc) {
171
170
  const totalTokens = acc.inputTokens + acc.outputTokens;
172
171
  return {
173
172
  messageId: acc.messageId,
173
+ messageStopReceived: acc.messageStopReceived,
174
+ firstUsefulOutputAt: acc.firstUsefulOutputAt,
174
175
  model: acc.model,
175
176
  usage: {
176
177
  inputTokens: acc.inputTokens,
@@ -327,7 +328,11 @@ function processEvent(acc, event) {
327
328
  // Malformed JSON — skip silently, bytes already forwarded to client
328
329
  return;
329
330
  }
330
- if (event.event === "error") {
331
+ const payloadType = parsed && typeof parsed === "object" && "type" in parsed
332
+ ? parsed.type
333
+ : undefined;
334
+ const eventType = event.event || (typeof payloadType === "string" ? payloadType : "");
335
+ if (eventType === "error" || payloadType === "error") {
331
336
  const payload = parsed && typeof parsed === "object"
332
337
  ? parsed
333
338
  : {};
@@ -340,7 +345,26 @@ function processEvent(acc, event) {
340
345
  ? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
341
346
  : truncateString(event.data, MAX_EVENT_DATA_BYTES);
342
347
  }
343
- switch (event.event) {
348
+ if (eventType === "message_stop") {
349
+ acc.messageStopReceived = true;
350
+ }
351
+ if (eventType === "content_block_delta" &&
352
+ parsed &&
353
+ typeof parsed === "object" &&
354
+ "delta" in parsed) {
355
+ const delta = parsed.delta;
356
+ if (delta &&
357
+ typeof delta === "object" &&
358
+ (("text" in delta &&
359
+ typeof delta.text === "string" &&
360
+ delta.text.length > 0) ||
361
+ ("partial_json" in delta &&
362
+ typeof delta.partial_json === "string" &&
363
+ delta.partial_json.length > 0))) {
364
+ acc.firstUsefulOutputAt ??= Date.now();
365
+ }
366
+ }
367
+ switch (eventType) {
344
368
  case "message_start":
345
369
  processMessageStart(acc, parsed);
346
370
  break;
@@ -415,14 +439,8 @@ export function createSSEInterceptor(options = {}) {
415
439
  appendRawTextChunk(acc, finalChunk);
416
440
  sseBuffer += finalChunk;
417
441
  }
418
- // Process any trailing data left in the buffer (e.g. a final event
419
- // not followed by a double-newline).
420
- if (sseBuffer.trim()) {
421
- const { events } = extractSSEEvents(sseBuffer + "\n\n");
422
- for (const event of events) {
423
- processEvent(acc, event);
424
- }
425
- }
442
+ // SSE dispatch requires a blank line. An unterminated terminal event
443
+ // is not evidence that the client could observe protocol completion.
426
444
  settle();
427
445
  },
428
446
  });
@@ -2,4 +2,4 @@ import type { AnthropicStreamPreflightResult, StreamTerminalOutcome, StreamTermi
2
2
  /** Hold only pre-commit SSE frames so immediate upstream errors remain retryable. */
3
3
  export declare function preflightAnthropicStream(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<AnthropicStreamPreflightResult>;
4
4
  export declare function createStreamTerminalOutcomeTracker(): StreamTerminalOutcomeTracker;
5
- export declare function mergeStreamTerminalOutcome(outcome: StreamTerminalOutcome, sseErrorMessage?: string): StreamTerminalOutcome;
5
+ export declare function mergeStreamTerminalOutcome(outcome: StreamTerminalOutcome, sseErrorMessage?: string, messageStopReceived?: boolean): StreamTerminalOutcome;
@@ -95,9 +95,15 @@ export function createStreamTerminalOutcomeTracker() {
95
95
  cancel: () => settle({ kind: "client_cancelled" }),
96
96
  };
97
97
  }
98
- export function mergeStreamTerminalOutcome(outcome, sseErrorMessage) {
98
+ export function mergeStreamTerminalOutcome(outcome, sseErrorMessage, messageStopReceived) {
99
99
  if (outcome.kind === "completed" && sseErrorMessage) {
100
100
  return { kind: "upstream_error", message: sseErrorMessage };
101
101
  }
102
+ if (outcome.kind === "completed" && messageStopReceived === false) {
103
+ return {
104
+ kind: "upstream_error",
105
+ message: "Anthropic stream ended without a message_stop event",
106
+ };
107
+ }
102
108
  return outcome;
103
109
  }
@@ -26,7 +26,7 @@ import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
26
26
  import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
27
27
  import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
28
28
  import { CodexFallbackResponseError, consumeCodexFallbackResponse, createCodexFallbackStream, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
29
- import { registerProxyResponseObserver } from "../../proxy/proxyActivity.js";
29
+ import { registerProxyResponseObserver, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
30
30
  import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
31
31
  import { tracers } from "../../telemetry/tracers.js";
32
32
  import { withSpan } from "../../telemetry/withSpan.js";
@@ -2408,6 +2408,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2408
2408
  const trackedStream = trackUpstreamReadableStream(responseBody);
2409
2409
  let streamSource = trackedStream.stream;
2410
2410
  let streamFinalized = false;
2411
+ let telemetryDone;
2411
2412
  const finalizeStream = (status, errorType, errorMessage, usage) => {
2412
2413
  if (streamFinalized) {
2413
2414
  return false;
@@ -2429,9 +2430,14 @@ async function handleClaudePassthroughStreamResponse(args) {
2429
2430
  const capturedUpstreamSpan = upstreamSpan;
2430
2431
  const capturedResponse = response;
2431
2432
  const capturedRequestBytes = bodyStr.length;
2432
- Promise.all([telemetry, clientCapture, trackedStream.outcome])
2433
+ telemetryDone = Promise.all([
2434
+ telemetry,
2435
+ clientCapture,
2436
+ trackedStream.outcome,
2437
+ ])
2433
2438
  .then(([data, clientBody, rawOutcome]) => {
2434
- const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2439
+ const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
2440
+ ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
2435
2441
  const failure = getStreamFailureDetails(terminalOutcome);
2436
2442
  capturedTracer.setUsage({
2437
2443
  inputTokens: data.usage.inputTokens,
@@ -2509,7 +2515,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2509
2515
  });
2510
2516
  }
2511
2517
  catch {
2512
- trackedStream.outcome.then((outcome) => {
2518
+ telemetryDone = trackedStream.outcome.then((outcome) => {
2513
2519
  const failure = getStreamFailureDetails(outcome);
2514
2520
  upstreamSpan?.end();
2515
2521
  if (failure) {
@@ -2526,9 +2532,14 @@ async function handleClaudePassthroughStreamResponse(args) {
2526
2532
  captureRawText: true,
2527
2533
  });
2528
2534
  streamSource = streamSource.pipeThrough(interceptor);
2529
- Promise.all([telemetry, clientCapture, trackedStream.outcome])
2535
+ telemetryDone = Promise.all([
2536
+ telemetry,
2537
+ clientCapture,
2538
+ trackedStream.outcome,
2539
+ ])
2530
2540
  .then(([data, clientBody, rawOutcome]) => {
2531
- const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2541
+ const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
2542
+ ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
2532
2543
  const failure = getStreamFailureDetails(terminalOutcome);
2533
2544
  finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message, {
2534
2545
  inputTokens: data.usage.inputTokens,
@@ -2570,13 +2581,16 @@ async function handleClaudePassthroughStreamResponse(args) {
2570
2581
  catch {
2571
2582
  // Streaming capture is best-effort; the tracked source still propagates
2572
2583
  // the transport failure to the client.
2573
- trackedStream.outcome.then((outcome) => {
2584
+ telemetryDone = trackedStream.outcome.then((outcome) => {
2574
2585
  const failure = getStreamFailureDetails(outcome);
2575
2586
  finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message);
2576
2587
  });
2577
2588
  }
2578
2589
  }
2579
2590
  const clientStream = streamSource.pipeThrough(clientCaptureStream);
2591
+ registerProxyResponseObserver(ctx.metadata, {
2592
+ onTerminal: () => telemetryDone,
2593
+ });
2580
2594
  return new Response(clientStream, {
2581
2595
  status: response.status,
2582
2596
  // Upstream headers first, then the proxy's own — passthrough already
@@ -3655,7 +3669,21 @@ async function executeClaudeCodexFallback(args) {
3655
3669
  // validation. A failed Codex attempt must not look like a served request.
3656
3670
  responseHeaders: {},
3657
3671
  };
3658
- const codexResponse = await handleCodexResponsesRequest(codexCtx);
3672
+ const childResponse = await handleCodexResponsesRequest(codexCtx);
3673
+ const childObservers = takeProxyResponseObservers(codexCtx.metadata);
3674
+ let finishChildAccounting = () => { };
3675
+ const childAccounting = new Promise((resolve) => {
3676
+ finishChildAccounting = resolve;
3677
+ });
3678
+ // The translated child has no HTTP runtime of its own. Drive its terminal
3679
+ // observers here so stream failures enrich the actual Codex attempt, while
3680
+ // the parent remains the sole client request in final success/error totals.
3681
+ const codexResponse = trackProxyResponse(childResponse, finishChildAccounting, {
3682
+ onTerminal: (details) => Promise.allSettled(childObservers.map(async (observer) => observer.onTerminal?.(details))),
3683
+ });
3684
+ registerProxyResponseObserver(ctx.metadata, {
3685
+ onTerminal: () => childAccounting,
3686
+ });
3659
3687
  const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
3660
3688
  if (body.stream) {
3661
3689
  const bridge = await createCodexFallbackStream(codexResponse, body.model);
@@ -3753,6 +3781,10 @@ async function executeClaudeCodexFallback(args) {
3753
3781
  return;
3754
3782
  }
3755
3783
  const frame = capture(next.value);
3784
+ if (frame.startsWith("event: content_block_delta\n") &&
3785
+ /"(?:text|partial_json)":"(?:[^"\\]|\\.)+"/.test(frame)) {
3786
+ ctx.metadata.firstUsefulOutputAt ??= Date.now();
3787
+ }
3756
3788
  if (frame.startsWith("event: message_stop\n")) {
3757
3789
  // Finalize before exposing the terminal frame: a client can close
3758
3790
  // immediately after receiving it without making another pull.
@@ -3911,6 +3943,13 @@ async function tryConfiguredClaudeFallbackChain(args) {
3911
3943
  const { ctx, body, parsedFallbackRequest, fallbackPlan: providedFallbackPlan, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
3912
3944
  const fallbackPlan = providedFallbackPlan ??
3913
3945
  buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedFallbackRequest);
3946
+ ctx.metadata.fallbackPlan = fallbackPlan.attempts
3947
+ .slice(1)
3948
+ .map(({ provider, model, reasoningEffort }) => ({
3949
+ provider,
3950
+ model,
3951
+ ...(reasoningEffort ? { reasoningEffort } : {}),
3952
+ }));
3914
3953
  logProxyBody({
3915
3954
  phase: "routing_decision",
3916
3955
  contentType: "application/json",
@@ -4669,6 +4708,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
4669
4708
  attemptNumber,
4670
4709
  finalBodyStr,
4671
4710
  upstreamSpan,
4711
+ logAttempt,
4672
4712
  logProxyBody,
4673
4713
  logFinalRequest,
4674
4714
  });
@@ -4692,13 +4732,14 @@ function getStreamFailureDetails(outcome) {
4692
4732
  }
4693
4733
  return undefined;
4694
4734
  }
4695
- function recordCommittedAnthropicStreamAttemptFailure(outcome, account) {
4735
+ function recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt) {
4696
4736
  if (outcome.kind === "upstream_error") {
4697
4737
  recordAttemptError(account.label, account.type, 502);
4738
+ logAttempt(502, "stream_error", outcome.message, { retryable: false });
4698
4739
  }
4699
4740
  }
4700
4741
  function attachAnthropicSuccessStreamTelemetry(args) {
4701
- const { ctx, account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
4742
+ const { ctx, account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
4702
4743
  const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
4703
4744
  let streamSource = remainingStream;
4704
4745
  let telemetryDone;
@@ -4716,8 +4757,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4716
4757
  const capturedAccountKey = account.key;
4717
4758
  telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
4718
4759
  .then(([data, clientBody, rawOutcome]) => {
4719
- const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
4720
- recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
4760
+ const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
4761
+ ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
4762
+ recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
4721
4763
  capturedTracer.setUsage({
4722
4764
  inputTokens: data.usage.inputTokens,
4723
4765
  outputTokens: data.usage.outputTokens,
@@ -4814,7 +4856,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4814
4856
  // still settle the request from the actual stream terminal outcome.
4815
4857
  telemetryDone = streamOutcome
4816
4858
  .then((outcome) => {
4817
- recordCommittedAnthropicStreamAttemptFailure(outcome, account);
4859
+ recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt);
4818
4860
  const failure = getStreamFailureDetails(outcome);
4819
4861
  upstreamSpan?.end();
4820
4862
  if (failure) {
@@ -4845,8 +4887,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4845
4887
  streamOutcome,
4846
4888
  ])
4847
4889
  .then(([data, clientBody, rawOutcome]) => {
4848
- const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
4849
- recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
4890
+ const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
4891
+ ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
4892
+ recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
4850
4893
  const failure = getStreamFailureDetails(terminalOutcome);
4851
4894
  const usage = {
4852
4895
  inputTokens: data.usage.inputTokens,
@@ -4915,7 +4958,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4915
4958
  });
4916
4959
  telemetryDone = streamOutcome
4917
4960
  .then((outcome) => {
4918
- recordCommittedAnthropicStreamAttemptFailure(outcome, account);
4961
+ recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt);
4919
4962
  const failure = getStreamFailureDetails(outcome);
4920
4963
  if (failure) {
4921
4964
  logFinalRequest(failure.status, account.label, account.type, failure.errorType, failure.message);
@@ -4928,6 +4971,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4928
4971
  }
4929
4972
  }
4930
4973
  const clientStream = streamSource.pipeThrough(clientCaptureStream);
4974
+ registerProxyResponseObserver(ctx.metadata, {
4975
+ onTerminal: () => telemetryDone,
4976
+ });
4931
4977
  // Limit headers published on the context are applied here rather than left
4932
4978
  // to the runtime wrapper: this Response goes straight to the client on every
4933
4979
  // mount (proxy runtime and the generic server adapters alike), so the
@@ -5958,6 +6004,11 @@ function createClaudeRequestRuntimeContext(args) {
5958
6004
  ...buildClientAttribution(ctx.headers),
5959
6005
  responseStatus: status,
5960
6006
  responseTimeMs: Date.now() - requestStartTime,
6007
+ ...(typeof ctx.metadata.firstUsefulOutputAt === "number"
6008
+ ? {
6009
+ firstUsefulOutputMs: Math.max(0, ctx.metadata.firstUsefulOutputAt - requestStartTime),
6010
+ }
6011
+ : {}),
5961
6012
  ...(errorType ? { errorType } : {}),
5962
6013
  ...(errorMessage ? { errorMessage } : {}),
5963
6014
  ...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
@@ -5980,6 +6031,9 @@ function createClaudeRequestRuntimeContext(args) {
5980
6031
  ? { traceId: traceCtx.traceId, spanId: traceCtx.spanId }
5981
6032
  : {}),
5982
6033
  ...(routingDecision ? { routingDecision } : {}),
6034
+ ...(Array.isArray(ctx.metadata.fallbackPlan)
6035
+ ? { fallbackPlan: ctx.metadata.fallbackPlan }
6036
+ : {}),
5983
6037
  });
5984
6038
  };
5985
6039
  const buildLoggedClaudeError = (status, message, errorType, extra) => {
@@ -281,6 +281,13 @@ export async function handleCodexResponsesRequest(ctx) {
281
281
  // not an independently final client request. The parent fallback owns the
282
282
  // final status and can still recover with a later provider.
283
283
  const isFallbackRequest = ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] === true;
284
+ const reasoning = body.reasoning;
285
+ const reasoningEffort = reasoning &&
286
+ typeof reasoning === "object" &&
287
+ "effort" in reasoning &&
288
+ typeof reasoning.effort === "string"
289
+ ? reasoning.effort
290
+ : undefined;
284
291
  const writeFinalLog = (account, responseStatus, extra = {}) => logRequest({
285
292
  timestamp: new Date().toISOString(),
286
293
  requestId: ctx.requestId,
@@ -328,6 +335,10 @@ export async function handleCodexResponsesRequest(ctx) {
328
335
  timestamp: new Date().toISOString(),
329
336
  requestId: ctx.requestId,
330
337
  attempt,
338
+ ...(isFallbackRequest
339
+ ? { parentRequestId: ctx.requestId.replace(/:codex-fallback$/, "") }
340
+ : {}),
341
+ ...(reasoningEffort ? { reasoningEffort } : {}),
331
342
  method: ctx.method,
332
343
  path: ctx.path,
333
344
  model,
@@ -473,23 +484,37 @@ export async function handleCodexResponsesRequest(ctx) {
473
484
  ...(ctx.responseHeaders ?? {}),
474
485
  };
475
486
  if (!upstream.body) {
476
- await recordFinalOutcome(account, upstream.status, {
477
- terminalOutcome: "bodyless",
487
+ recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
488
+ writeAttempt(account, attempt, attemptStartedAt, 502, {
489
+ errorType: "incomplete_stream",
490
+ errorMessage: "Codex returned no response stream",
491
+ retryable: false,
492
+ });
493
+ await recordFinalOutcome(account, 502, {
494
+ terminalOutcome: "stream_error",
495
+ errorType: "incomplete_stream",
496
+ errorMessage: "Codex returned no response stream",
478
497
  });
479
498
  return new Response(upstream.body, {
480
499
  status: upstream.status,
481
500
  headers,
482
501
  });
483
502
  }
484
- const { stream: usageTap, usage: usageSeen } = createCodexUsageTap();
503
+ const { stream: usageTap, usage: usageSeen, evidence, } = createCodexUsageTap();
485
504
  const relay = new Response(upstream.body.pipeThrough(usageTap), {
486
505
  status: upstream.status,
487
506
  headers,
488
507
  });
489
508
  registerProxyResponseObserver(ctx.metadata, {
490
- onTerminal: ({ outcome }) => {
491
- void usageSeen
509
+ onTerminal: ({ outcome, error, observedBodyBytes }) => {
510
+ return usageSeen
492
511
  .then((usage) => {
512
+ const semantic = evidence();
513
+ const completedFrameDelivered = semantic.completed &&
514
+ observedBodyBytes >= semantic.terminalBytes;
515
+ const failed = semantic.errorType ||
516
+ ((outcome === "completed" || outcome === "bodyless") &&
517
+ !semantic.completed);
493
518
  const usageExtra = usage
494
519
  ? {
495
520
  inputTokens: usage.inputTokens,
@@ -498,10 +523,46 @@ export async function handleCodexResponsesRequest(ctx) {
498
523
  cacheCreationTokens: usage.cacheCreationTokens,
499
524
  }
500
525
  : {};
501
- if (outcome === "completed" || outcome === "bodyless") {
526
+ const timing = semantic.firstUsefulOutputAt === undefined
527
+ ? {}
528
+ : {
529
+ firstUsefulOutputMs: Math.max(0, semantic.firstUsefulOutputAt - requestStartTime),
530
+ };
531
+ if (failed) {
532
+ recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
533
+ writeAttempt(account, attempt, attemptStartedAt, 502, {
534
+ errorType: semantic.errorType ?? "incomplete_stream",
535
+ errorCode: semantic.errorCode,
536
+ errorMessage: semantic.errorMessage ??
537
+ "Codex stream ended without a completion event",
538
+ retryable: false,
539
+ });
540
+ return recordFinalOutcome(account, 502, {
541
+ ...usageExtra,
542
+ ...timing,
543
+ terminalOutcome: "stream_error",
544
+ errorType: semantic.errorType ?? "incomplete_stream",
545
+ errorCode: semantic.errorCode,
546
+ errorMessage: semantic.errorMessage ??
547
+ "Codex stream ended without a completion event",
548
+ });
549
+ }
550
+ if (outcome === "completed" ||
551
+ outcome === "bodyless" ||
552
+ (outcome === "client_cancelled" && completedFrameDelivered)) {
502
553
  return recordFinalOutcome(account, upstream.status, {
503
- terminalOutcome: outcome,
554
+ terminalOutcome: "completed",
504
555
  ...usageExtra,
556
+ ...timing,
557
+ });
558
+ }
559
+ if (outcome === "stream_error") {
560
+ recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
561
+ writeAttempt(account, attempt, attemptStartedAt, 502, {
562
+ errorType: "stream_error",
563
+ errorCode: getCodexTransportErrorCode(error),
564
+ errorMessage: summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
565
+ retryable: false,
505
566
  });
506
567
  }
507
568
  return recordFinalOutcome(account, outcome === "client_cancelled" ? 499 : 502, {
@@ -510,9 +571,13 @@ export async function handleCodexResponsesRequest(ctx) {
510
571
  : "stream_error",
511
572
  errorMessage: outcome === "client_cancelled"
512
573
  ? "Client cancelled Codex stream"
513
- : "Codex upstream stream failed",
574
+ : summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
575
+ ...(outcome === "stream_error"
576
+ ? { errorCode: getCodexTransportErrorCode(error) }
577
+ : {}),
514
578
  terminalOutcome: outcome,
515
579
  ...usageExtra,
580
+ ...timing,
516
581
  });
517
582
  })
518
583
  .catch(() => undefined);
@@ -1490,6 +1490,12 @@ export type TextGenerationResult = {
1490
1490
  model?: string;
1491
1491
  usage?: TokenUsage;
1492
1492
  responseTime?: number;
1493
+ /** The executed tool calls of a native turn — the same shape `GenerateResult` exposes. */
1494
+ toolCalls?: Array<{
1495
+ toolCallId: string;
1496
+ toolName: string;
1497
+ args: StandardRecord;
1498
+ }>;
1493
1499
  toolsUsed?: string[];
1494
1500
  toolExecutions?: Array<{
1495
1501
  toolName: string;