@juspay/neurolink 12.14.7 → 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 +357 -357
- package/dist/cli/commands/proxy.js +7 -1
- package/dist/proxy/bodyCaptureProcessing.js +47 -3
- package/dist/proxy/bodyCaptureWorker.js +14 -10
- package/dist/proxy/claudeOutputObservation.d.ts +9 -0
- package/dist/proxy/claudeOutputObservation.js +85 -0
- package/dist/proxy/codexUsage.js +12 -10
- package/dist/proxy/otelLogSink.js +1 -1
- package/dist/proxy/processTelemetry.d.ts +3 -0
- package/dist/proxy/processTelemetry.js +25 -0
- package/dist/proxy/rawStreamCapture.js +17 -26
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +5 -1
- 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 +83 -2
- package/dist/types/proxy.d.ts +14 -0
- 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/proxy-telemetry-check.mjs +39 -5
|
@@ -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 } : {}),
|
|
@@ -25,7 +25,10 @@ import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
|
25
25
|
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
|
|
26
26
|
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
27
27
|
import { registerProxyResponseObserver } from "../../proxy/proxyActivity.js";
|
|
28
|
-
import { logRequest, logRequestAttempt } from "../../proxy/requestLogger.js";
|
|
28
|
+
import { logRequest, logRequestAttempt, logBodyCapture, isProxyBodyCaptureEnabled, } from "../../proxy/requestLogger.js";
|
|
29
|
+
import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
|
|
30
|
+
import { resolveProxyLogTraceContext } from "../../proxy/proxyTraceContext.js";
|
|
31
|
+
import { isBorrowedRequest } from "../../proxy/shareContext.js";
|
|
29
32
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
30
33
|
import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
|
|
31
34
|
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
|
|
@@ -272,6 +275,68 @@ function publishCodexHeaders(ctx, account, attempt, quota) {
|
|
|
272
275
|
}
|
|
273
276
|
/** Core pooled handler for POST /backend-api/codex/responses. */
|
|
274
277
|
export async function handleCodexResponsesRequest(ctx) {
|
|
278
|
+
if (ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] !== true) {
|
|
279
|
+
void logBodyCapture({
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
requestId: ctx.requestId,
|
|
282
|
+
phase: "client_request",
|
|
283
|
+
model: codexCaptureModel(ctx),
|
|
284
|
+
stream: true,
|
|
285
|
+
headers: ctx.headers,
|
|
286
|
+
body: ctx.body,
|
|
287
|
+
contentType: "application/json",
|
|
288
|
+
...resolveProxyLogTraceContext({ requestId: ctx.requestId }),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const response = await executeCodexResponsesRequest(ctx);
|
|
292
|
+
return ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] === true
|
|
293
|
+
? response
|
|
294
|
+
: captureCodexResponse(ctx, response, "client_response");
|
|
295
|
+
}
|
|
296
|
+
function codexCaptureModel(ctx) {
|
|
297
|
+
const body = ctx.body;
|
|
298
|
+
return typeof body?.model === "string" ? body.model : "-";
|
|
299
|
+
}
|
|
300
|
+
/** Observe bytes through the existing bounded pass-through; cancellation still reaches upstream. */
|
|
301
|
+
function captureCodexResponse(ctx, response, phase, account, attempt) {
|
|
302
|
+
if (!isProxyBodyCaptureEnabled()) {
|
|
303
|
+
return response;
|
|
304
|
+
}
|
|
305
|
+
const metadata = {
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
requestId: ctx.requestId,
|
|
308
|
+
phase,
|
|
309
|
+
model: codexCaptureModel(ctx),
|
|
310
|
+
stream: true,
|
|
311
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
312
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
313
|
+
responseStatus: response.status,
|
|
314
|
+
account: account?.label,
|
|
315
|
+
accountType: account ? CODEX_ACCOUNT_TYPE : undefined,
|
|
316
|
+
attempt,
|
|
317
|
+
...resolveProxyLogTraceContext({ requestId: ctx.requestId }),
|
|
318
|
+
};
|
|
319
|
+
if (!response.body || isBorrowedRequest()) {
|
|
320
|
+
void logBodyCapture(metadata);
|
|
321
|
+
return response;
|
|
322
|
+
}
|
|
323
|
+
const observed = createRawStreamCapture();
|
|
324
|
+
void observed.capture
|
|
325
|
+
.then((capture) => logBodyCapture({
|
|
326
|
+
...metadata,
|
|
327
|
+
timestamp: new Date().toISOString(),
|
|
328
|
+
body: capture.text,
|
|
329
|
+
bodySize: capture.totalBytes,
|
|
330
|
+
sourceTruncated: capture.truncated,
|
|
331
|
+
}))
|
|
332
|
+
.catch(() => undefined);
|
|
333
|
+
return new Response(response.body.pipeThrough(observed.stream), {
|
|
334
|
+
status: response.status,
|
|
335
|
+
statusText: response.statusText,
|
|
336
|
+
headers: response.headers,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async function executeCodexResponsesRequest(ctx) {
|
|
275
340
|
const requestStartTime = Date.now();
|
|
276
341
|
const body = ctx.body ?? {};
|
|
277
342
|
const bodyStr = JSON.stringify(body);
|
|
@@ -475,9 +540,24 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
475
540
|
recordAttempt(account.label, CODEX_ACCOUNT_TYPE);
|
|
476
541
|
let upstream;
|
|
477
542
|
try {
|
|
543
|
+
const upstreamHeaders = buildCodexUpstreamHeaders(ctx.headers, account);
|
|
544
|
+
void logBodyCapture({
|
|
545
|
+
timestamp: new Date().toISOString(),
|
|
546
|
+
requestId: ctx.requestId,
|
|
547
|
+
phase: "upstream_request",
|
|
548
|
+
model,
|
|
549
|
+
stream: true,
|
|
550
|
+
body: bodyStr,
|
|
551
|
+
headers: upstreamHeaders,
|
|
552
|
+
contentType: "application/json",
|
|
553
|
+
account: account.label,
|
|
554
|
+
accountType: CODEX_ACCOUNT_TYPE,
|
|
555
|
+
attempt,
|
|
556
|
+
...tracer?.getTraceContext(),
|
|
557
|
+
});
|
|
478
558
|
upstream = await fetch(CODEX_RESPONSES_URL, {
|
|
479
559
|
method: "POST",
|
|
480
|
-
headers:
|
|
560
|
+
headers: upstreamHeaders,
|
|
481
561
|
body: bodyStr,
|
|
482
562
|
signal: ctx.abortSignal
|
|
483
563
|
? AbortSignal.any([
|
|
@@ -541,6 +621,7 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
541
621
|
lastErrorStatus = 502;
|
|
542
622
|
break; // rotate to next account
|
|
543
623
|
}
|
|
624
|
+
upstream = captureCodexResponse(ctx, upstream, "upstream_response", account, attempt);
|
|
544
625
|
if (upstream.ok) {
|
|
545
626
|
const quota = parseCodexRateLimitHeaders(upstream.headers);
|
|
546
627
|
if (quota) {
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -561,6 +561,8 @@ export type RequestLogEntry = {
|
|
|
561
561
|
firstUsefulOutputStatus?: "observed" | "no_useful_output" | "not_observed";
|
|
562
562
|
/** Native protocol event that established the first useful output. */
|
|
563
563
|
firstUsefulOutputEvent?: string;
|
|
564
|
+
/** Why first-output timing could not be established for this response. */
|
|
565
|
+
firstUsefulOutputUnavailableReason?: string;
|
|
564
566
|
/** Requested reasoning effort retained independently of body capture. */
|
|
565
567
|
reasoningEffort?: string;
|
|
566
568
|
/** Small routing evidence retained even when response bodies are pruned. */
|
|
@@ -645,6 +647,8 @@ export type ProcessedProxyBodyCapture = {
|
|
|
645
647
|
export type ProxyBodyCaptureAdmission = {
|
|
646
648
|
limitingResource: "entry" | "captures" | "bytes" | "worker";
|
|
647
649
|
estimatedBytes?: number;
|
|
650
|
+
/** Per-entry clone ceiling, independent of the aggregate queue ceiling. */
|
|
651
|
+
maxEntryBytes?: number;
|
|
648
652
|
pending: number;
|
|
649
653
|
pendingBytes: number;
|
|
650
654
|
maxPending: number;
|
|
@@ -739,6 +743,7 @@ export type ProxyTelemetryCheck = {
|
|
|
739
743
|
};
|
|
740
744
|
/** Small stored metadata used by the doctor; bodies are queried separately. */
|
|
741
745
|
export type ProxyTelemetryStoredRecord = Partial<RequestLogEntry> & {
|
|
746
|
+
recordedAtMicroseconds?: number;
|
|
742
747
|
captureId?: string;
|
|
743
748
|
bodySha256?: string;
|
|
744
749
|
redactedBodyBytes?: number;
|
|
@@ -837,6 +842,8 @@ export type CodexTokenRefresher = (refreshToken: string) => Promise<{
|
|
|
837
842
|
}>;
|
|
838
843
|
export type ProxyBodyCaptureInput = {
|
|
839
844
|
phase: string;
|
|
845
|
+
/** The stream observer retained only a bounded prefix. */
|
|
846
|
+
sourceTruncated?: boolean;
|
|
840
847
|
headers?: Record<string, string>;
|
|
841
848
|
body?: unknown;
|
|
842
849
|
bodySize?: number;
|
|
@@ -2165,6 +2172,8 @@ export type ProxyBodyCaptureEntry = {
|
|
|
2165
2172
|
headers?: Record<string, string>;
|
|
2166
2173
|
body?: unknown;
|
|
2167
2174
|
bodySize?: number;
|
|
2175
|
+
/** The bounded stream observer omitted a suffix before redaction/processing. */
|
|
2176
|
+
sourceTruncated?: boolean;
|
|
2168
2177
|
contentType?: string;
|
|
2169
2178
|
responseStatus?: number;
|
|
2170
2179
|
durationMs?: number;
|
|
@@ -2335,6 +2344,8 @@ export type SSEContentBlock = {
|
|
|
2335
2344
|
export type SSETelemetry = {
|
|
2336
2345
|
messageStopReceived: boolean;
|
|
2337
2346
|
firstUsefulOutputAt?: number;
|
|
2347
|
+
firstUsefulOutputEvent?: string;
|
|
2348
|
+
observationIncomplete?: boolean;
|
|
2338
2349
|
messageId: string;
|
|
2339
2350
|
model: string;
|
|
2340
2351
|
usage: {
|
|
@@ -2358,6 +2369,7 @@ export type SSETelemetry = {
|
|
|
2358
2369
|
/** Error carried as a terminal SSE `event: error`, if one was observed. */
|
|
2359
2370
|
streamErrorMessage?: string;
|
|
2360
2371
|
rawText?: string;
|
|
2372
|
+
rawTextTruncated?: boolean;
|
|
2361
2373
|
};
|
|
2362
2374
|
/** Terminal outcome of a response body after the HTTP headers were sent. */
|
|
2363
2375
|
export type StreamTerminalOutcome = {
|
|
@@ -2379,6 +2391,8 @@ export type StreamTerminalOutcomeTracker = {
|
|
|
2379
2391
|
export type TelemetryAccumulator = {
|
|
2380
2392
|
messageStopReceived: boolean;
|
|
2381
2393
|
firstUsefulOutputAt?: number;
|
|
2394
|
+
firstUsefulOutputEvent?: string;
|
|
2395
|
+
observationIncomplete?: boolean;
|
|
2382
2396
|
messageId: string;
|
|
2383
2397
|
model: string;
|
|
2384
2398
|
inputTokens: number;
|
|
@@ -44,6 +44,31 @@ export type ProxyRestartControlOptions = {
|
|
|
44
44
|
getInstalledVersion: () => Promise<string | undefined>;
|
|
45
45
|
isUpdatePending: () => boolean;
|
|
46
46
|
getStatus: () => Promise<unknown>;
|
|
47
|
+
/** Current supervisor process evidence; never infer it from a worker/plist. */
|
|
48
|
+
getTelemetry?: () => ProxyProcessTelemetrySnapshot;
|
|
47
49
|
/** Report control-server errors without stopping the serving listener. */
|
|
48
50
|
log?: (message: string) => void;
|
|
49
51
|
};
|
|
52
|
+
/** Small read-only process evidence, excluding environment variables and log content. */
|
|
53
|
+
export type ProxyProcessTelemetrySnapshot = {
|
|
54
|
+
pid: number;
|
|
55
|
+
checkedAt: string;
|
|
56
|
+
configuredSink: "otel" | "file";
|
|
57
|
+
lifecycleSink: string;
|
|
58
|
+
otelInitialized: boolean;
|
|
59
|
+
stdio: {
|
|
60
|
+
stdout: "file" | "non_file" | "unavailable";
|
|
61
|
+
stderr: "file" | "non_file" | "unavailable";
|
|
62
|
+
};
|
|
63
|
+
exportDropped: number;
|
|
64
|
+
exportUnconfirmed: number;
|
|
65
|
+
};
|
|
66
|
+
export type ProxySupervisorTelemetry = {
|
|
67
|
+
status: "available";
|
|
68
|
+
process: ProxyProcessTelemetrySnapshot;
|
|
69
|
+
} | {
|
|
70
|
+
status: "unavailable";
|
|
71
|
+
reason: string;
|
|
72
|
+
} | {
|
|
73
|
+
status: "not_applicable";
|
|
74
|
+
};
|
|
@@ -47,7 +47,7 @@ const SECRET_PATTERN = new RegExp(
|
|
|
47
47
|
// Bare tokens by known prefix (e.g. `sk-abc…`, `r8_xyz…`)
|
|
48
48
|
`|\\b(?:${PREFIX_PATTERN})[_\\-][A-Za-z0-9_\\-\\.]{8,}\\b` +
|
|
49
49
|
// Generic key=value pairs (URL params, JSON bodies, header dumps)
|
|
50
|
-
"|\\b(?:api[_-]?key|access[_-]?token|secret[_-]?key|refresh[_-]?token)
|
|
50
|
+
"|\\b(?:api[_-]?key|access[_-]?token|secret[_-]?key|refresh[_-]?token)['\"]?\\s*[:=]\\s*['\"]?[^\\s,;'\"&]+", "gi");
|
|
51
51
|
/** Header names that should always be redacted regardless of value shape. */
|
|
52
52
|
const SENSITIVE_HEADER_NAMES = [
|
|
53
53
|
"authorization",
|