@juspay/neurolink 12.14.7 → 12.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/dist/adapters/providerImageAdapter.js +1 -0
- package/dist/browser/neurolink.min.js +379 -379
- package/dist/cli/commands/proxy.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/mcp/servers/aiProviders/aiAnalysisTools.js +43 -30
- package/dist/neurolink.d.ts +4 -1
- package/dist/neurolink.js +41 -6
- package/dist/providers/azureOpenai.js +1 -1
- package/dist/providers/catalog/loader.d.ts +1 -1
- package/dist/providers/catalog/loader.js +30 -1
- package/dist/providers/cohere.js +1 -1
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/googleAiStudio/client.js +6 -5
- package/dist/providers/googleVertex/client.d.ts +11 -30
- package/dist/providers/googleVertex/client.js +49 -131
- package/dist/providers/huggingFace/client.js +1 -1
- package/dist/providers/llamaCpp.js +1 -1
- package/dist/providers/lmStudio.js +1 -1
- package/dist/providers/nvidiaNim/client.js +1 -1
- package/dist/providers/ollama/client.js +1 -1
- package/dist/providers/openRouter/client.js +1 -1
- package/dist/providers/openaiCompatible/client.js +1 -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/dist/utils/timeout.d.ts +13 -2
- package/dist/utils/timeout.js +14 -3
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/proxy-telemetry-check.mjs +39 -5
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fstatSync } from "node:fs";
|
|
2
|
+
import { getProxyOtelLogSnapshot, isProxyOtelOnly } from "./otelLogSink.js";
|
|
3
|
+
import { getProxyLifecycleLoggerSnapshot } from "./proxyLifecycle.js";
|
|
4
|
+
/** Inspect this process, not the launcher configuration intended for its successor. */
|
|
5
|
+
export function getProxyProcessTelemetry() {
|
|
6
|
+
const descriptor = (fd) => {
|
|
7
|
+
try {
|
|
8
|
+
return fstatSync(fd).isFile() ? "file" : "non_file";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "unavailable";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const logs = getProxyOtelLogSnapshot();
|
|
15
|
+
return {
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
checkedAt: new Date().toISOString(),
|
|
18
|
+
configuredSink: isProxyOtelOnly() ? "otel" : "file",
|
|
19
|
+
lifecycleSink: getProxyLifecycleLoggerSnapshot().sink ?? "unavailable",
|
|
20
|
+
otelInitialized: logs.initialized,
|
|
21
|
+
stdio: { stdout: descriptor(1), stderr: descriptor(2) },
|
|
22
|
+
exportDropped: logs.queues.reduce((n, q) => n + q.dropped, 0),
|
|
23
|
+
exportUnconfirmed: logs.queues.reduce((n, q) => n + q.exportUnconfirmed, 0),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Maximum bytes to capture before stopping accumulation (1 MB). */
|
|
2
2
|
const MAX_CAPTURE_BYTES = 1024 * 1024;
|
|
3
|
+
// Payload accounting across concurrent stream observers, independent of the
|
|
4
|
+
// worker's clone/publication pool. Never retain an unbounded number of prefixes.
|
|
5
|
+
const MAX_PENDING_CAPTURE_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
let pendingCaptureBytes = 0;
|
|
3
7
|
const TRUNCATION_MARKER = "\n...[TRUNCATED]";
|
|
4
8
|
export function createRawStreamCapture() {
|
|
5
9
|
const decoder = new TextDecoder();
|
|
@@ -17,21 +21,13 @@ export function createRawStreamCapture() {
|
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
23
|
resolved = true;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
chunks.push(finalChunk.slice(0, remainingBytes));
|
|
25
|
-
capturedBytes += Math.min(finalChunk.length, remainingBytes);
|
|
26
|
-
}
|
|
27
|
-
else if (!truncated) {
|
|
28
|
-
chunks.push(TRUNCATION_MARKER);
|
|
29
|
-
truncated = true;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
24
|
+
const text = chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") +
|
|
25
|
+
(truncated ? "" : decoder.decode());
|
|
26
|
+
pendingCaptureBytes -= capturedBytes;
|
|
27
|
+
chunks.length = 0;
|
|
32
28
|
resolveCapture({
|
|
33
29
|
totalBytes,
|
|
34
|
-
text:
|
|
30
|
+
text: text + (truncated ? TRUNCATION_MARKER : ""),
|
|
35
31
|
truncated,
|
|
36
32
|
});
|
|
37
33
|
}
|
|
@@ -39,21 +35,16 @@ export function createRawStreamCapture() {
|
|
|
39
35
|
transform(chunk, controller) {
|
|
40
36
|
controller.enqueue(chunk);
|
|
41
37
|
totalBytes += chunk.byteLength;
|
|
42
|
-
if (capturedBytes < MAX_CAPTURE_BYTES) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const slice =
|
|
38
|
+
if (!truncated && capturedBytes < MAX_CAPTURE_BYTES) {
|
|
39
|
+
const remainingBytes = Math.min(MAX_CAPTURE_BYTES - capturedBytes, Math.max(0, MAX_PENDING_CAPTURE_BYTES - pendingCaptureBytes));
|
|
40
|
+
// Buffer.slice is a view; copy to avoid retaining the whole wire chunk.
|
|
41
|
+
const slice = Uint8Array.from(chunk.subarray(0, remainingBytes));
|
|
46
42
|
chunks.push(slice);
|
|
47
|
-
capturedBytes +=
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
truncated = true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
else if (!truncated) {
|
|
54
|
-
chunks.push(TRUNCATION_MARKER);
|
|
55
|
-
truncated = true;
|
|
43
|
+
capturedBytes += slice.byteLength;
|
|
44
|
+
pendingCaptureBytes += slice.byteLength;
|
|
45
|
+
truncated = slice.byteLength < chunk.byteLength;
|
|
56
46
|
}
|
|
47
|
+
truncated ||= totalBytes > MAX_CAPTURE_BYTES;
|
|
57
48
|
},
|
|
58
49
|
flush() {
|
|
59
50
|
settle();
|
|
@@ -35,6 +35,8 @@ export declare function prepareProxyBodyForLogging(body: unknown): {
|
|
|
35
35
|
truncated: boolean;
|
|
36
36
|
originalBytes?: number;
|
|
37
37
|
};
|
|
38
|
+
/** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
|
|
39
|
+
export declare function isProxyBodyCaptureEnabled(): boolean;
|
|
38
40
|
/** Capture an owned request body with bounded processing and tracked index/export publication. */
|
|
39
41
|
export declare function logBodyCapture(entry: ProxyBodyCaptureEntry): Promise<void>;
|
|
40
42
|
/**
|
|
@@ -514,9 +514,13 @@ function emitOtlpBodyLogRecord(entry, stored) {
|
|
|
514
514
|
return undefined;
|
|
515
515
|
});
|
|
516
516
|
}
|
|
517
|
+
/** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
|
|
518
|
+
export function isProxyBodyCaptureEnabled() {
|
|
519
|
+
return logEnabled && (Boolean(logDir) || isProxyOtelOnly());
|
|
520
|
+
}
|
|
517
521
|
/** Capture an owned request body with bounded processing and tracked index/export publication. */
|
|
518
522
|
export async function logBodyCapture(entry) {
|
|
519
|
-
if (!
|
|
523
|
+
if (!isProxyBodyCaptureEnabled()) {
|
|
520
524
|
return;
|
|
521
525
|
}
|
|
522
526
|
// Borrowed traffic is somebody else's conversation. Capturing it would leave
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProxyRestartControlIdentity, ProxyRestartControlOptions, ProxyRestartResult } from "../types/index.js";
|
|
1
|
+
import type { ProxyRestartControlIdentity, ProxyRestartControlOptions, ProxyRestartResult, ProxySupervisorTelemetry } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Own restart completion in the supervisor, so a disconnected CLI cannot leave
|
|
4
4
|
* admission closed. The control socket is private to the service's OS user.
|
|
@@ -8,5 +8,7 @@ export declare function startProxyRestartControl(options: ProxyRestartControlOpt
|
|
|
8
8
|
identity: ProxyRestartControlIdentity;
|
|
9
9
|
close: () => Promise<void>;
|
|
10
10
|
}>;
|
|
11
|
+
/** Query the private supervisor endpoint with a short, bounded read; never restart. */
|
|
12
|
+
export declare function requestProxySupervisorTelemetry(identity: ProxyRestartControlIdentity | undefined, expectedPid: number): Promise<ProxySupervisorTelemetry>;
|
|
11
13
|
/** Perform one local control operation; never fall back to killing a process. */
|
|
12
14
|
export declare function requestProxyRestart(identity: ProxyRestartControlIdentity, check: boolean): Promise<ProxyRestartResult>;
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { createServer, request } from "node:http";
|
|
2
|
-
import { chmod, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, rm, lstat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
const processTelemetrySchema = z.object({
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
checkedAt: z.string().datetime(),
|
|
9
|
+
configuredSink: z.enum(["otel", "file"]),
|
|
10
|
+
lifecycleSink: z.string(),
|
|
11
|
+
otelInitialized: z.boolean(),
|
|
12
|
+
stdio: z.object({
|
|
13
|
+
stdout: z.enum(["file", "non_file", "unavailable"]),
|
|
14
|
+
stderr: z.enum(["file", "non_file", "unavailable"]),
|
|
15
|
+
}),
|
|
16
|
+
exportDropped: z.number().int().nonnegative(),
|
|
17
|
+
exportUnconfirmed: z.number().int().nonnegative(),
|
|
18
|
+
});
|
|
19
|
+
/** A worker-only restart cannot repair inherited supervisor log descriptors. */
|
|
20
|
+
function assertSupervisorLogging(status, snapshot) {
|
|
21
|
+
if (status.observability.requestLogs.diskEnabled === false &&
|
|
22
|
+
(!snapshot ||
|
|
23
|
+
snapshot.configuredSink !== "otel" ||
|
|
24
|
+
snapshot.lifecycleSink !== "otel" ||
|
|
25
|
+
!snapshot.otelInitialized ||
|
|
26
|
+
snapshot.stdio.stdout !== "non_file" ||
|
|
27
|
+
snapshot.stdio.stderr !== "non_file")) {
|
|
28
|
+
throw new Error(snapshot
|
|
29
|
+
? "Supervisor logging is not OTel-only. Worker replacement cannot complete the logging cutover; activate the configured supervisor service first."
|
|
30
|
+
: "Supervisor telemetry is unavailable, so OTel-only logging cannot be proven. Activate the configured supervisor service first.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
const statusSchema = z.object({
|
|
7
34
|
pid: z.number().int().positive(),
|
|
8
35
|
version: z.string(),
|
|
@@ -99,6 +126,7 @@ export async function startProxyRestartControl(options) {
|
|
|
99
126
|
}
|
|
100
127
|
previousWorkerPid = active.pid;
|
|
101
128
|
const status = statusSchema.parse(await options.getStatus());
|
|
129
|
+
assertSupervisorLogging(status, options.getTelemetry?.());
|
|
102
130
|
if (status.pid !== previousWorkerPid ||
|
|
103
131
|
status.version !== active.version) {
|
|
104
132
|
throw new Error("Serving worker identity changed during preflight; retry the check.");
|
|
@@ -181,6 +209,26 @@ export async function startProxyRestartControl(options) {
|
|
|
181
209
|
res.writeHead(400, { connection: "close" }).end();
|
|
182
210
|
return;
|
|
183
211
|
}
|
|
212
|
+
if (req.method === "GET" && req.url === "/telemetry") {
|
|
213
|
+
req.resume();
|
|
214
|
+
try {
|
|
215
|
+
const snapshot = options.getTelemetry?.();
|
|
216
|
+
if (!snapshot) {
|
|
217
|
+
res.writeHead(404).end();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const payload = JSON.stringify(snapshot);
|
|
221
|
+
res.writeHead(200, {
|
|
222
|
+
"content-type": "application/json",
|
|
223
|
+
connection: "close",
|
|
224
|
+
});
|
|
225
|
+
res.end(payload);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
res.writeHead(503, { connection: "close" }).end();
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
184
232
|
if (!((req.method === "GET" && req.url === "/check") ||
|
|
185
233
|
(req.method === "POST" && req.url === "/restart"))) {
|
|
186
234
|
res.writeHead(404).end();
|
|
@@ -238,6 +286,75 @@ export async function startProxyRestartControl(options) {
|
|
|
238
286
|
},
|
|
239
287
|
};
|
|
240
288
|
}
|
|
289
|
+
/** Query the private supervisor endpoint with a short, bounded read; never restart. */
|
|
290
|
+
export async function requestProxySupervisorTelemetry(identity, expectedPid) {
|
|
291
|
+
if (!identity) {
|
|
292
|
+
return { status: "unavailable", reason: "supervisor_upgrade_required" };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
if (!identity.socketPath.endsWith(`/restart-${expectedPid}-${identity.instanceId}.sock`)) {
|
|
296
|
+
throw new Error("identity");
|
|
297
|
+
}
|
|
298
|
+
const socket = await lstat(identity.socketPath);
|
|
299
|
+
if (!socket.isSocket() ||
|
|
300
|
+
(socket.mode & 0o077) !== 0 ||
|
|
301
|
+
(process.getuid && socket.uid !== process.getuid())) {
|
|
302
|
+
throw new Error("socket");
|
|
303
|
+
}
|
|
304
|
+
const snapshot = await new Promise((resolve, reject) => {
|
|
305
|
+
const req = request({
|
|
306
|
+
socketPath: identity.socketPath,
|
|
307
|
+
path: "/telemetry",
|
|
308
|
+
method: "GET",
|
|
309
|
+
headers: {
|
|
310
|
+
"x-neurolink-instance": identity.instanceId,
|
|
311
|
+
connection: "close",
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const timer = setTimeout(() => req.destroy(new Error("timeout")), 1000);
|
|
315
|
+
req.once("error", (error) => {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
reject(error);
|
|
318
|
+
});
|
|
319
|
+
req.once("response", (res) => {
|
|
320
|
+
let body = "";
|
|
321
|
+
res.setEncoding("utf8");
|
|
322
|
+
res.on("data", (chunk) => {
|
|
323
|
+
body += chunk;
|
|
324
|
+
if (body.length > 8192) {
|
|
325
|
+
req.destroy(new Error("size"));
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
res.once("error", (error) => {
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
reject(error);
|
|
331
|
+
});
|
|
332
|
+
res.once("end", () => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
try {
|
|
335
|
+
if (res.statusCode !== 200) {
|
|
336
|
+
throw new Error("unsupported");
|
|
337
|
+
}
|
|
338
|
+
const value = processTelemetrySchema.parse(JSON.parse(body));
|
|
339
|
+
if (value.pid !== expectedPid ||
|
|
340
|
+
Math.abs(Date.now() - Date.parse(value.checkedAt)) > 5000) {
|
|
341
|
+
throw new Error("stale");
|
|
342
|
+
}
|
|
343
|
+
resolve(value);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
reject(error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
req.end();
|
|
351
|
+
});
|
|
352
|
+
return { status: "available", process: snapshot };
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return { status: "unavailable", reason: "supervisor_telemetry_unverified" };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
241
358
|
/** Perform one local control operation; never fall back to killing a process. */
|
|
242
359
|
export async function requestProxyRestart(identity, check) {
|
|
243
360
|
return new Promise((resolve, reject) => {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* upstreamResponse.body.pipeThrough(stream).pipeTo(clientWritable);
|
|
16
16
|
* const data = await telemetry; // resolves on stream end
|
|
17
17
|
*/
|
|
18
|
+
import { hasUsefulClaudeContent } from "./claudeOutputObservation.js";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Constants
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -172,6 +173,8 @@ function finalize(acc) {
|
|
|
172
173
|
messageId: acc.messageId,
|
|
173
174
|
messageStopReceived: acc.messageStopReceived,
|
|
174
175
|
firstUsefulOutputAt: acc.firstUsefulOutputAt,
|
|
176
|
+
firstUsefulOutputEvent: acc.firstUsefulOutputEvent,
|
|
177
|
+
observationIncomplete: acc.observationIncomplete,
|
|
175
178
|
model: acc.model,
|
|
176
179
|
usage: {
|
|
177
180
|
inputTokens: acc.inputTokens,
|
|
@@ -191,6 +194,7 @@ function finalize(acc) {
|
|
|
191
194
|
? { streamErrorMessage: acc.streamErrorMessage }
|
|
192
195
|
: {}),
|
|
193
196
|
...(acc.rawTextChunks ? { rawText: acc.rawTextChunks.join("") } : {}),
|
|
197
|
+
rawTextTruncated: acc.rawTextTruncated,
|
|
194
198
|
};
|
|
195
199
|
}
|
|
196
200
|
// ---------------------------------------------------------------------------
|
|
@@ -320,12 +324,17 @@ function processEvent(acc, event) {
|
|
|
320
324
|
if (!event.data) {
|
|
321
325
|
return;
|
|
322
326
|
}
|
|
327
|
+
if (event.data.length > 1024 * 1024) {
|
|
328
|
+
acc.observationIncomplete = true;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
323
331
|
let parsed;
|
|
324
332
|
try {
|
|
325
333
|
parsed = JSON.parse(event.data);
|
|
326
334
|
}
|
|
327
335
|
catch {
|
|
328
336
|
// Malformed JSON — skip silently, bytes already forwarded to client
|
|
337
|
+
acc.observationIncomplete = true;
|
|
329
338
|
return;
|
|
330
339
|
}
|
|
331
340
|
const payloadType = parsed && typeof parsed === "object" && "type" in parsed
|
|
@@ -357,11 +366,52 @@ function processEvent(acc, event) {
|
|
|
357
366
|
typeof delta === "object" &&
|
|
358
367
|
(("text" in delta &&
|
|
359
368
|
typeof delta.text === "string" &&
|
|
360
|
-
delta.text.length > 0) ||
|
|
369
|
+
delta.text.trim().length > 0) ||
|
|
361
370
|
("partial_json" in delta &&
|
|
362
371
|
typeof delta.partial_json === "string" &&
|
|
363
|
-
delta.partial_json.length > 0)
|
|
364
|
-
|
|
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 } : {}),
|