@juspay/neurolink 12.14.7 → 12.14.9

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 (49) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/providerImageAdapter.js +1 -0
  3. package/dist/browser/neurolink.min.js +379 -379
  4. package/dist/cli/commands/proxy.js +7 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +11 -0
  7. package/dist/mcp/servers/aiProviders/aiAnalysisTools.js +43 -30
  8. package/dist/neurolink.d.ts +4 -1
  9. package/dist/neurolink.js +41 -6
  10. package/dist/providers/azureOpenai.js +1 -1
  11. package/dist/providers/catalog/loader.d.ts +1 -1
  12. package/dist/providers/catalog/loader.js +30 -1
  13. package/dist/providers/cohere.js +1 -1
  14. package/dist/providers/deepseek.js +1 -1
  15. package/dist/providers/googleAiStudio/client.js +6 -5
  16. package/dist/providers/googleVertex/client.d.ts +11 -30
  17. package/dist/providers/googleVertex/client.js +49 -131
  18. package/dist/providers/huggingFace/client.js +1 -1
  19. package/dist/providers/llamaCpp.js +1 -1
  20. package/dist/providers/lmStudio.js +1 -1
  21. package/dist/providers/nvidiaNim/client.js +1 -1
  22. package/dist/providers/ollama/client.js +1 -1
  23. package/dist/providers/openRouter/client.js +1 -1
  24. package/dist/providers/openaiCompatible/client.js +1 -1
  25. package/dist/proxy/bodyCaptureProcessing.js +47 -3
  26. package/dist/proxy/bodyCaptureWorker.js +14 -10
  27. package/dist/proxy/claudeOutputObservation.d.ts +9 -0
  28. package/dist/proxy/claudeOutputObservation.js +85 -0
  29. package/dist/proxy/codexUsage.js +12 -10
  30. package/dist/proxy/otelLogSink.js +1 -1
  31. package/dist/proxy/processTelemetry.d.ts +3 -0
  32. package/dist/proxy/processTelemetry.js +25 -0
  33. package/dist/proxy/rawStreamCapture.js +17 -26
  34. package/dist/proxy/requestLogger.d.ts +2 -0
  35. package/dist/proxy/requestLogger.js +5 -1
  36. package/dist/proxy/restartControl.d.ts +3 -1
  37. package/dist/proxy/restartControl.js +118 -1
  38. package/dist/proxy/sseInterceptor.js +71 -3
  39. package/dist/server/routes/claudeProxyRoutes.d.ts +0 -11
  40. package/dist/server/routes/claudeProxyRoutes.js +27 -12
  41. package/dist/server/routes/codexProxyRoutes.js +83 -2
  42. package/dist/types/proxy.d.ts +14 -0
  43. package/dist/types/proxyRestart.d.ts +25 -0
  44. package/dist/utils/logSanitize.js +1 -1
  45. package/dist/utils/timeout.d.ts +13 -2
  46. package/dist/utils/timeout.js +14 -3
  47. package/docs-site/static/search-index.json +1 -1
  48. package/package.json +1 -1
  49. package/scripts/observability/proxy-telemetry-check.mjs +39 -5
@@ -0,0 +1,3 @@
1
+ import type { ProxyProcessTelemetrySnapshot } from "../types/index.js";
2
+ /** Inspect this process, not the launcher configuration intended for its successor. */
3
+ export declare function getProxyProcessTelemetry(): ProxyProcessTelemetrySnapshot;
@@ -0,0 +1,25 @@
1
+ import { fstatSync } from "node:fs";
2
+ import { getProxyOtelLogSnapshot, isProxyOtelOnly } from "./otelLogSink.js";
3
+ import { getProxyLifecycleLoggerSnapshot } from "./proxyLifecycle.js";
4
+ /** Inspect this process, not the launcher configuration intended for its successor. */
5
+ export function getProxyProcessTelemetry() {
6
+ const descriptor = (fd) => {
7
+ try {
8
+ return fstatSync(fd).isFile() ? "file" : "non_file";
9
+ }
10
+ catch {
11
+ return "unavailable";
12
+ }
13
+ };
14
+ const logs = getProxyOtelLogSnapshot();
15
+ return {
16
+ pid: process.pid,
17
+ checkedAt: new Date().toISOString(),
18
+ configuredSink: isProxyOtelOnly() ? "otel" : "file",
19
+ lifecycleSink: getProxyLifecycleLoggerSnapshot().sink ?? "unavailable",
20
+ otelInitialized: logs.initialized,
21
+ stdio: { stdout: descriptor(1), stderr: descriptor(2) },
22
+ exportDropped: logs.queues.reduce((n, q) => n + q.dropped, 0),
23
+ exportUnconfirmed: logs.queues.reduce((n, q) => n + q.exportUnconfirmed, 0),
24
+ };
25
+ }
@@ -1,5 +1,9 @@
1
1
  /** Maximum bytes to capture before stopping accumulation (1 MB). */
2
2
  const MAX_CAPTURE_BYTES = 1024 * 1024;
3
+ // Payload accounting across concurrent stream observers, independent of the
4
+ // worker's clone/publication pool. Never retain an unbounded number of prefixes.
5
+ const MAX_PENDING_CAPTURE_BYTES = 16 * 1024 * 1024;
6
+ let pendingCaptureBytes = 0;
3
7
  const TRUNCATION_MARKER = "\n...[TRUNCATED]";
4
8
  export function createRawStreamCapture() {
5
9
  const decoder = new TextDecoder();
@@ -17,21 +21,13 @@ export function createRawStreamCapture() {
17
21
  return;
18
22
  }
19
23
  resolved = true;
20
- const finalChunk = decoder.decode();
21
- if (finalChunk) {
22
- if (capturedBytes < MAX_CAPTURE_BYTES) {
23
- const remainingBytes = MAX_CAPTURE_BYTES - capturedBytes;
24
- chunks.push(finalChunk.slice(0, remainingBytes));
25
- capturedBytes += Math.min(finalChunk.length, remainingBytes);
26
- }
27
- else if (!truncated) {
28
- chunks.push(TRUNCATION_MARKER);
29
- truncated = true;
30
- }
31
- }
24
+ const text = chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") +
25
+ (truncated ? "" : decoder.decode());
26
+ pendingCaptureBytes -= capturedBytes;
27
+ chunks.length = 0;
32
28
  resolveCapture({
33
29
  totalBytes,
34
- text: chunks.join(""),
30
+ text: text + (truncated ? TRUNCATION_MARKER : ""),
35
31
  truncated,
36
32
  });
37
33
  }
@@ -39,21 +35,16 @@ export function createRawStreamCapture() {
39
35
  transform(chunk, controller) {
40
36
  controller.enqueue(chunk);
41
37
  totalBytes += chunk.byteLength;
42
- if (capturedBytes < MAX_CAPTURE_BYTES) {
43
- const decoded = decoder.decode(chunk, { stream: true });
44
- const remainingBytes = MAX_CAPTURE_BYTES - capturedBytes;
45
- const slice = decoded.slice(0, remainingBytes);
38
+ if (!truncated && capturedBytes < MAX_CAPTURE_BYTES) {
39
+ const remainingBytes = Math.min(MAX_CAPTURE_BYTES - capturedBytes, Math.max(0, MAX_PENDING_CAPTURE_BYTES - pendingCaptureBytes));
40
+ // Buffer.slice is a view; copy to avoid retaining the whole wire chunk.
41
+ const slice = Uint8Array.from(chunk.subarray(0, remainingBytes));
46
42
  chunks.push(slice);
47
- capturedBytes += Math.min(decoded.length, remainingBytes);
48
- if (decoded.length > remainingBytes && !truncated) {
49
- chunks.push(TRUNCATION_MARKER);
50
- truncated = true;
51
- }
52
- }
53
- else if (!truncated) {
54
- chunks.push(TRUNCATION_MARKER);
55
- truncated = true;
43
+ capturedBytes += slice.byteLength;
44
+ pendingCaptureBytes += slice.byteLength;
45
+ truncated = slice.byteLength < chunk.byteLength;
56
46
  }
47
+ truncated ||= totalBytes > MAX_CAPTURE_BYTES;
57
48
  },
58
49
  flush() {
59
50
  settle();
@@ -35,6 +35,8 @@ export declare function prepareProxyBodyForLogging(body: unknown): {
35
35
  truncated: boolean;
36
36
  originalBytes?: number;
37
37
  };
38
+ /** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
39
+ export declare function isProxyBodyCaptureEnabled(): boolean;
38
40
  /** Capture an owned request body with bounded processing and tracked index/export publication. */
39
41
  export declare function logBodyCapture(entry: ProxyBodyCaptureEntry): Promise<void>;
40
42
  /**
@@ -514,9 +514,13 @@ function emitOtlpBodyLogRecord(entry, stored) {
514
514
  return undefined;
515
515
  });
516
516
  }
517
+ /** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
518
+ export function isProxyBodyCaptureEnabled() {
519
+ return logEnabled && (Boolean(logDir) || isProxyOtelOnly());
520
+ }
517
521
  /** Capture an owned request body with bounded processing and tracked index/export publication. */
518
522
  export async function logBodyCapture(entry) {
519
- if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
523
+ if (!isProxyBodyCaptureEnabled()) {
520
524
  return;
521
525
  }
522
526
  // Borrowed traffic is somebody else's conversation. Capturing it would leave
@@ -1,4 +1,4 @@
1
- import type { ProxyRestartControlIdentity, ProxyRestartControlOptions, ProxyRestartResult } from "../types/index.js";
1
+ import type { ProxyRestartControlIdentity, ProxyRestartControlOptions, ProxyRestartResult, ProxySupervisorTelemetry } from "../types/index.js";
2
2
  /**
3
3
  * Own restart completion in the supervisor, so a disconnected CLI cannot leave
4
4
  * admission closed. The control socket is private to the service's OS user.
@@ -8,5 +8,7 @@ export declare function startProxyRestartControl(options: ProxyRestartControlOpt
8
8
  identity: ProxyRestartControlIdentity;
9
9
  close: () => Promise<void>;
10
10
  }>;
11
+ /** Query the private supervisor endpoint with a short, bounded read; never restart. */
12
+ export declare function requestProxySupervisorTelemetry(identity: ProxyRestartControlIdentity | undefined, expectedPid: number): Promise<ProxySupervisorTelemetry>;
11
13
  /** Perform one local control operation; never fall back to killing a process. */
12
14
  export declare function requestProxyRestart(identity: ProxyRestartControlIdentity, check: boolean): Promise<ProxyRestartResult>;
@@ -1,8 +1,35 @@
1
1
  import { createServer, request } from "node:http";
2
- import { chmod, mkdir, rm } from "node:fs/promises";
2
+ import { chmod, mkdir, rm, lstat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { z } from "zod";
6
+ const processTelemetrySchema = z.object({
7
+ pid: z.number().int().positive(),
8
+ checkedAt: z.string().datetime(),
9
+ configuredSink: z.enum(["otel", "file"]),
10
+ lifecycleSink: z.string(),
11
+ otelInitialized: z.boolean(),
12
+ stdio: z.object({
13
+ stdout: z.enum(["file", "non_file", "unavailable"]),
14
+ stderr: z.enum(["file", "non_file", "unavailable"]),
15
+ }),
16
+ exportDropped: z.number().int().nonnegative(),
17
+ exportUnconfirmed: z.number().int().nonnegative(),
18
+ });
19
+ /** A worker-only restart cannot repair inherited supervisor log descriptors. */
20
+ function assertSupervisorLogging(status, snapshot) {
21
+ if (status.observability.requestLogs.diskEnabled === false &&
22
+ (!snapshot ||
23
+ snapshot.configuredSink !== "otel" ||
24
+ snapshot.lifecycleSink !== "otel" ||
25
+ !snapshot.otelInitialized ||
26
+ snapshot.stdio.stdout !== "non_file" ||
27
+ snapshot.stdio.stderr !== "non_file")) {
28
+ throw new Error(snapshot
29
+ ? "Supervisor logging is not OTel-only. Worker replacement cannot complete the logging cutover; activate the configured supervisor service first."
30
+ : "Supervisor telemetry is unavailable, so OTel-only logging cannot be proven. Activate the configured supervisor service first.");
31
+ }
32
+ }
6
33
  const statusSchema = z.object({
7
34
  pid: z.number().int().positive(),
8
35
  version: z.string(),
@@ -99,6 +126,7 @@ export async function startProxyRestartControl(options) {
99
126
  }
100
127
  previousWorkerPid = active.pid;
101
128
  const status = statusSchema.parse(await options.getStatus());
129
+ assertSupervisorLogging(status, options.getTelemetry?.());
102
130
  if (status.pid !== previousWorkerPid ||
103
131
  status.version !== active.version) {
104
132
  throw new Error("Serving worker identity changed during preflight; retry the check.");
@@ -181,6 +209,26 @@ export async function startProxyRestartControl(options) {
181
209
  res.writeHead(400, { connection: "close" }).end();
182
210
  return;
183
211
  }
212
+ if (req.method === "GET" && req.url === "/telemetry") {
213
+ req.resume();
214
+ try {
215
+ const snapshot = options.getTelemetry?.();
216
+ if (!snapshot) {
217
+ res.writeHead(404).end();
218
+ return;
219
+ }
220
+ const payload = JSON.stringify(snapshot);
221
+ res.writeHead(200, {
222
+ "content-type": "application/json",
223
+ connection: "close",
224
+ });
225
+ res.end(payload);
226
+ }
227
+ catch {
228
+ res.writeHead(503, { connection: "close" }).end();
229
+ }
230
+ return;
231
+ }
184
232
  if (!((req.method === "GET" && req.url === "/check") ||
185
233
  (req.method === "POST" && req.url === "/restart"))) {
186
234
  res.writeHead(404).end();
@@ -238,6 +286,75 @@ export async function startProxyRestartControl(options) {
238
286
  },
239
287
  };
240
288
  }
289
+ /** Query the private supervisor endpoint with a short, bounded read; never restart. */
290
+ export async function requestProxySupervisorTelemetry(identity, expectedPid) {
291
+ if (!identity) {
292
+ return { status: "unavailable", reason: "supervisor_upgrade_required" };
293
+ }
294
+ try {
295
+ if (!identity.socketPath.endsWith(`/restart-${expectedPid}-${identity.instanceId}.sock`)) {
296
+ throw new Error("identity");
297
+ }
298
+ const socket = await lstat(identity.socketPath);
299
+ if (!socket.isSocket() ||
300
+ (socket.mode & 0o077) !== 0 ||
301
+ (process.getuid && socket.uid !== process.getuid())) {
302
+ throw new Error("socket");
303
+ }
304
+ const snapshot = await new Promise((resolve, reject) => {
305
+ const req = request({
306
+ socketPath: identity.socketPath,
307
+ path: "/telemetry",
308
+ method: "GET",
309
+ headers: {
310
+ "x-neurolink-instance": identity.instanceId,
311
+ connection: "close",
312
+ },
313
+ });
314
+ const timer = setTimeout(() => req.destroy(new Error("timeout")), 1000);
315
+ req.once("error", (error) => {
316
+ clearTimeout(timer);
317
+ reject(error);
318
+ });
319
+ req.once("response", (res) => {
320
+ let body = "";
321
+ res.setEncoding("utf8");
322
+ res.on("data", (chunk) => {
323
+ body += chunk;
324
+ if (body.length > 8192) {
325
+ req.destroy(new Error("size"));
326
+ }
327
+ });
328
+ res.once("error", (error) => {
329
+ clearTimeout(timer);
330
+ reject(error);
331
+ });
332
+ res.once("end", () => {
333
+ clearTimeout(timer);
334
+ try {
335
+ if (res.statusCode !== 200) {
336
+ throw new Error("unsupported");
337
+ }
338
+ const value = processTelemetrySchema.parse(JSON.parse(body));
339
+ if (value.pid !== expectedPid ||
340
+ Math.abs(Date.now() - Date.parse(value.checkedAt)) > 5000) {
341
+ throw new Error("stale");
342
+ }
343
+ resolve(value);
344
+ }
345
+ catch (error) {
346
+ reject(error);
347
+ }
348
+ });
349
+ });
350
+ req.end();
351
+ });
352
+ return { status: "available", process: snapshot };
353
+ }
354
+ catch {
355
+ return { status: "unavailable", reason: "supervisor_telemetry_unverified" };
356
+ }
357
+ }
241
358
  /** Perform one local control operation; never fall back to killing a process. */
242
359
  export async function requestProxyRestart(identity, check) {
243
360
  return new Promise((resolve, reject) => {
@@ -15,6 +15,7 @@
15
15
  * upstreamResponse.body.pipeThrough(stream).pipeTo(clientWritable);
16
16
  * const data = await telemetry; // resolves on stream end
17
17
  */
18
+ import { hasUsefulClaudeContent } from "./claudeOutputObservation.js";
18
19
  // ---------------------------------------------------------------------------
19
20
  // Constants
20
21
  // ---------------------------------------------------------------------------
@@ -172,6 +173,8 @@ function finalize(acc) {
172
173
  messageId: acc.messageId,
173
174
  messageStopReceived: acc.messageStopReceived,
174
175
  firstUsefulOutputAt: acc.firstUsefulOutputAt,
176
+ firstUsefulOutputEvent: acc.firstUsefulOutputEvent,
177
+ observationIncomplete: acc.observationIncomplete,
175
178
  model: acc.model,
176
179
  usage: {
177
180
  inputTokens: acc.inputTokens,
@@ -191,6 +194,7 @@ function finalize(acc) {
191
194
  ? { streamErrorMessage: acc.streamErrorMessage }
192
195
  : {}),
193
196
  ...(acc.rawTextChunks ? { rawText: acc.rawTextChunks.join("") } : {}),
197
+ rawTextTruncated: acc.rawTextTruncated,
194
198
  };
195
199
  }
196
200
  // ---------------------------------------------------------------------------
@@ -320,12 +324,17 @@ function processEvent(acc, event) {
320
324
  if (!event.data) {
321
325
  return;
322
326
  }
327
+ if (event.data.length > 1024 * 1024) {
328
+ acc.observationIncomplete = true;
329
+ return;
330
+ }
323
331
  let parsed;
324
332
  try {
325
333
  parsed = JSON.parse(event.data);
326
334
  }
327
335
  catch {
328
336
  // Malformed JSON — skip silently, bytes already forwarded to client
337
+ acc.observationIncomplete = true;
329
338
  return;
330
339
  }
331
340
  const payloadType = parsed && typeof parsed === "object" && "type" in parsed
@@ -357,11 +366,52 @@ function processEvent(acc, event) {
357
366
  typeof delta === "object" &&
358
367
  (("text" in delta &&
359
368
  typeof delta.text === "string" &&
360
- delta.text.length > 0) ||
369
+ delta.text.trim().length > 0) ||
361
370
  ("partial_json" in delta &&
362
371
  typeof delta.partial_json === "string" &&
363
- delta.partial_json.length > 0))) {
364
- acc.firstUsefulOutputAt ??= Date.now();
372
+ delta.partial_json.trim().length > 0) ||
373
+ ("refusal" in delta &&
374
+ typeof delta.refusal === "string" &&
375
+ delta.refusal.trim().length > 0))) {
376
+ if (acc.firstUsefulOutputAt === undefined) {
377
+ acc.firstUsefulOutputAt = now;
378
+ acc.firstUsefulOutputEvent = `content_block_delta.${"text" in delta ? "text" : "refusal" in delta ? "refusal" : "partial_json"}`;
379
+ }
380
+ }
381
+ }
382
+ if (acc.firstUsefulOutputAt === undefined &&
383
+ parsed &&
384
+ typeof parsed === "object") {
385
+ const payload = parsed;
386
+ if (eventType === "message_start" &&
387
+ hasUsefulClaudeContent(payload.message)) {
388
+ acc.firstUsefulOutputAt = now;
389
+ acc.firstUsefulOutputEvent = "message_start.content";
390
+ }
391
+ const block = payload.content_block;
392
+ if (eventType === "content_block_start" &&
393
+ block &&
394
+ typeof block === "object") {
395
+ const content = block;
396
+ if ((typeof content.text === "string" && content.text.trim().length > 0) ||
397
+ (typeof content.refusal === "string" &&
398
+ content.refusal.trim().length > 0) ||
399
+ ((content.type === "tool_use" || content.type === "server_tool_use") &&
400
+ content.input &&
401
+ typeof content.input === "object" &&
402
+ Object.keys(content.input).length > 0)) {
403
+ acc.firstUsefulOutputAt = now;
404
+ acc.firstUsefulOutputEvent = "content_block_start";
405
+ }
406
+ }
407
+ // A completed zero-argument tool call is useful even when no input delta was sent.
408
+ if (eventType === "content_block_stop" &&
409
+ typeof payload.index === "number") {
410
+ const content = acc.contentBlocks.find((block) => block.index === payload.index);
411
+ if (content?.type === "tool_use" && content.toolName) {
412
+ acc.firstUsefulOutputAt = now;
413
+ acc.firstUsefulOutputEvent = "content_block_stop.tool_use";
414
+ }
365
415
  }
366
416
  }
367
417
  switch (eventType) {
@@ -403,6 +453,7 @@ export function createSSEInterceptor(options = {}) {
403
453
  const captureRawText = options.captureRawText ?? false;
404
454
  const acc = createAccumulator(captureRawText);
405
455
  let sseBuffer = "";
456
+ let discardingFrame = false;
406
457
  let resolved = false;
407
458
  const decoder = new TextDecoder();
408
459
  let resolveTelemetry;
@@ -415,6 +466,9 @@ export function createSSEInterceptor(options = {}) {
415
466
  return;
416
467
  }
417
468
  resolved = true;
469
+ if (sseBuffer.trim()) {
470
+ acc.observationIncomplete = true;
471
+ }
418
472
  resolveTelemetry(finalize(acc));
419
473
  }
420
474
  const transform = new TransformStream({
@@ -427,8 +481,22 @@ export function createSSEInterceptor(options = {}) {
427
481
  const decodedChunk = decoder.decode(chunk, { stream: true });
428
482
  appendRawTextChunk(acc, decodedChunk);
429
483
  sseBuffer += decodedChunk;
484
+ if (discardingFrame) {
485
+ const boundary = /\r\n\r\n|\n\n|\r\r/.exec(sseBuffer);
486
+ if (!boundary) {
487
+ sseBuffer = sseBuffer.slice(-4);
488
+ return;
489
+ }
490
+ sseBuffer = sseBuffer.slice(boundary.index + boundary[0].length);
491
+ discardingFrame = false;
492
+ }
430
493
  const { events, remainder } = extractSSEEvents(sseBuffer);
431
494
  sseBuffer = remainder;
495
+ if (sseBuffer.length > 1024 * 1024) {
496
+ acc.observationIncomplete = true;
497
+ discardingFrame = true;
498
+ sseBuffer = sseBuffer.slice(-4);
499
+ }
432
500
  for (const event of events) {
433
501
  processEvent(acc, event);
434
502
  }
@@ -1,14 +1,3 @@
1
- /**
2
- * Claude-Compatible Proxy Routes
3
- *
4
- * Exposes Anthropic-compatible /v1/messages, /v1/models, and /v1/messages/count_tokens
5
- * endpoints. ALL requests are routed through ctx.neurolink.generate() / ctx.neurolink.stream()
6
- * -- no direct HTTP calls to Anthropic.
7
- *
8
- * An optional ModelRouter can remap incoming model names to different
9
- * provider/model pairs (e.g. "claude-sonnet-4-20250514" -> vertex/gemini-2.5-pro).
10
- * Without a router, models are passed through to the Anthropic provider.
11
- */
12
1
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
2
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
3
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
@@ -1,3 +1,4 @@
1
+ import { claudeOutputLogFields, observeClaudeJsonOutput, observeClaudeStreamOutput, } from "../../proxy/claudeOutputObservation.js";
1
2
  /**
2
3
  * Claude-Compatible Proxy Routes
3
4
  *
@@ -2437,7 +2438,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2437
2438
  ])
2438
2439
  .then(([data, clientBody, rawOutcome]) => {
2439
2440
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
2440
- ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
2441
+ observeClaudeStreamOutput(ctx.metadata, data);
2441
2442
  const failure = getStreamFailureDetails(terminalOutcome);
2442
2443
  capturedTracer.setUsage({
2443
2444
  inputTokens: data.usage.inputTokens,
@@ -2482,6 +2483,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2482
2483
  phase: "upstream_response",
2483
2484
  headers: responseHeaders,
2484
2485
  body: data.rawText ?? "",
2486
+ sourceTruncated: data.rawTextTruncated,
2485
2487
  bodySize: data.totalBytesReceived,
2486
2488
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
2487
2489
  account: "passthrough",
@@ -2494,6 +2496,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2494
2496
  phase: "client_response",
2495
2497
  headers: responseHeaders,
2496
2498
  body: clientBody.text,
2499
+ sourceTruncated: clientBody.truncated,
2497
2500
  bodySize: clientBody.totalBytes,
2498
2501
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
2499
2502
  account: "passthrough",
@@ -2539,7 +2542,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2539
2542
  ])
2540
2543
  .then(([data, clientBody, rawOutcome]) => {
2541
2544
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
2542
- ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
2545
+ observeClaudeStreamOutput(ctx.metadata, data);
2543
2546
  const failure = getStreamFailureDetails(terminalOutcome);
2544
2547
  finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message, {
2545
2548
  inputTokens: data.usage.inputTokens,
@@ -2551,6 +2554,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2551
2554
  phase: "upstream_response",
2552
2555
  headers: responseHeaders,
2553
2556
  body: data.rawText ?? "",
2557
+ sourceTruncated: data.rawTextTruncated,
2554
2558
  bodySize: data.totalBytesReceived,
2555
2559
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
2556
2560
  account: "passthrough",
@@ -2563,6 +2567,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2563
2567
  phase: "client_response",
2564
2568
  headers: responseHeaders,
2565
2569
  body: clientBody.text,
2570
+ sourceTruncated: clientBody.truncated,
2566
2571
  bodySize: clientBody.totalBytes,
2567
2572
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
2568
2573
  account: "passthrough",
@@ -2603,7 +2608,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2603
2608
  });
2604
2609
  }
2605
2610
  async function handleClaudePassthroughJsonResponse(args) {
2606
- const { bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
2611
+ const { ctx, bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
2607
2612
  const responseText = await response.text();
2608
2613
  tracer?.logUpstreamResponseBody(responseText);
2609
2614
  logProxyBody({
@@ -2631,6 +2636,7 @@ async function handleClaudePassthroughJsonResponse(args) {
2631
2636
  durationMs: Date.now() - requestStartTime,
2632
2637
  });
2633
2638
  const responseJson = JSON.parse(responseText);
2639
+ observeClaudeJsonOutput(ctx.metadata, responseJson);
2634
2640
  if (tracer && responseJson && typeof responseJson === "object") {
2635
2641
  const usage = responseJson.usage;
2636
2642
  if (usage) {
@@ -3569,6 +3575,8 @@ async function executeClaudeFallbackTranslation(args) {
3569
3575
  for (const frame of serializer.finish(resolvedUsage.output, reason)) {
3570
3576
  frames.push(frame);
3571
3577
  }
3578
+ ctx.metadata.firstUsefulOutputAt = Date.now();
3579
+ ctx.metadata.firstUsefulOutputEvent = "translated_response.ready";
3572
3580
  // Telemetry AFTER validation — not before like the old lazy path
3573
3581
  tracer?.end(200, Date.now() - requestStartTime);
3574
3582
  logFinalRequest(200, "", providerLabel, undefined, undefined, {
@@ -3609,6 +3617,7 @@ async function executeClaudeFallbackTranslation(args) {
3609
3617
  };
3610
3618
  tracer?.end(200, Date.now() - requestStartTime);
3611
3619
  const clientResponse = serializeClaudeResponse(internal, body.model);
3620
+ observeClaudeJsonOutput(ctx.metadata, clientResponse);
3612
3621
  logFinalRequest(200, "", providerLabel, undefined, undefined, {
3613
3622
  inputTokens: internal.usage?.input,
3614
3623
  outputTokens: internal.usage?.output,
@@ -3855,6 +3864,7 @@ async function executeClaudeCodexFallback(args) {
3855
3864
  };
3856
3865
  tracer?.end(200, Date.now() - requestStartTime);
3857
3866
  const clientResponse = serializeClaudeResponse(internal, body.model);
3867
+ observeClaudeJsonOutput(ctx.metadata, clientResponse);
3858
3868
  logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
3859
3869
  inputTokens: parsed.usage?.input,
3860
3870
  outputTokens: parsed.usage?.output,
@@ -4476,6 +4486,7 @@ async function handleAnthropicSuccessfulResponse(args) {
4476
4486
  });
4477
4487
  }
4478
4488
  return handleAnthropicJsonSuccessResponse({
4489
+ ctx,
4479
4490
  account,
4480
4491
  response,
4481
4492
  responseHeaders,
@@ -4758,7 +4769,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4758
4769
  telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
4759
4770
  .then(([data, clientBody, rawOutcome]) => {
4760
4771
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
4761
- ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
4772
+ observeClaudeStreamOutput(ctx.metadata, data);
4762
4773
  recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
4763
4774
  capturedTracer.setUsage({
4764
4775
  inputTokens: data.usage.inputTokens,
@@ -4823,6 +4834,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4823
4834
  phase: "upstream_response",
4824
4835
  headers: responseHeaders,
4825
4836
  body: data.rawText ?? "",
4837
+ sourceTruncated: data.rawTextTruncated,
4826
4838
  bodySize: data.totalBytesReceived,
4827
4839
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
4828
4840
  account: capturedAccountLabel,
@@ -4835,6 +4847,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4835
4847
  phase: "client_response",
4836
4848
  headers: responseHeaders,
4837
4849
  body: clientBody.text,
4850
+ sourceTruncated: clientBody.truncated,
4838
4851
  bodySize: clientBody.totalBytes,
4839
4852
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
4840
4853
  account: capturedAccountLabel,
@@ -4888,7 +4901,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4888
4901
  ])
4889
4902
  .then(([data, clientBody, rawOutcome]) => {
4890
4903
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
4891
- ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
4904
+ observeClaudeStreamOutput(ctx.metadata, data);
4892
4905
  recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
4893
4906
  const failure = getStreamFailureDetails(terminalOutcome);
4894
4907
  const usage = {
@@ -4911,6 +4924,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4911
4924
  phase: "upstream_response",
4912
4925
  headers: responseHeaders,
4913
4926
  body: data.rawText ?? "",
4927
+ sourceTruncated: data.rawTextTruncated,
4914
4928
  bodySize: data.totalBytesReceived,
4915
4929
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
4916
4930
  account: capturedAccountLabel,
@@ -4923,6 +4937,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4923
4937
  phase: "client_response",
4924
4938
  headers: responseHeaders,
4925
4939
  body: clientBody.text,
4940
+ sourceTruncated: clientBody.truncated,
4926
4941
  bodySize: clientBody.totalBytes,
4927
4942
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
4928
4943
  account: capturedAccountLabel,
@@ -4944,6 +4959,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4944
4959
  phase: "client_response",
4945
4960
  headers: responseHeaders,
4946
4961
  body: clientBody.text,
4962
+ sourceTruncated: clientBody.truncated,
4947
4963
  bodySize: clientBody.totalBytes,
4948
4964
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
4949
4965
  account: account.label,
@@ -4995,7 +5011,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
4995
5011
  };
4996
5012
  }
4997
5013
  async function handleAnthropicJsonSuccessResponse(args) {
4998
- const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
5014
+ const { ctx, account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
4999
5015
  const responseText = await response.text();
5000
5016
  logAttempt(response.status, undefined, undefined, {
5001
5017
  attemptDurationMs: Date.now() - fetchStartMs,
@@ -5026,6 +5042,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5026
5042
  durationMs: Date.now() - requestStartTime,
5027
5043
  });
5028
5044
  const responseJson = JSON.parse(responseText);
5045
+ observeClaudeJsonOutput(ctx.metadata, responseJson);
5029
5046
  // Settlement is not diagnostics. It ran inside the tracer branch, so a node
5030
5047
  // with tracing off served every borrowed request for free and the lender's
5031
5048
  // ledger never moved — settle from the response itself instead.
@@ -5087,7 +5104,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5087
5104
  return { response: responseJson };
5088
5105
  }
5089
5106
  async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
5090
- const { account, accountState, requestedModel, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
5107
+ const { ctx, account, accountState, requestedModel, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
5091
5108
  const retryQuota = parseQuotaHeaders(retryResp.headers, {
5092
5109
  model: requestedModel,
5093
5110
  });
@@ -5145,6 +5162,7 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
5145
5162
  durationMs: Date.now() - requestStartTime,
5146
5163
  });
5147
5164
  const retryJson = JSON.parse(retryText);
5165
+ observeClaudeJsonOutput(ctx.metadata, retryJson);
5148
5166
  // A response served after an auth retry is a served response: it costs the
5149
5167
  // lender's account exactly what any other one does.
5150
5168
  settleFromResponseUsage(account, retryJson);
@@ -5267,6 +5285,7 @@ async function handleAnthropicAuthRetry(args) {
5267
5285
  })
5268
5286
  : {
5269
5287
  response: await handleAnthropicSuccessfulNonStreamRetryResponse({
5288
+ ctx,
5270
5289
  account,
5271
5290
  accountState,
5272
5291
  requestedModel: body.model,
@@ -6004,11 +6023,7 @@ function createClaudeRequestRuntimeContext(args) {
6004
6023
  ...buildClientAttribution(ctx.headers),
6005
6024
  responseStatus: status,
6006
6025
  responseTimeMs: Date.now() - requestStartTime,
6007
- ...(typeof ctx.metadata.firstUsefulOutputAt === "number"
6008
- ? {
6009
- firstUsefulOutputMs: Math.max(0, ctx.metadata.firstUsefulOutputAt - requestStartTime),
6010
- }
6011
- : {}),
6026
+ ...claudeOutputLogFields(ctx.metadata, requestStartTime),
6012
6027
  ...(errorType ? { errorType } : {}),
6013
6028
  ...(errorMessage ? { errorMessage } : {}),
6014
6029
  ...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),