@juspay/neurolink 12.12.6 → 12.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -2
- package/dist/browser/neurolink.min.js +405 -407
- package/dist/cli/commands/proxy.js +62 -24
- package/dist/cli/commands/proxyAnalyze.js +4 -1
- package/dist/core/toolExecutionRecorder.d.ts +16 -1
- package/dist/core/toolExecutionRecorder.js +21 -0
- package/dist/middleware/builtin/guardrails.js +67 -17
- package/dist/neurolink.js +6 -0
- package/dist/providers/amazonSagemaker.js +2 -1
- package/dist/providers/anthropic/client.js +2 -1
- package/dist/providers/openaiChatCompletionsBase.js +2 -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/generate.d.ts +6 -0
- package/dist/types/middleware.d.ts +1 -1
- package/dist/types/proxy.d.ts +69 -3
- package/package.json +3 -1
|
@@ -17,11 +17,78 @@ import { isBorrowedRequest } from "./shareContext.js";
|
|
|
17
17
|
import { OtelBridge } from "../observability/otelBridge.js";
|
|
18
18
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
19
19
|
import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
|
|
20
|
+
import { notifyProxyFinalLog } from "./proxyActivity.js";
|
|
20
21
|
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
21
22
|
let logDir = null;
|
|
22
23
|
let logEnabled = false;
|
|
23
24
|
const pendingLogOperations = new Set();
|
|
24
25
|
const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
|
|
26
|
+
const MAX_PENDING_METADATA_RECORDS = 4_096;
|
|
27
|
+
const appendChains = new Map();
|
|
28
|
+
let appendMetadataFile = writeFile;
|
|
29
|
+
const createSinkSnapshot = () => ({
|
|
30
|
+
attempted: 0,
|
|
31
|
+
written: 0,
|
|
32
|
+
inFlight: 0,
|
|
33
|
+
pending: 0,
|
|
34
|
+
dropped: 0,
|
|
35
|
+
writeTimeouts: 0,
|
|
36
|
+
unconfirmedWrites: 0,
|
|
37
|
+
});
|
|
38
|
+
const metadataSinks = {
|
|
39
|
+
requests: createSinkSnapshot(),
|
|
40
|
+
attempts: createSinkSnapshot(),
|
|
41
|
+
debug: createSinkSnapshot(),
|
|
42
|
+
};
|
|
43
|
+
export function getRequestLoggerSnapshot() {
|
|
44
|
+
return {
|
|
45
|
+
enabled: logEnabled,
|
|
46
|
+
requests: { ...metadataSinks.requests },
|
|
47
|
+
attempts: { ...metadataSinks.attempts },
|
|
48
|
+
debug: { ...metadataSinks.debug },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function appendMetadataRecord(file, line, kind) {
|
|
52
|
+
const sink = metadataSinks[kind];
|
|
53
|
+
sink.attempted += 1;
|
|
54
|
+
if (sink.pending + sink.inFlight >= MAX_PENDING_METADATA_RECORDS) {
|
|
55
|
+
sink.dropped += 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
sink.pending += 1;
|
|
59
|
+
// writeFile may perform multiple append syscalls for a large record. Order
|
|
60
|
+
// them per destination so concurrent records cannot interleave in this worker.
|
|
61
|
+
const operation = trackLogOperation((appendChains.get(file) ?? Promise.resolve()).then(async () => {
|
|
62
|
+
sink.pending -= 1;
|
|
63
|
+
sink.inFlight += 1;
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
sink.writeTimeouts += 1;
|
|
66
|
+
}, REQUEST_LOG_IO_TIMEOUT_MS);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
try {
|
|
69
|
+
await appendMetadataFile(file, line, { mode: 0o600, flag: "a" });
|
|
70
|
+
sink.written += 1;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// A failed append may have written a prefix; never replay it.
|
|
74
|
+
sink.unconfirmedWrites += 1;
|
|
75
|
+
sink.lastErrorCode =
|
|
76
|
+
error?.code ?? "UNKNOWN";
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
sink.inFlight -= 1;
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
83
|
+
appendChains.set(file, operation);
|
|
84
|
+
void operation.then(() => {
|
|
85
|
+
if (appendChains.get(file) === operation) {
|
|
86
|
+
appendChains.delete(file);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// Bound the caller's wait, not the lifetime/ownership of the underlying write.
|
|
90
|
+
await withTimeout(operation, REQUEST_LOG_IO_TIMEOUT_MS, "Proxy metadata write remains pending").catch(() => undefined);
|
|
91
|
+
}
|
|
25
92
|
function trackLogOperation(operation) {
|
|
26
93
|
pendingLogOperations.add(operation);
|
|
27
94
|
void operation.then(() => pendingLogOperations.delete(operation), () => pendingLogOperations.delete(operation));
|
|
@@ -33,15 +100,7 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_IO_TIMEOUT_MS) {
|
|
|
33
100
|
while (pendingLogOperations.size > 0) {
|
|
34
101
|
const admitted = [...pendingLogOperations];
|
|
35
102
|
const remainingMs = Math.max(1, deadline - Date.now());
|
|
36
|
-
|
|
37
|
-
await withTimeout(Promise.allSettled(admitted), remainingMs, `Timed out flushing ${admitted.length} proxy request log operation(s)`);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
for (const operation of admitted) {
|
|
41
|
-
pendingLogOperations.delete(operation);
|
|
42
|
-
}
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
103
|
+
await withTimeout(Promise.allSettled(admitted), remainingMs, `Timed out flushing ${admitted.length} proxy request log operation(s)`);
|
|
45
104
|
if (Date.now() >= deadline && pendingLogOperations.size > 0) {
|
|
46
105
|
const remaining = pendingLogOperations.size;
|
|
47
106
|
throw new Error(`Timed out flushing ${remaining} proxy request log operation(s)`);
|
|
@@ -52,6 +111,12 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_IO_TIMEOUT_MS) {
|
|
|
52
111
|
export const __requestLoggerTestHooks = {
|
|
53
112
|
pendingOperationCount: () => pendingLogOperations.size,
|
|
54
113
|
trackLogOperation,
|
|
114
|
+
setAppendFileForTests: (writer) => {
|
|
115
|
+
appendMetadataFile = writer;
|
|
116
|
+
},
|
|
117
|
+
restoreAppendFileForTests: () => {
|
|
118
|
+
appendMetadataFile = writeFile;
|
|
119
|
+
},
|
|
55
120
|
};
|
|
56
121
|
/**
|
|
57
122
|
* Lazily-resolved LoggerProvider from OTel instrumentation.
|
|
@@ -104,6 +169,15 @@ export function initRequestLogger(enabled = true, customLogsDir) {
|
|
|
104
169
|
}
|
|
105
170
|
}
|
|
106
171
|
export async function logRequest(entry) {
|
|
172
|
+
entry.terminalOutcome ??=
|
|
173
|
+
entry.errorType === "client_cancelled" || entry.responseStatus === 499
|
|
174
|
+
? "client_cancelled"
|
|
175
|
+
: entry.errorType?.includes("stream")
|
|
176
|
+
? "stream_error"
|
|
177
|
+
: entry.responseStatus >= 400 || entry.errorType
|
|
178
|
+
? "handler_error"
|
|
179
|
+
: "completed";
|
|
180
|
+
notifyProxyFinalLog(entry);
|
|
107
181
|
if (!logEnabled || !logDir) {
|
|
108
182
|
return;
|
|
109
183
|
}
|
|
@@ -121,11 +195,7 @@ export async function logRequest(entry) {
|
|
|
121
195
|
const logFile = join(logDir, `proxy-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
122
196
|
const line = JSON.stringify(entry) + "\n";
|
|
123
197
|
try {
|
|
124
|
-
await
|
|
125
|
-
mode: 0o600,
|
|
126
|
-
flag: "a",
|
|
127
|
-
signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
|
|
128
|
-
}));
|
|
198
|
+
await appendMetadataRecord(logFile, line, "requests");
|
|
129
199
|
}
|
|
130
200
|
catch {
|
|
131
201
|
// Non-fatal — don't crash proxy for logging failures
|
|
@@ -153,11 +223,7 @@ export async function logRequestAttempt(entry) {
|
|
|
153
223
|
const logFile = join(logDir, `proxy-attempts-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
154
224
|
const line = JSON.stringify(entry) + "\n";
|
|
155
225
|
try {
|
|
156
|
-
await
|
|
157
|
-
mode: 0o600,
|
|
158
|
-
flag: "a",
|
|
159
|
-
signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
|
|
160
|
-
}));
|
|
226
|
+
await appendMetadataRecord(logFile, line, "attempts");
|
|
161
227
|
}
|
|
162
228
|
catch {
|
|
163
229
|
// Non-fatal — don't crash proxy for logging failures
|
|
@@ -615,11 +681,7 @@ export async function logBodyCapture(entry) {
|
|
|
615
681
|
indexEntry.spanId = traceCtx.spanId;
|
|
616
682
|
}
|
|
617
683
|
try {
|
|
618
|
-
await
|
|
619
|
-
mode: 0o600,
|
|
620
|
-
flag: "a",
|
|
621
|
-
signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
|
|
622
|
-
}));
|
|
684
|
+
await appendMetadataRecord(logFile, JSON.stringify(indexEntry) + "\n", "debug");
|
|
623
685
|
}
|
|
624
686
|
catch {
|
|
625
687
|
// Non-fatal
|
|
@@ -691,11 +753,7 @@ export async function logStreamError(entry) {
|
|
|
691
753
|
logEntry.spanId = traceCtx.spanId;
|
|
692
754
|
}
|
|
693
755
|
try {
|
|
694
|
-
await
|
|
695
|
-
mode: 0o600,
|
|
696
|
-
flag: "a",
|
|
697
|
-
signal: AbortSignal.timeout(REQUEST_LOG_IO_TIMEOUT_MS),
|
|
698
|
-
}));
|
|
756
|
+
await appendMetadataRecord(logFile, JSON.stringify(logEntry) + "\n", "requests");
|
|
699
757
|
}
|
|
700
758
|
catch {
|
|
701
759
|
// Non-fatal — don't crash proxy for logging failures
|
|
@@ -50,20 +50,18 @@ export function extractSSEEvents(buffer) {
|
|
|
50
50
|
const rawBlock = buffer.slice(cursor, boundary);
|
|
51
51
|
cursor = boundary + boundaryMatch[0].length;
|
|
52
52
|
let eventType = "";
|
|
53
|
-
|
|
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/generate.d.ts
CHANGED
|
@@ -1490,6 +1490,12 @@ export type TextGenerationResult = {
|
|
|
1490
1490
|
model?: string;
|
|
1491
1491
|
usage?: TokenUsage;
|
|
1492
1492
|
responseTime?: number;
|
|
1493
|
+
/** The executed tool calls of a native turn — the same shape `GenerateResult` exposes. */
|
|
1494
|
+
toolCalls?: Array<{
|
|
1495
|
+
toolCallId: string;
|
|
1496
|
+
toolName: string;
|
|
1497
|
+
args: StandardRecord;
|
|
1498
|
+
}>;
|
|
1493
1499
|
toolsUsed?: string[];
|
|
1494
1500
|
toolExecutions?: Array<{
|
|
1495
1501
|
toolName: string;
|