@juspay/neurolink 12.14.6 → 12.14.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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +411 -411
- package/dist/cli/commands/proxy.js +83 -14
- package/dist/proxy/bodyCaptureProcessing.js +47 -3
- package/dist/proxy/bodyCaptureWorker.js +39 -10
- package/dist/proxy/claudeOutputObservation.d.ts +9 -0
- package/dist/proxy/claudeOutputObservation.js +85 -0
- package/dist/proxy/codexUsage.js +86 -3
- package/dist/proxy/otelLogSink.d.ts +13 -0
- package/dist/proxy/otelLogSink.js +63 -2
- package/dist/proxy/processTelemetry.d.ts +3 -0
- package/dist/proxy/processTelemetry.js +25 -0
- package/dist/proxy/proxyTraceContext.d.ts +21 -0
- package/dist/proxy/proxyTraceContext.js +47 -0
- package/dist/proxy/proxyTracer.d.ts +9 -5
- package/dist/proxy/proxyTracer.js +81 -2
- package/dist/proxy/rawStreamCapture.js +17 -26
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +29 -19
- package/dist/proxy/restartControl.d.ts +3 -1
- package/dist/proxy/restartControl.js +118 -1
- package/dist/proxy/sseInterceptor.js +71 -3
- package/dist/server/routes/claudeProxyRoutes.d.ts +0 -11
- package/dist/server/routes/claudeProxyRoutes.js +27 -12
- package/dist/server/routes/codexProxyRoutes.js +496 -333
- package/dist/types/cli.d.ts +7 -1
- package/dist/types/proxy.d.ts +133 -3
- package/dist/types/proxyRestart.d.ts +25 -0
- package/dist/utils/logSanitize.js +1 -1
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/check-proxy-telemetry.mjs +29 -241
- package/scripts/observability/proxy-telemetry-backend.mjs +202 -0
- package/scripts/observability/proxy-telemetry-check.mjs +578 -0
- package/scripts/observability/query-proxy-history.mjs +30 -19
|
@@ -21,6 +21,7 @@ import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
|
21
21
|
import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
|
|
22
22
|
import { notifyProxyFinalLog, notifyProxyAttemptLog } from "./proxyActivity.js";
|
|
23
23
|
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
24
|
+
import { resolveProxyLogTraceContext, proxyLogContext, } from "./proxyTraceContext.js";
|
|
24
25
|
let logDir = null;
|
|
25
26
|
let logEnabled = false;
|
|
26
27
|
const pendingLogOperations = new Set();
|
|
@@ -184,6 +185,12 @@ export function initRequestLogger(enabled = true, customLogsDir) {
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
export async function logRequest(entry) {
|
|
188
|
+
if (!entry.traceId || entry.traceFlags === undefined) {
|
|
189
|
+
const traceCtx = resolveProxyLogTraceContext(entry);
|
|
190
|
+
if (traceCtx) {
|
|
191
|
+
Object.assign(entry, traceCtx);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
187
194
|
entry.terminalOutcome ??=
|
|
188
195
|
entry.errorType === "client_cancelled" || entry.responseStatus === 499
|
|
189
196
|
? "client_cancelled"
|
|
@@ -196,17 +203,6 @@ export async function logRequest(entry) {
|
|
|
196
203
|
if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
|
|
197
204
|
return;
|
|
198
205
|
}
|
|
199
|
-
// Only use OtelBridge if traceId not already provided by caller.
|
|
200
|
-
// Deferred .then() callbacks lose async context, so OtelBridge would
|
|
201
|
-
// return undefined and overwrite the valid traceId the caller passed.
|
|
202
|
-
if (!entry.traceId) {
|
|
203
|
-
const bridge = new OtelBridge();
|
|
204
|
-
const traceCtx = bridge.getCurrentTraceContext();
|
|
205
|
-
if (traceCtx) {
|
|
206
|
-
entry.traceId = traceCtx.traceId;
|
|
207
|
-
entry.spanId = traceCtx.spanId;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
206
|
if (isProxyOtelOnly()) {
|
|
211
207
|
await emitOtlpLogRecord(entry);
|
|
212
208
|
return;
|
|
@@ -228,12 +224,12 @@ export async function logRequest(entry) {
|
|
|
228
224
|
* or OTLP-derived dashboard panels.
|
|
229
225
|
*/
|
|
230
226
|
export async function logRequestAttempt(entry) {
|
|
231
|
-
if (!entry.traceId) {
|
|
232
|
-
const
|
|
233
|
-
const traceCtx = bridge.getCurrentTraceContext();
|
|
227
|
+
if (!entry.traceId || entry.traceFlags === undefined) {
|
|
228
|
+
const traceCtx = resolveProxyLogTraceContext(entry);
|
|
234
229
|
if (traceCtx) {
|
|
235
230
|
entry.traceId = traceCtx.traceId;
|
|
236
231
|
entry.spanId = traceCtx.spanId;
|
|
232
|
+
entry.traceFlags = traceCtx.traceFlags;
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
235
|
notifyProxyAttemptLog(entry);
|
|
@@ -313,6 +309,7 @@ function emitOtlpLogRecord(entry) {
|
|
|
313
309
|
: SeverityNumber.INFO;
|
|
314
310
|
const severityText = isError ? (isRateLimit ? "WARN" : "ERROR") : "INFO";
|
|
315
311
|
otelLogger.emit({
|
|
312
|
+
context: proxyLogContext(entry),
|
|
316
313
|
severityNumber,
|
|
317
314
|
severityText,
|
|
318
315
|
body: isProxyOtelOnly()
|
|
@@ -453,6 +450,7 @@ function emitOtlpBodyLogRecord(entry, stored) {
|
|
|
453
450
|
const captureId = entry.captureId ?? randomUUID();
|
|
454
451
|
const emit = (chunk, chunkIndex, totalChunks) => {
|
|
455
452
|
otelLogger.emit({
|
|
453
|
+
context: proxyLogContext(entry),
|
|
456
454
|
severityNumber: (entry.responseStatus ?? 0) >= 400
|
|
457
455
|
? SeverityNumber.WARN
|
|
458
456
|
: SeverityNumber.INFO,
|
|
@@ -516,9 +514,13 @@ function emitOtlpBodyLogRecord(entry, stored) {
|
|
|
516
514
|
return undefined;
|
|
517
515
|
});
|
|
518
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
|
+
}
|
|
519
521
|
/** Capture an owned request body with bounded processing and tracked index/export publication. */
|
|
520
522
|
export async function logBodyCapture(entry) {
|
|
521
|
-
if (!
|
|
523
|
+
if (!isProxyBodyCaptureEnabled()) {
|
|
522
524
|
return;
|
|
523
525
|
}
|
|
524
526
|
// Borrowed traffic is somebody else's conversation. Capturing it would leave
|
|
@@ -526,12 +528,16 @@ export async function logBodyCapture(entry) {
|
|
|
526
528
|
// not something a share token can be read as consenting to. The request is
|
|
527
529
|
// still logged; only the bodies are dropped.
|
|
528
530
|
if (isBorrowedRequest()) {
|
|
531
|
+
emitProxyOtelEvent("body_capture_index", {
|
|
532
|
+
timestamp: entry.timestamp,
|
|
533
|
+
requestId: entry.requestId,
|
|
534
|
+
captureId: entry.captureId ?? randomUUID(),
|
|
535
|
+
phase: entry.phase,
|
|
536
|
+
bodyDelivery: { status: "policy_excluded", reason: "borrowed_traffic" },
|
|
537
|
+
});
|
|
529
538
|
return;
|
|
530
539
|
}
|
|
531
|
-
const
|
|
532
|
-
const traceCtx = entry.traceId && entry.spanId
|
|
533
|
-
? { traceId: entry.traceId, spanId: entry.spanId }
|
|
534
|
-
: bridge.getCurrentTraceContext();
|
|
540
|
+
const traceCtx = resolveProxyLogTraceContext(entry);
|
|
535
541
|
const destination = logDir;
|
|
536
542
|
// Publication callbacks retain metadata and the bounded redacted result,
|
|
537
543
|
// never the original unbounded body while a sink is slow.
|
|
@@ -573,6 +579,7 @@ export async function logBodyCapture(entry) {
|
|
|
573
579
|
originalRedactedBodyBytes: stored.originalRedactedBodyBytes,
|
|
574
580
|
bodyWriteFailed: stored.bodyWriteFailed,
|
|
575
581
|
captureError: processed.error,
|
|
582
|
+
captureAdmission: processed.admission,
|
|
576
583
|
captureQueueWaitMs: processed.queueWaitMs,
|
|
577
584
|
captureProcessingMs: processed.processingMs,
|
|
578
585
|
metadata: processed.error ? undefined : metadata.metadata,
|
|
@@ -580,12 +587,14 @@ export async function logBodyCapture(entry) {
|
|
|
580
587
|
if (traceCtx) {
|
|
581
588
|
indexEntry.traceId = traceCtx.traceId;
|
|
582
589
|
indexEntry.spanId = traceCtx.spanId;
|
|
590
|
+
indexEntry.traceFlags = traceCtx.traceFlags;
|
|
583
591
|
}
|
|
584
592
|
if (isProxyOtelOnly()) {
|
|
585
593
|
const delivery = await emitOtlpBodyLogRecord({
|
|
586
594
|
...metadata,
|
|
587
595
|
traceId: traceCtx?.traceId ?? metadata.traceId,
|
|
588
596
|
spanId: traceCtx?.spanId ?? metadata.spanId,
|
|
597
|
+
traceFlags: traceCtx?.traceFlags ?? metadata.traceFlags,
|
|
589
598
|
}, stored);
|
|
590
599
|
indexEntry.bodyDelivery = delivery ?? {
|
|
591
600
|
status: processed.error
|
|
@@ -612,6 +621,7 @@ export async function logBodyCapture(entry) {
|
|
|
612
621
|
...metadata,
|
|
613
622
|
traceId: traceCtx?.traceId ?? metadata.traceId,
|
|
614
623
|
spanId: traceCtx?.spanId ?? metadata.spanId,
|
|
624
|
+
traceFlags: traceCtx?.traceFlags ?? metadata.traceFlags,
|
|
615
625
|
}, stored);
|
|
616
626
|
};
|
|
617
627
|
const operation = trackLogOperation(captureProxyBody(entry, destination, consume));
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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 } : {}),
|