@juspay/neurolink 12.12.6 → 12.12.7
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 +398 -400
- package/dist/cli/commands/proxy.js +62 -24
- package/dist/cli/commands/proxyAnalyze.js +4 -1
- package/dist/proxy/codexUsage.d.ts +2 -1
- package/dist/proxy/codexUsage.js +82 -33
- package/dist/proxy/proxyActivity.d.ts +4 -1
- package/dist/proxy/proxyActivity.js +40 -14
- package/dist/proxy/proxyAnalysis.js +184 -59
- package/dist/proxy/proxyLifecycle.d.ts +1 -1
- package/dist/proxy/proxyLifecycle.js +54 -8
- package/dist/proxy/requestLogger.d.ts +2 -1
- package/dist/proxy/requestLogger.js +87 -29
- package/dist/proxy/sseInterceptor.js +36 -18
- package/dist/proxy/streamOutcome.d.ts +1 -1
- package/dist/proxy/streamOutcome.js +7 -1
- package/dist/server/routes/claudeProxyRoutes.js +70 -16
- package/dist/server/routes/codexProxyRoutes.js +73 -8
- package/dist/types/proxy.d.ts +69 -3
- package/package.json +2 -1
|
@@ -50,20 +50,18 @@ export function extractSSEEvents(buffer) {
|
|
|
50
50
|
const rawBlock = buffer.slice(cursor, boundary);
|
|
51
51
|
cursor = boundary + boundaryMatch[0].length;
|
|
52
52
|
let eventType = "";
|
|
53
|
-
|
|
53
|
+
const dataLines = [];
|
|
54
54
|
const lines = rawBlock.split(/\r\n|\n|\r/);
|
|
55
55
|
for (const line of lines) {
|
|
56
|
-
if (line.startsWith("event:
|
|
57
|
-
eventType = line.slice(
|
|
58
|
-
}
|
|
59
|
-
else if (line.startsWith("data: ")) {
|
|
60
|
-
dataValue = line.slice(6);
|
|
56
|
+
if (line.startsWith("event:")) {
|
|
57
|
+
eventType = line.slice(6).trim();
|
|
61
58
|
}
|
|
62
59
|
else if (line.startsWith("data:")) {
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const value = line.slice(5);
|
|
61
|
+
dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
65
62
|
}
|
|
66
63
|
}
|
|
64
|
+
const dataValue = dataLines.join("\n");
|
|
67
65
|
if (eventType || dataValue) {
|
|
68
66
|
events.push({ event: eventType, data: dataValue });
|
|
69
67
|
}
|
|
@@ -75,6 +73,7 @@ export function extractSSEEvents(buffer) {
|
|
|
75
73
|
// ---------------------------------------------------------------------------
|
|
76
74
|
function createAccumulator(captureRawText) {
|
|
77
75
|
return {
|
|
76
|
+
messageStopReceived: false,
|
|
78
77
|
messageId: "",
|
|
79
78
|
model: "",
|
|
80
79
|
inputTokens: 0,
|
|
@@ -171,6 +170,8 @@ function finalize(acc) {
|
|
|
171
170
|
const totalTokens = acc.inputTokens + acc.outputTokens;
|
|
172
171
|
return {
|
|
173
172
|
messageId: acc.messageId,
|
|
173
|
+
messageStopReceived: acc.messageStopReceived,
|
|
174
|
+
firstUsefulOutputAt: acc.firstUsefulOutputAt,
|
|
174
175
|
model: acc.model,
|
|
175
176
|
usage: {
|
|
176
177
|
inputTokens: acc.inputTokens,
|
|
@@ -327,7 +328,11 @@ function processEvent(acc, event) {
|
|
|
327
328
|
// Malformed JSON — skip silently, bytes already forwarded to client
|
|
328
329
|
return;
|
|
329
330
|
}
|
|
330
|
-
|
|
331
|
+
const payloadType = parsed && typeof parsed === "object" && "type" in parsed
|
|
332
|
+
? parsed.type
|
|
333
|
+
: undefined;
|
|
334
|
+
const eventType = event.event || (typeof payloadType === "string" ? payloadType : "");
|
|
335
|
+
if (eventType === "error" || payloadType === "error") {
|
|
331
336
|
const payload = parsed && typeof parsed === "object"
|
|
332
337
|
? parsed
|
|
333
338
|
: {};
|
|
@@ -340,7 +345,26 @@ function processEvent(acc, event) {
|
|
|
340
345
|
? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
|
|
341
346
|
: truncateString(event.data, MAX_EVENT_DATA_BYTES);
|
|
342
347
|
}
|
|
343
|
-
|
|
348
|
+
if (eventType === "message_stop") {
|
|
349
|
+
acc.messageStopReceived = true;
|
|
350
|
+
}
|
|
351
|
+
if (eventType === "content_block_delta" &&
|
|
352
|
+
parsed &&
|
|
353
|
+
typeof parsed === "object" &&
|
|
354
|
+
"delta" in parsed) {
|
|
355
|
+
const delta = parsed.delta;
|
|
356
|
+
if (delta &&
|
|
357
|
+
typeof delta === "object" &&
|
|
358
|
+
(("text" in delta &&
|
|
359
|
+
typeof delta.text === "string" &&
|
|
360
|
+
delta.text.length > 0) ||
|
|
361
|
+
("partial_json" in delta &&
|
|
362
|
+
typeof delta.partial_json === "string" &&
|
|
363
|
+
delta.partial_json.length > 0))) {
|
|
364
|
+
acc.firstUsefulOutputAt ??= Date.now();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
switch (eventType) {
|
|
344
368
|
case "message_start":
|
|
345
369
|
processMessageStart(acc, parsed);
|
|
346
370
|
break;
|
|
@@ -415,14 +439,8 @@ export function createSSEInterceptor(options = {}) {
|
|
|
415
439
|
appendRawTextChunk(acc, finalChunk);
|
|
416
440
|
sseBuffer += finalChunk;
|
|
417
441
|
}
|
|
418
|
-
//
|
|
419
|
-
// not
|
|
420
|
-
if (sseBuffer.trim()) {
|
|
421
|
-
const { events } = extractSSEEvents(sseBuffer + "\n\n");
|
|
422
|
-
for (const event of events) {
|
|
423
|
-
processEvent(acc, event);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
442
|
+
// SSE dispatch requires a blank line. An unterminated terminal event
|
|
443
|
+
// is not evidence that the client could observe protocol completion.
|
|
426
444
|
settle();
|
|
427
445
|
},
|
|
428
446
|
});
|
|
@@ -2,4 +2,4 @@ import type { AnthropicStreamPreflightResult, StreamTerminalOutcome, StreamTermi
|
|
|
2
2
|
/** Hold only pre-commit SSE frames so immediate upstream errors remain retryable. */
|
|
3
3
|
export declare function preflightAnthropicStream(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<AnthropicStreamPreflightResult>;
|
|
4
4
|
export declare function createStreamTerminalOutcomeTracker(): StreamTerminalOutcomeTracker;
|
|
5
|
-
export declare function mergeStreamTerminalOutcome(outcome: StreamTerminalOutcome, sseErrorMessage?: string): StreamTerminalOutcome;
|
|
5
|
+
export declare function mergeStreamTerminalOutcome(outcome: StreamTerminalOutcome, sseErrorMessage?: string, messageStopReceived?: boolean): StreamTerminalOutcome;
|
|
@@ -95,9 +95,15 @@ export function createStreamTerminalOutcomeTracker() {
|
|
|
95
95
|
cancel: () => settle({ kind: "client_cancelled" }),
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
-
export function mergeStreamTerminalOutcome(outcome, sseErrorMessage) {
|
|
98
|
+
export function mergeStreamTerminalOutcome(outcome, sseErrorMessage, messageStopReceived) {
|
|
99
99
|
if (outcome.kind === "completed" && sseErrorMessage) {
|
|
100
100
|
return { kind: "upstream_error", message: sseErrorMessage };
|
|
101
101
|
}
|
|
102
|
+
if (outcome.kind === "completed" && messageStopReceived === false) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "upstream_error",
|
|
105
|
+
message: "Anthropic stream ended without a message_stop event",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
102
108
|
return outcome;
|
|
103
109
|
}
|
|
@@ -26,7 +26,7 @@ import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
|
26
26
|
import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
|
|
27
27
|
import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
28
28
|
import { CodexFallbackResponseError, consumeCodexFallbackResponse, createCodexFallbackStream, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
|
|
29
|
-
import { registerProxyResponseObserver } from "../../proxy/proxyActivity.js";
|
|
29
|
+
import { registerProxyResponseObserver, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
30
30
|
import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
|
|
31
31
|
import { tracers } from "../../telemetry/tracers.js";
|
|
32
32
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
@@ -2408,6 +2408,7 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
2408
2408
|
const trackedStream = trackUpstreamReadableStream(responseBody);
|
|
2409
2409
|
let streamSource = trackedStream.stream;
|
|
2410
2410
|
let streamFinalized = false;
|
|
2411
|
+
let telemetryDone;
|
|
2411
2412
|
const finalizeStream = (status, errorType, errorMessage, usage) => {
|
|
2412
2413
|
if (streamFinalized) {
|
|
2413
2414
|
return false;
|
|
@@ -2429,9 +2430,14 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
2429
2430
|
const capturedUpstreamSpan = upstreamSpan;
|
|
2430
2431
|
const capturedResponse = response;
|
|
2431
2432
|
const capturedRequestBytes = bodyStr.length;
|
|
2432
|
-
Promise.all([
|
|
2433
|
+
telemetryDone = Promise.all([
|
|
2434
|
+
telemetry,
|
|
2435
|
+
clientCapture,
|
|
2436
|
+
trackedStream.outcome,
|
|
2437
|
+
])
|
|
2433
2438
|
.then(([data, clientBody, rawOutcome]) => {
|
|
2434
|
-
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
|
|
2439
|
+
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
|
|
2440
|
+
ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
|
|
2435
2441
|
const failure = getStreamFailureDetails(terminalOutcome);
|
|
2436
2442
|
capturedTracer.setUsage({
|
|
2437
2443
|
inputTokens: data.usage.inputTokens,
|
|
@@ -2509,7 +2515,7 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
2509
2515
|
});
|
|
2510
2516
|
}
|
|
2511
2517
|
catch {
|
|
2512
|
-
trackedStream.outcome.then((outcome) => {
|
|
2518
|
+
telemetryDone = trackedStream.outcome.then((outcome) => {
|
|
2513
2519
|
const failure = getStreamFailureDetails(outcome);
|
|
2514
2520
|
upstreamSpan?.end();
|
|
2515
2521
|
if (failure) {
|
|
@@ -2526,9 +2532,14 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
2526
2532
|
captureRawText: true,
|
|
2527
2533
|
});
|
|
2528
2534
|
streamSource = streamSource.pipeThrough(interceptor);
|
|
2529
|
-
Promise.all([
|
|
2535
|
+
telemetryDone = Promise.all([
|
|
2536
|
+
telemetry,
|
|
2537
|
+
clientCapture,
|
|
2538
|
+
trackedStream.outcome,
|
|
2539
|
+
])
|
|
2530
2540
|
.then(([data, clientBody, rawOutcome]) => {
|
|
2531
|
-
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
|
|
2541
|
+
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
|
|
2542
|
+
ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
|
|
2532
2543
|
const failure = getStreamFailureDetails(terminalOutcome);
|
|
2533
2544
|
finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message, {
|
|
2534
2545
|
inputTokens: data.usage.inputTokens,
|
|
@@ -2570,13 +2581,16 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
2570
2581
|
catch {
|
|
2571
2582
|
// Streaming capture is best-effort; the tracked source still propagates
|
|
2572
2583
|
// the transport failure to the client.
|
|
2573
|
-
trackedStream.outcome.then((outcome) => {
|
|
2584
|
+
telemetryDone = trackedStream.outcome.then((outcome) => {
|
|
2574
2585
|
const failure = getStreamFailureDetails(outcome);
|
|
2575
2586
|
finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message);
|
|
2576
2587
|
});
|
|
2577
2588
|
}
|
|
2578
2589
|
}
|
|
2579
2590
|
const clientStream = streamSource.pipeThrough(clientCaptureStream);
|
|
2591
|
+
registerProxyResponseObserver(ctx.metadata, {
|
|
2592
|
+
onTerminal: () => telemetryDone,
|
|
2593
|
+
});
|
|
2580
2594
|
return new Response(clientStream, {
|
|
2581
2595
|
status: response.status,
|
|
2582
2596
|
// Upstream headers first, then the proxy's own — passthrough already
|
|
@@ -3655,7 +3669,21 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3655
3669
|
// validation. A failed Codex attempt must not look like a served request.
|
|
3656
3670
|
responseHeaders: {},
|
|
3657
3671
|
};
|
|
3658
|
-
const
|
|
3672
|
+
const childResponse = await handleCodexResponsesRequest(codexCtx);
|
|
3673
|
+
const childObservers = takeProxyResponseObservers(codexCtx.metadata);
|
|
3674
|
+
let finishChildAccounting = () => { };
|
|
3675
|
+
const childAccounting = new Promise((resolve) => {
|
|
3676
|
+
finishChildAccounting = resolve;
|
|
3677
|
+
});
|
|
3678
|
+
// The translated child has no HTTP runtime of its own. Drive its terminal
|
|
3679
|
+
// observers here so stream failures enrich the actual Codex attempt, while
|
|
3680
|
+
// the parent remains the sole client request in final success/error totals.
|
|
3681
|
+
const codexResponse = trackProxyResponse(childResponse, finishChildAccounting, {
|
|
3682
|
+
onTerminal: (details) => Promise.allSettled(childObservers.map(async (observer) => observer.onTerminal?.(details))),
|
|
3683
|
+
});
|
|
3684
|
+
registerProxyResponseObserver(ctx.metadata, {
|
|
3685
|
+
onTerminal: () => childAccounting,
|
|
3686
|
+
});
|
|
3659
3687
|
const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
|
|
3660
3688
|
if (body.stream) {
|
|
3661
3689
|
const bridge = await createCodexFallbackStream(codexResponse, body.model);
|
|
@@ -3753,6 +3781,10 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3753
3781
|
return;
|
|
3754
3782
|
}
|
|
3755
3783
|
const frame = capture(next.value);
|
|
3784
|
+
if (frame.startsWith("event: content_block_delta\n") &&
|
|
3785
|
+
/"(?:text|partial_json)":"(?:[^"\\]|\\.)+"/.test(frame)) {
|
|
3786
|
+
ctx.metadata.firstUsefulOutputAt ??= Date.now();
|
|
3787
|
+
}
|
|
3756
3788
|
if (frame.startsWith("event: message_stop\n")) {
|
|
3757
3789
|
// Finalize before exposing the terminal frame: a client can close
|
|
3758
3790
|
// immediately after receiving it without making another pull.
|
|
@@ -3911,6 +3943,13 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
3911
3943
|
const { ctx, body, parsedFallbackRequest, fallbackPlan: providedFallbackPlan, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
3912
3944
|
const fallbackPlan = providedFallbackPlan ??
|
|
3913
3945
|
buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedFallbackRequest);
|
|
3946
|
+
ctx.metadata.fallbackPlan = fallbackPlan.attempts
|
|
3947
|
+
.slice(1)
|
|
3948
|
+
.map(({ provider, model, reasoningEffort }) => ({
|
|
3949
|
+
provider,
|
|
3950
|
+
model,
|
|
3951
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
3952
|
+
}));
|
|
3914
3953
|
logProxyBody({
|
|
3915
3954
|
phase: "routing_decision",
|
|
3916
3955
|
contentType: "application/json",
|
|
@@ -4669,6 +4708,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
4669
4708
|
attemptNumber,
|
|
4670
4709
|
finalBodyStr,
|
|
4671
4710
|
upstreamSpan,
|
|
4711
|
+
logAttempt,
|
|
4672
4712
|
logProxyBody,
|
|
4673
4713
|
logFinalRequest,
|
|
4674
4714
|
});
|
|
@@ -4692,13 +4732,14 @@ function getStreamFailureDetails(outcome) {
|
|
|
4692
4732
|
}
|
|
4693
4733
|
return undefined;
|
|
4694
4734
|
}
|
|
4695
|
-
function recordCommittedAnthropicStreamAttemptFailure(outcome, account) {
|
|
4735
|
+
function recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt) {
|
|
4696
4736
|
if (outcome.kind === "upstream_error") {
|
|
4697
4737
|
recordAttemptError(account.label, account.type, 502);
|
|
4738
|
+
logAttempt(502, "stream_error", outcome.message, { retryable: false });
|
|
4698
4739
|
}
|
|
4699
4740
|
}
|
|
4700
4741
|
function attachAnthropicSuccessStreamTelemetry(args) {
|
|
4701
|
-
const { ctx, account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
|
|
4742
|
+
const { ctx, account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
|
|
4702
4743
|
const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
|
|
4703
4744
|
let streamSource = remainingStream;
|
|
4704
4745
|
let telemetryDone;
|
|
@@ -4716,8 +4757,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
4716
4757
|
const capturedAccountKey = account.key;
|
|
4717
4758
|
telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
|
|
4718
4759
|
.then(([data, clientBody, rawOutcome]) => {
|
|
4719
|
-
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
|
|
4720
|
-
|
|
4760
|
+
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
|
|
4761
|
+
ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
|
|
4762
|
+
recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
|
|
4721
4763
|
capturedTracer.setUsage({
|
|
4722
4764
|
inputTokens: data.usage.inputTokens,
|
|
4723
4765
|
outputTokens: data.usage.outputTokens,
|
|
@@ -4814,7 +4856,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
4814
4856
|
// still settle the request from the actual stream terminal outcome.
|
|
4815
4857
|
telemetryDone = streamOutcome
|
|
4816
4858
|
.then((outcome) => {
|
|
4817
|
-
recordCommittedAnthropicStreamAttemptFailure(outcome, account);
|
|
4859
|
+
recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt);
|
|
4818
4860
|
const failure = getStreamFailureDetails(outcome);
|
|
4819
4861
|
upstreamSpan?.end();
|
|
4820
4862
|
if (failure) {
|
|
@@ -4845,8 +4887,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
4845
4887
|
streamOutcome,
|
|
4846
4888
|
])
|
|
4847
4889
|
.then(([data, clientBody, rawOutcome]) => {
|
|
4848
|
-
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
|
|
4849
|
-
|
|
4890
|
+
const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage, data.messageStopReceived);
|
|
4891
|
+
ctx.metadata.firstUsefulOutputAt = data.firstUsefulOutputAt;
|
|
4892
|
+
recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account, logAttempt);
|
|
4850
4893
|
const failure = getStreamFailureDetails(terminalOutcome);
|
|
4851
4894
|
const usage = {
|
|
4852
4895
|
inputTokens: data.usage.inputTokens,
|
|
@@ -4915,7 +4958,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
4915
4958
|
});
|
|
4916
4959
|
telemetryDone = streamOutcome
|
|
4917
4960
|
.then((outcome) => {
|
|
4918
|
-
recordCommittedAnthropicStreamAttemptFailure(outcome, account);
|
|
4961
|
+
recordCommittedAnthropicStreamAttemptFailure(outcome, account, logAttempt);
|
|
4919
4962
|
const failure = getStreamFailureDetails(outcome);
|
|
4920
4963
|
if (failure) {
|
|
4921
4964
|
logFinalRequest(failure.status, account.label, account.type, failure.errorType, failure.message);
|
|
@@ -4928,6 +4971,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
4928
4971
|
}
|
|
4929
4972
|
}
|
|
4930
4973
|
const clientStream = streamSource.pipeThrough(clientCaptureStream);
|
|
4974
|
+
registerProxyResponseObserver(ctx.metadata, {
|
|
4975
|
+
onTerminal: () => telemetryDone,
|
|
4976
|
+
});
|
|
4931
4977
|
// Limit headers published on the context are applied here rather than left
|
|
4932
4978
|
// to the runtime wrapper: this Response goes straight to the client on every
|
|
4933
4979
|
// mount (proxy runtime and the generic server adapters alike), so the
|
|
@@ -5958,6 +6004,11 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
5958
6004
|
...buildClientAttribution(ctx.headers),
|
|
5959
6005
|
responseStatus: status,
|
|
5960
6006
|
responseTimeMs: Date.now() - requestStartTime,
|
|
6007
|
+
...(typeof ctx.metadata.firstUsefulOutputAt === "number"
|
|
6008
|
+
? {
|
|
6009
|
+
firstUsefulOutputMs: Math.max(0, ctx.metadata.firstUsefulOutputAt - requestStartTime),
|
|
6010
|
+
}
|
|
6011
|
+
: {}),
|
|
5961
6012
|
...(errorType ? { errorType } : {}),
|
|
5962
6013
|
...(errorMessage ? { errorMessage } : {}),
|
|
5963
6014
|
...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
|
|
@@ -5980,6 +6031,9 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
5980
6031
|
? { traceId: traceCtx.traceId, spanId: traceCtx.spanId }
|
|
5981
6032
|
: {}),
|
|
5982
6033
|
...(routingDecision ? { routingDecision } : {}),
|
|
6034
|
+
...(Array.isArray(ctx.metadata.fallbackPlan)
|
|
6035
|
+
? { fallbackPlan: ctx.metadata.fallbackPlan }
|
|
6036
|
+
: {}),
|
|
5983
6037
|
});
|
|
5984
6038
|
};
|
|
5985
6039
|
const buildLoggedClaudeError = (status, message, errorType, extra) => {
|
|
@@ -281,6 +281,13 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
281
281
|
// not an independently final client request. The parent fallback owns the
|
|
282
282
|
// final status and can still recover with a later provider.
|
|
283
283
|
const isFallbackRequest = ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] === true;
|
|
284
|
+
const reasoning = body.reasoning;
|
|
285
|
+
const reasoningEffort = reasoning &&
|
|
286
|
+
typeof reasoning === "object" &&
|
|
287
|
+
"effort" in reasoning &&
|
|
288
|
+
typeof reasoning.effort === "string"
|
|
289
|
+
? reasoning.effort
|
|
290
|
+
: undefined;
|
|
284
291
|
const writeFinalLog = (account, responseStatus, extra = {}) => logRequest({
|
|
285
292
|
timestamp: new Date().toISOString(),
|
|
286
293
|
requestId: ctx.requestId,
|
|
@@ -328,6 +335,10 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
328
335
|
timestamp: new Date().toISOString(),
|
|
329
336
|
requestId: ctx.requestId,
|
|
330
337
|
attempt,
|
|
338
|
+
...(isFallbackRequest
|
|
339
|
+
? { parentRequestId: ctx.requestId.replace(/:codex-fallback$/, "") }
|
|
340
|
+
: {}),
|
|
341
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
331
342
|
method: ctx.method,
|
|
332
343
|
path: ctx.path,
|
|
333
344
|
model,
|
|
@@ -473,23 +484,37 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
473
484
|
...(ctx.responseHeaders ?? {}),
|
|
474
485
|
};
|
|
475
486
|
if (!upstream.body) {
|
|
476
|
-
|
|
477
|
-
|
|
487
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
|
|
488
|
+
writeAttempt(account, attempt, attemptStartedAt, 502, {
|
|
489
|
+
errorType: "incomplete_stream",
|
|
490
|
+
errorMessage: "Codex returned no response stream",
|
|
491
|
+
retryable: false,
|
|
492
|
+
});
|
|
493
|
+
await recordFinalOutcome(account, 502, {
|
|
494
|
+
terminalOutcome: "stream_error",
|
|
495
|
+
errorType: "incomplete_stream",
|
|
496
|
+
errorMessage: "Codex returned no response stream",
|
|
478
497
|
});
|
|
479
498
|
return new Response(upstream.body, {
|
|
480
499
|
status: upstream.status,
|
|
481
500
|
headers,
|
|
482
501
|
});
|
|
483
502
|
}
|
|
484
|
-
const { stream: usageTap, usage: usageSeen } = createCodexUsageTap();
|
|
503
|
+
const { stream: usageTap, usage: usageSeen, evidence, } = createCodexUsageTap();
|
|
485
504
|
const relay = new Response(upstream.body.pipeThrough(usageTap), {
|
|
486
505
|
status: upstream.status,
|
|
487
506
|
headers,
|
|
488
507
|
});
|
|
489
508
|
registerProxyResponseObserver(ctx.metadata, {
|
|
490
|
-
onTerminal: ({ outcome }) => {
|
|
491
|
-
|
|
509
|
+
onTerminal: ({ outcome, error, observedBodyBytes }) => {
|
|
510
|
+
return usageSeen
|
|
492
511
|
.then((usage) => {
|
|
512
|
+
const semantic = evidence();
|
|
513
|
+
const completedFrameDelivered = semantic.completed &&
|
|
514
|
+
observedBodyBytes >= semantic.terminalBytes;
|
|
515
|
+
const failed = semantic.errorType ||
|
|
516
|
+
((outcome === "completed" || outcome === "bodyless") &&
|
|
517
|
+
!semantic.completed);
|
|
493
518
|
const usageExtra = usage
|
|
494
519
|
? {
|
|
495
520
|
inputTokens: usage.inputTokens,
|
|
@@ -498,10 +523,46 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
498
523
|
cacheCreationTokens: usage.cacheCreationTokens,
|
|
499
524
|
}
|
|
500
525
|
: {};
|
|
501
|
-
|
|
526
|
+
const timing = semantic.firstUsefulOutputAt === undefined
|
|
527
|
+
? {}
|
|
528
|
+
: {
|
|
529
|
+
firstUsefulOutputMs: Math.max(0, semantic.firstUsefulOutputAt - requestStartTime),
|
|
530
|
+
};
|
|
531
|
+
if (failed) {
|
|
532
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
|
|
533
|
+
writeAttempt(account, attempt, attemptStartedAt, 502, {
|
|
534
|
+
errorType: semantic.errorType ?? "incomplete_stream",
|
|
535
|
+
errorCode: semantic.errorCode,
|
|
536
|
+
errorMessage: semantic.errorMessage ??
|
|
537
|
+
"Codex stream ended without a completion event",
|
|
538
|
+
retryable: false,
|
|
539
|
+
});
|
|
540
|
+
return recordFinalOutcome(account, 502, {
|
|
541
|
+
...usageExtra,
|
|
542
|
+
...timing,
|
|
543
|
+
terminalOutcome: "stream_error",
|
|
544
|
+
errorType: semantic.errorType ?? "incomplete_stream",
|
|
545
|
+
errorCode: semantic.errorCode,
|
|
546
|
+
errorMessage: semantic.errorMessage ??
|
|
547
|
+
"Codex stream ended without a completion event",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (outcome === "completed" ||
|
|
551
|
+
outcome === "bodyless" ||
|
|
552
|
+
(outcome === "client_cancelled" && completedFrameDelivered)) {
|
|
502
553
|
return recordFinalOutcome(account, upstream.status, {
|
|
503
|
-
terminalOutcome:
|
|
554
|
+
terminalOutcome: "completed",
|
|
504
555
|
...usageExtra,
|
|
556
|
+
...timing,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (outcome === "stream_error") {
|
|
560
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
|
|
561
|
+
writeAttempt(account, attempt, attemptStartedAt, 502, {
|
|
562
|
+
errorType: "stream_error",
|
|
563
|
+
errorCode: getCodexTransportErrorCode(error),
|
|
564
|
+
errorMessage: summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
|
|
565
|
+
retryable: false,
|
|
505
566
|
});
|
|
506
567
|
}
|
|
507
568
|
return recordFinalOutcome(account, outcome === "client_cancelled" ? 499 : 502, {
|
|
@@ -510,9 +571,13 @@ export async function handleCodexResponsesRequest(ctx) {
|
|
|
510
571
|
: "stream_error",
|
|
511
572
|
errorMessage: outcome === "client_cancelled"
|
|
512
573
|
? "Client cancelled Codex stream"
|
|
513
|
-
: "Codex upstream stream failed",
|
|
574
|
+
: summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
|
|
575
|
+
...(outcome === "stream_error"
|
|
576
|
+
? { errorCode: getCodexTransportErrorCode(error) }
|
|
577
|
+
: {}),
|
|
514
578
|
terminalOutcome: outcome,
|
|
515
579
|
...usageExtra,
|
|
580
|
+
...timing,
|
|
516
581
|
});
|
|
517
582
|
})
|
|
518
583
|
.catch(() => undefined);
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -555,6 +555,14 @@ export type ProxyAccountSortMetrics = {
|
|
|
555
555
|
scopedSaturated: boolean;
|
|
556
556
|
};
|
|
557
557
|
export type RequestLogEntry = {
|
|
558
|
+
/** First output text or tool-argument delta, excluding SSE control frames. */
|
|
559
|
+
firstUsefulOutputMs?: number;
|
|
560
|
+
/** Small routing evidence retained even when response bodies are pruned. */
|
|
561
|
+
fallbackPlan?: Array<{
|
|
562
|
+
provider: string;
|
|
563
|
+
model: string;
|
|
564
|
+
reasoningEffort?: string;
|
|
565
|
+
}>;
|
|
558
566
|
timestamp: string;
|
|
559
567
|
requestId: string;
|
|
560
568
|
method: string;
|
|
@@ -605,9 +613,31 @@ export type RequestLogEntry = {
|
|
|
605
613
|
/** Exact secret-free inputs and result of initial account selection. */
|
|
606
614
|
routingDecision?: ProxyAccountRoutingDecision;
|
|
607
615
|
};
|
|
616
|
+
/** File-sink evidence is independent of model/request success counters. */
|
|
617
|
+
export type ProxyRequestLogSinkSnapshot = {
|
|
618
|
+
attempted: number;
|
|
619
|
+
written: number;
|
|
620
|
+
inFlight: number;
|
|
621
|
+
pending: number;
|
|
622
|
+
/** Records not admitted because the bounded writer queue was full. */
|
|
623
|
+
dropped: number;
|
|
624
|
+
writeTimeouts: number;
|
|
625
|
+
unconfirmedWrites: number;
|
|
626
|
+
lastErrorCode?: string;
|
|
627
|
+
};
|
|
628
|
+
export type ProxyRequestLoggerSnapshot = {
|
|
629
|
+
enabled: boolean;
|
|
630
|
+
requests: ProxyRequestLogSinkSnapshot;
|
|
631
|
+
attempts: ProxyRequestLogSinkSnapshot;
|
|
632
|
+
debug: ProxyRequestLogSinkSnapshot;
|
|
633
|
+
};
|
|
608
634
|
export type RequestAttemptLogEntry = {
|
|
609
635
|
timestamp: string;
|
|
610
636
|
requestId: string;
|
|
637
|
+
/** Parent client request for an internal fallback invocation. */
|
|
638
|
+
parentRequestId?: string;
|
|
639
|
+
/** Requested effort retained independently of full body captures. */
|
|
640
|
+
reasoningEffort?: string;
|
|
611
641
|
attempt: number;
|
|
612
642
|
method: string;
|
|
613
643
|
path: string;
|
|
@@ -649,7 +679,7 @@ export type RequestAttemptLogEntry = {
|
|
|
649
679
|
spanId?: string;
|
|
650
680
|
};
|
|
651
681
|
/** Additional fields recorded when a Codex response becomes client-final. */
|
|
652
|
-
export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "terminalOutcome">>;
|
|
682
|
+
export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "terminalOutcome" | "firstUsefulOutputMs">>;
|
|
653
683
|
/** Additional fields recorded for each upstream Codex account attempt. */
|
|
654
684
|
export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason">>;
|
|
655
685
|
/** Minimal persistence contract needed by Codex rotating-token refreshes. */
|
|
@@ -1580,15 +1610,17 @@ export type ProxyResponseTrackingObserver = {
|
|
|
1580
1610
|
}) => void;
|
|
1581
1611
|
onTerminal?: (details: {
|
|
1582
1612
|
outcome: ProxyResponseTerminalOutcome;
|
|
1613
|
+
/** Underlying read failure for structured transport diagnostics. */
|
|
1614
|
+
error?: unknown;
|
|
1583
1615
|
/** Decoded response-body bytes observed by the adapter. */
|
|
1584
1616
|
observedBodyBytes: number;
|
|
1585
1617
|
responseChunks: number;
|
|
1586
|
-
}) =>
|
|
1618
|
+
}) => unknown;
|
|
1587
1619
|
};
|
|
1588
1620
|
/** Versioned lifecycle event names persisted by the proxy adapter. */
|
|
1589
1621
|
export type ProxyLifecycleEventName = "request_accepted" | "response_headers" | "response_first_chunk" | "request_terminal";
|
|
1590
1622
|
/** Client-facing terminal classifications recorded by lifecycle metadata. */
|
|
1591
|
-
export type ProxyLifecycleTerminalOutcome = ProxyResponseTerminalOutcome | "handler_error";
|
|
1623
|
+
export type ProxyLifecycleTerminalOutcome = ProxyResponseTerminalOutcome | "handler_error" | "unknown";
|
|
1592
1624
|
/** Content-free lifecycle event accepted by the bounded metadata logger. */
|
|
1593
1625
|
export type ProxyLifecycleEventInput = {
|
|
1594
1626
|
event: ProxyLifecycleEventName;
|
|
@@ -1601,6 +1633,13 @@ export type ProxyLifecycleEventInput = {
|
|
|
1601
1633
|
sessionHash?: string;
|
|
1602
1634
|
requestBytes?: number;
|
|
1603
1635
|
responseStatus?: number;
|
|
1636
|
+
/** Semantic final status; the HTTP status may already have been committed. */
|
|
1637
|
+
finalStatus?: number;
|
|
1638
|
+
/** Terminal bookkeeping health, separate from the model outcome. */
|
|
1639
|
+
telemetryStatus?: "complete" | "timeout" | "observer_error" | "missing_final";
|
|
1640
|
+
/** Transport completion is independent of successful model completion. */
|
|
1641
|
+
transportOutcome?: ProxyResponseTerminalOutcome;
|
|
1642
|
+
outcomeSource?: "final_request" | "transport_error" | "http_status" | "unknown";
|
|
1604
1643
|
/** Decoded response-body bytes observed by the adapter. */
|
|
1605
1644
|
observedBodyBytes?: number;
|
|
1606
1645
|
responseChunks?: number;
|
|
@@ -1627,6 +1666,10 @@ export type ProxyLifecycleLoggerSnapshot = {
|
|
|
1627
1666
|
writeFailures: number;
|
|
1628
1667
|
/** Events requeued after a transient lifecycle metadata write failure. */
|
|
1629
1668
|
writeRetries: number;
|
|
1669
|
+
/** Slow appends still owned by the original writer, never replayed on timeout. */
|
|
1670
|
+
writeTimeouts: number;
|
|
1671
|
+
/** Records in failed appends that may have partially reached the file. */
|
|
1672
|
+
unconfirmedWrites: number;
|
|
1630
1673
|
pending: number;
|
|
1631
1674
|
inFlight: number;
|
|
1632
1675
|
flushing: boolean;
|
|
@@ -1698,6 +1741,12 @@ export type ProxyAnalysisReport = {
|
|
|
1698
1741
|
unsupportedLifecycleLines: number;
|
|
1699
1742
|
lifecycleSequenceGaps: number;
|
|
1700
1743
|
lifecycleSequenceDuplicates: number;
|
|
1744
|
+
conflictingLifecycleDuplicates: number;
|
|
1745
|
+
duplicateAttempts: number;
|
|
1746
|
+
finalOutcomeConflicts: number;
|
|
1747
|
+
/** Missing evidence; may include in-flight or interrupted requests. */
|
|
1748
|
+
acceptedWithoutFinal: number;
|
|
1749
|
+
terminalWithoutFinal: number;
|
|
1701
1750
|
streams: Record<ProxyAnalysisStreamName, {
|
|
1702
1751
|
observedFrom: string | null;
|
|
1703
1752
|
observedTo: string | null;
|
|
@@ -1755,6 +1804,7 @@ export type ProxyAnalysisReport = {
|
|
|
1755
1804
|
latencyMs: {
|
|
1756
1805
|
headers: ProxyLatencySummary;
|
|
1757
1806
|
firstChunk: ProxyLatencySummary;
|
|
1807
|
+
firstUsefulOutput: ProxyLatencySummary;
|
|
1758
1808
|
terminal: ProxyLatencySummary;
|
|
1759
1809
|
finalRequest: ProxyLatencySummary;
|
|
1760
1810
|
attempt: ProxyLatencySummary;
|
|
@@ -1817,6 +1867,7 @@ export type ProxyAnalysisAttemptRecord = {
|
|
|
1817
1867
|
};
|
|
1818
1868
|
/** Final request fields retained while joining offline proxy log records. */
|
|
1819
1869
|
export type ProxyAnalysisFinalRequestRecord = {
|
|
1870
|
+
firstUsefulOutputMs: number | null;
|
|
1820
1871
|
timestamp: string;
|
|
1821
1872
|
status: number;
|
|
1822
1873
|
durationMs: number | null;
|
|
@@ -1861,6 +1912,15 @@ export type CodexStreamUsage = {
|
|
|
1861
1912
|
cacheCreationTokens: number;
|
|
1862
1913
|
reasoningTokens: number;
|
|
1863
1914
|
};
|
|
1915
|
+
/** Semantic completion evidence observed in native Codex SSE bytes. */
|
|
1916
|
+
export type CodexStreamEvidence = {
|
|
1917
|
+
completed: boolean;
|
|
1918
|
+
terminalBytes: number;
|
|
1919
|
+
firstUsefulOutputAt?: number;
|
|
1920
|
+
errorType?: string;
|
|
1921
|
+
errorMessage?: string;
|
|
1922
|
+
errorCode?: string;
|
|
1923
|
+
};
|
|
1864
1924
|
/** Validated account-routing evidence joined to a final request log. */
|
|
1865
1925
|
export type ProxyAnalysisRoutingRecord = {
|
|
1866
1926
|
requestId: string;
|
|
@@ -1883,6 +1943,8 @@ export type RuntimeRequestMetadata = {
|
|
|
1883
1943
|
rejectForUpdate?: boolean;
|
|
1884
1944
|
terminalErrorType?: string;
|
|
1885
1945
|
terminalErrorCode?: string;
|
|
1946
|
+
/** Canonical final record, populated synchronously before asynchronous I/O. */
|
|
1947
|
+
terminalResult?: RequestLogEntry;
|
|
1886
1948
|
/** Releases this request's peer-share concurrency slot. Set by the share
|
|
1887
1949
|
* gate for borrowed traffic; invoked once the response body completes, so a
|
|
1888
1950
|
* long stream holds its slot for as long as it is actually streaming. */
|
|
@@ -2073,6 +2135,8 @@ export type SSEContentBlock = {
|
|
|
2073
2135
|
};
|
|
2074
2136
|
/** Aggregated telemetry resolved when an SSE stream completes. */
|
|
2075
2137
|
export type SSETelemetry = {
|
|
2138
|
+
messageStopReceived: boolean;
|
|
2139
|
+
firstUsefulOutputAt?: number;
|
|
2076
2140
|
messageId: string;
|
|
2077
2141
|
model: string;
|
|
2078
2142
|
usage: {
|
|
@@ -2115,6 +2179,8 @@ export type StreamTerminalOutcomeTracker = {
|
|
|
2115
2179
|
};
|
|
2116
2180
|
/** Mutable accumulator the SSE interceptor uses internally. */
|
|
2117
2181
|
export type TelemetryAccumulator = {
|
|
2182
|
+
messageStopReceived: boolean;
|
|
2183
|
+
firstUsefulOutputAt?: number;
|
|
2118
2184
|
messageId: string;
|
|
2119
2185
|
model: string;
|
|
2120
2186
|
inputTokens: number;
|