@tonyclaw/agent-inspector 2.0.18 → 2.0.20
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/.output/cli.js +137 -8
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CepU-dLW.js → CompareDrawer-CUqYtaH3.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CX0-R0nn.js +114 -0
- package/.output/public/assets/ReplayDialog-CPXNEqVg.js +1 -0
- package/.output/public/assets/{RequestAnatomy-C_C6ZU3T.js → RequestAnatomy-qkj8QgkY.js} +1 -1
- package/.output/public/assets/ResponseView-BPNLZpuw.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-DGs-YFXI.js +1 -0
- package/.output/public/assets/_sessionId-CXtbrFTj.js +1 -0
- package/.output/public/assets/index-DrppF281.js +1 -0
- package/.output/public/assets/index-DynnYt7S.css +1 -0
- package/.output/public/assets/{main-BRXsrxtv.js → main-j3vS8rmP.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +141 -118
- package/.output/server/_libs/tanstack__react-virtual.mjs +13 -2
- package/.output/server/_libs/tanstack__virtual-core.mjs +24 -1
- package/.output/server/{_sessionId-B8Bbfs-v.mjs → _sessionId-Bc5Nohhe.mjs} +3 -2
- package/.output/server/_ssr/{CompareDrawer-TgGZLf1J.mjs → CompareDrawer-D0UewydM.mjs} +4 -3
- package/.output/server/_ssr/{ProxyViewerContainer-B_ppaNmQ.mjs → ProxyViewerContainer-BEUWt2eX.mjs} +351 -76
- package/.output/server/_ssr/{ReplayDialog-BXlajnMC.mjs → ReplayDialog-zaN-xMOw.mjs} +199 -17
- package/.output/server/_ssr/{RequestAnatomy-CCrx09-i.mjs → RequestAnatomy-BMomYged.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-BxtMJrov.mjs → ResponseView-DSoHCyAM.mjs} +4 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BlpdSPb8.mjs → StreamingChunkSequence-pt3pZOOL.mjs} +4 -3
- package/.output/server/_ssr/{index-BeTMNSJy.mjs → index-BCTKxcgV.mjs} +3 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Ddl1tuU7.mjs → router-DpaDa5q5.mjs} +872 -232
- package/.output/server/{_tanstack-start-manifest_v-Bvdz_U0f.mjs → _tanstack-start-manifest_v-EsubwO0A.mjs} +1 -1
- package/.output/server/index.mjs +51 -51
- package/README.md +11 -109
- package/package.json +2 -1
- package/src/cli/doctor.ts +150 -5
- package/src/components/OnboardingBanner.tsx +13 -13
- package/src/components/ProxyViewer.tsx +55 -38
- package/src/components/ProxyViewerContainer.tsx +5 -0
- package/src/components/providers/SettingsDialog.tsx +113 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +20 -3
- package/src/components/proxy-viewer/ConversationGroupList.tsx +144 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -2
- package/src/components/proxy-viewer/ReplayDialog.tsx +200 -13
- package/src/components/proxy-viewer/index.ts +1 -1
- package/src/components/ui/scroll-area.tsx +2 -2
- package/src/knowledge/openclawClient.ts +0 -8
- package/src/knowledge/types.ts +0 -2
- package/src/lib/export-logs.ts +72 -5
- package/src/lib/providerTestContract.ts +0 -1
- package/src/lib/useProviders.ts +1 -1
- package/src/mcp/server.ts +14 -6
- package/src/mcp/toolHandlers.ts +21 -4
- package/src/proxy/chunkStorage.ts +2 -4
- package/src/proxy/dataDir.ts +3 -3
- package/src/proxy/formats/anthropic/index.ts +0 -1
- package/src/proxy/formats/anthropic/schemas.ts +0 -2
- package/src/proxy/handler.ts +41 -29
- package/src/proxy/logFinalizer.ts +93 -11
- package/src/proxy/logger.ts +35 -10
- package/src/proxy/providers.ts +12 -5
- package/src/proxy/rawStreamCapture.ts +85 -0
- package/src/proxy/schemas.ts +1 -51
- package/src/proxy/sessionRuntime.ts +0 -9
- package/src/proxy/socketTracker.ts +15 -8
- package/src/proxy/store.ts +414 -13
- package/src/routes/api/logs.$id.replay.ts +66 -1
- package/src/routes/api/logs.stream.ts +11 -3
- package/src/routes/api/logs.ts +51 -17
- package/src/routes/api/providers.$providerId.ts +10 -3
- package/src/routes/api/providers.ts +6 -3
- package/.output/public/assets/ProxyViewerContainer-BG7rc33d.js +0 -114
- package/.output/public/assets/ReplayDialog-DK6SPv8X.js +0 -1
- package/.output/public/assets/ResponseView-Bqpra4Zw.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-CdPbpprq.js +0 -1
- package/.output/public/assets/_sessionId-CsNARHj5.js +0 -1
- package/.output/public/assets/index-37NfO0zx.js +0 -1
- package/.output/public/assets/index-D_nZj9Vt.css +0 -1
- package/src/components/ui/json-expansion-button.tsx +0 -56
package/src/proxy/handler.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { stripClaudeCodeBillingHeader } from "./claudeCodeStrip";
|
|
|
23
23
|
import { stripOpenAIOrphanToolMessages } from "./openaiOrphanToolStrip";
|
|
24
24
|
import { buildFileLogEntry, type FinalizeLogJob } from "./logFinalizer";
|
|
25
25
|
import { enqueueFinalizeLogJob } from "./sessionRuntime";
|
|
26
|
+
import { RawStreamCapture } from "./rawStreamCapture";
|
|
26
27
|
import {
|
|
27
28
|
buildUpstreamUrl,
|
|
28
29
|
describeApiRoute,
|
|
@@ -138,27 +139,50 @@ function handleStreamingResponse(
|
|
|
138
139
|
log.streaming = true;
|
|
139
140
|
log.responseStatus = upstreamRes.status;
|
|
140
141
|
|
|
141
|
-
const
|
|
142
|
+
const capture = new RawStreamCapture();
|
|
142
143
|
const decoder = new TextDecoder();
|
|
144
|
+
let finalizationScheduled = false;
|
|
145
|
+
|
|
146
|
+
const scheduleStreamingFinalization = (type: "streaming" | "stream-abort"): void => {
|
|
147
|
+
if (finalizationScheduled) return;
|
|
148
|
+
finalizationScheduled = true;
|
|
149
|
+
|
|
150
|
+
const elapsedMs = Date.now() - startTime;
|
|
151
|
+
const rawStream = capture.snapshot();
|
|
152
|
+
switch (type) {
|
|
153
|
+
case "streaming":
|
|
154
|
+
scheduleLogFinalization({
|
|
155
|
+
type: "streaming",
|
|
156
|
+
log,
|
|
157
|
+
upstreamUrl,
|
|
158
|
+
elapsedMs,
|
|
159
|
+
responseStatus: upstreamRes.status,
|
|
160
|
+
rawStream,
|
|
161
|
+
collectStreamingChunks,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
case "stream-abort":
|
|
165
|
+
scheduleLogFinalization({
|
|
166
|
+
type: "stream-abort",
|
|
167
|
+
log,
|
|
168
|
+
upstreamUrl,
|
|
169
|
+
elapsedMs,
|
|
170
|
+
rawStream,
|
|
171
|
+
hasChunks: capture.hasChunks,
|
|
172
|
+
collectStreamingChunks,
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
143
177
|
|
|
144
178
|
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
145
|
-
transform(chunk, controller) {
|
|
179
|
+
async transform(chunk, controller) {
|
|
146
180
|
controller.enqueue(chunk);
|
|
147
|
-
|
|
181
|
+
await capture.append(decoder.decode(chunk, { stream: true }));
|
|
148
182
|
},
|
|
149
|
-
flush() {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
scheduleLogFinalization({
|
|
154
|
-
type: "streaming",
|
|
155
|
-
log,
|
|
156
|
-
upstreamUrl,
|
|
157
|
-
elapsedMs,
|
|
158
|
-
responseStatus: upstreamRes.status,
|
|
159
|
-
rawStream: full,
|
|
160
|
-
collectStreamingChunks,
|
|
161
|
-
});
|
|
183
|
+
async flush() {
|
|
184
|
+
await capture.append(decoder.decode());
|
|
185
|
+
scheduleStreamingFinalization("streaming");
|
|
162
186
|
},
|
|
163
187
|
});
|
|
164
188
|
|
|
@@ -171,19 +195,7 @@ function handleStreamingResponse(
|
|
|
171
195
|
req.signal?.addEventListener("abort", () => {
|
|
172
196
|
if (log.responseText === null) {
|
|
173
197
|
logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
|
|
174
|
-
|
|
175
|
-
const full = chunks.join("");
|
|
176
|
-
const hasChunks = chunks.length > 0;
|
|
177
|
-
|
|
178
|
-
scheduleLogFinalization({
|
|
179
|
-
type: "stream-abort",
|
|
180
|
-
log,
|
|
181
|
-
upstreamUrl,
|
|
182
|
-
elapsedMs,
|
|
183
|
-
rawStream: full,
|
|
184
|
-
hasChunks,
|
|
185
|
-
collectStreamingChunks,
|
|
186
|
-
});
|
|
198
|
+
scheduleStreamingFinalization("stream-abort");
|
|
187
199
|
}
|
|
188
200
|
});
|
|
189
201
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
1
2
|
import { Worker } from "node:worker_threads";
|
|
2
3
|
import { writeChunks } from "./chunkStorage";
|
|
3
4
|
import { formatForPath } from "./formats";
|
|
4
5
|
import { appendLogEntry, logger } from "./logger";
|
|
6
|
+
import type { RawStreamSource } from "./rawStreamCapture";
|
|
5
7
|
import type { CapturedLog } from "./schemas";
|
|
6
8
|
import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
|
|
7
9
|
import { finalizeLogUpdate } from "./store";
|
|
@@ -21,13 +23,13 @@ export type FinalizeNonStreamingLogJob = BaseFinalizeLogJob & {
|
|
|
21
23
|
export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
22
24
|
type: "streaming";
|
|
23
25
|
responseStatus: number;
|
|
24
|
-
rawStream: string;
|
|
26
|
+
rawStream: string | RawStreamSource;
|
|
25
27
|
collectStreamingChunks: boolean;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
29
31
|
type: "stream-abort";
|
|
30
|
-
rawStream: string;
|
|
32
|
+
rawStream: string | RawStreamSource;
|
|
31
33
|
hasChunks: boolean;
|
|
32
34
|
collectStreamingChunks: boolean;
|
|
33
35
|
};
|
|
@@ -41,6 +43,7 @@ export type FinalizeLogResult = {
|
|
|
41
43
|
log: CapturedLog;
|
|
42
44
|
upstreamUrl: string;
|
|
43
45
|
error: string | null;
|
|
46
|
+
cleanupRawStreamPath?: string | null;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
|
|
@@ -98,6 +101,7 @@ function finalizeWithError(
|
|
|
98
101
|
fallbackStatus: number,
|
|
99
102
|
fallbackResponseText: string,
|
|
100
103
|
err: unknown,
|
|
104
|
+
cleanupRawStreamPath: string | null = null,
|
|
101
105
|
): FinalizeLogResult {
|
|
102
106
|
const message = errorMessage(err);
|
|
103
107
|
logger.error(`[logFinalizer] Failed to finalize log #${log.id}:`, message);
|
|
@@ -105,7 +109,7 @@ function finalizeWithError(
|
|
|
105
109
|
log.responseText = log.responseText ?? fallbackResponseText;
|
|
106
110
|
log.elapsedMs = job.elapsedMs;
|
|
107
111
|
log.error = message;
|
|
108
|
-
return { log, upstreamUrl: job.upstreamUrl, error: message };
|
|
112
|
+
return { log, upstreamUrl: job.upstreamUrl, error: message, cleanupRawStreamPath };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
function finalizeNonStreaming(
|
|
@@ -134,40 +138,100 @@ function finalizeNonStreaming(
|
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
function cleanupPathForRawStream(rawStream: string | RawStreamSource): string | null {
|
|
142
|
+
if (typeof rawStream === "string") return null;
|
|
143
|
+
switch (rawStream.type) {
|
|
144
|
+
case "memory":
|
|
145
|
+
return null;
|
|
146
|
+
case "file":
|
|
147
|
+
return rawStream.path;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readRawStream(rawStream: string | RawStreamSource): {
|
|
152
|
+
rawStreamText: string;
|
|
153
|
+
cleanupRawStreamPath: string | null;
|
|
154
|
+
} {
|
|
155
|
+
if (typeof rawStream === "string") {
|
|
156
|
+
return { rawStreamText: rawStream, cleanupRawStreamPath: null };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
switch (rawStream.type) {
|
|
160
|
+
case "memory":
|
|
161
|
+
return { rawStreamText: rawStream.rawStream, cleanupRawStreamPath: null };
|
|
162
|
+
case "file":
|
|
163
|
+
return {
|
|
164
|
+
rawStreamText: readFileSync(rawStream.path, "utf-8"),
|
|
165
|
+
cleanupRawStreamPath: rawStream.path,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): FinalizeLogResult {
|
|
138
171
|
const formatHandler = formatForPath(log.path);
|
|
139
172
|
if (formatHandler === null) {
|
|
140
|
-
return finalizeWithError(
|
|
173
|
+
return finalizeWithError(
|
|
174
|
+
job,
|
|
175
|
+
log,
|
|
176
|
+
job.responseStatus,
|
|
177
|
+
"Streaming response unavailable",
|
|
178
|
+
"Unsupported format",
|
|
179
|
+
cleanupPathForRawStream(job.rawStream),
|
|
180
|
+
);
|
|
141
181
|
}
|
|
142
182
|
|
|
183
|
+
let rawStreamText = "";
|
|
184
|
+
let cleanupRawStreamPath: string | null = null;
|
|
143
185
|
try {
|
|
186
|
+
const raw = readRawStream(job.rawStream);
|
|
187
|
+
rawStreamText = raw.rawStreamText;
|
|
188
|
+
cleanupRawStreamPath = raw.cleanupRawStreamPath;
|
|
144
189
|
log.elapsedMs = job.elapsedMs;
|
|
145
190
|
log.responseStatus = job.responseStatus;
|
|
146
191
|
log.responseText = formatHandler.extractStream(
|
|
147
|
-
|
|
192
|
+
rawStreamText,
|
|
148
193
|
log,
|
|
149
194
|
log.model ?? undefined,
|
|
150
195
|
job.collectStreamingChunks,
|
|
151
196
|
);
|
|
152
197
|
persistStreamingChunks(log);
|
|
153
198
|
|
|
154
|
-
return { log, upstreamUrl: job.upstreamUrl, error: null };
|
|
199
|
+
return { log, upstreamUrl: job.upstreamUrl, error: null, cleanupRawStreamPath };
|
|
155
200
|
} catch (err) {
|
|
156
|
-
return finalizeWithError(
|
|
201
|
+
return finalizeWithError(
|
|
202
|
+
job,
|
|
203
|
+
log,
|
|
204
|
+
job.responseStatus,
|
|
205
|
+
rawStreamText === "" ? "Streaming response unavailable" : rawStreamText,
|
|
206
|
+
err,
|
|
207
|
+
cleanupRawStreamPath ?? cleanupPathForRawStream(job.rawStream),
|
|
208
|
+
);
|
|
157
209
|
}
|
|
158
210
|
}
|
|
159
211
|
|
|
160
212
|
function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog): FinalizeLogResult {
|
|
161
213
|
const formatHandler = formatForPath(log.path);
|
|
162
214
|
if (formatHandler === null && job.hasChunks) {
|
|
163
|
-
return finalizeWithError(
|
|
215
|
+
return finalizeWithError(
|
|
216
|
+
job,
|
|
217
|
+
log,
|
|
218
|
+
499,
|
|
219
|
+
"Client aborted",
|
|
220
|
+
"Unsupported format",
|
|
221
|
+
cleanupPathForRawStream(job.rawStream),
|
|
222
|
+
);
|
|
164
223
|
}
|
|
165
224
|
|
|
225
|
+
let rawStreamText = "";
|
|
226
|
+
let cleanupRawStreamPath: string | null = null;
|
|
166
227
|
try {
|
|
167
228
|
log.elapsedMs = job.elapsedMs;
|
|
168
229
|
if (job.hasChunks && formatHandler !== null) {
|
|
230
|
+
const raw = readRawStream(job.rawStream);
|
|
231
|
+
rawStreamText = raw.rawStreamText;
|
|
232
|
+
cleanupRawStreamPath = raw.cleanupRawStreamPath;
|
|
169
233
|
log.responseText = formatHandler.extractStream(
|
|
170
|
-
|
|
234
|
+
rawStreamText,
|
|
171
235
|
log,
|
|
172
236
|
log.model ?? undefined,
|
|
173
237
|
job.collectStreamingChunks,
|
|
@@ -177,9 +241,16 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
177
241
|
log.responseText = "Client aborted";
|
|
178
242
|
}
|
|
179
243
|
|
|
180
|
-
return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted" };
|
|
244
|
+
return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted", cleanupRawStreamPath };
|
|
181
245
|
} catch (err) {
|
|
182
|
-
return finalizeWithError(
|
|
246
|
+
return finalizeWithError(
|
|
247
|
+
job,
|
|
248
|
+
log,
|
|
249
|
+
499,
|
|
250
|
+
rawStreamText === "" ? "Client aborted" : rawStreamText,
|
|
251
|
+
err,
|
|
252
|
+
cleanupRawStreamPath ?? cleanupPathForRawStream(job.rawStream),
|
|
253
|
+
);
|
|
183
254
|
}
|
|
184
255
|
}
|
|
185
256
|
|
|
@@ -199,6 +270,17 @@ export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
|
|
|
199
270
|
export function commitFinalizeLogResult(result: FinalizeLogResult): void {
|
|
200
271
|
appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: result.error });
|
|
201
272
|
finalizeLogUpdate(result.log);
|
|
273
|
+
if (result.cleanupRawStreamPath !== undefined && result.cleanupRawStreamPath !== null) {
|
|
274
|
+
try {
|
|
275
|
+
rmSync(result.cleanupRawStreamPath, { force: true });
|
|
276
|
+
} catch (err) {
|
|
277
|
+
logger.warn(
|
|
278
|
+
"[logFinalizer] Failed to remove raw stream temp file:",
|
|
279
|
+
result.cleanupRawStreamPath,
|
|
280
|
+
errorMessage(err),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
202
284
|
}
|
|
203
285
|
|
|
204
286
|
// ── Worker pool ────────────────────────────────────────────────
|
package/src/proxy/logger.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { getDataDir } from "./dataDir";
|
|
|
5
5
|
|
|
6
6
|
const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
|
|
7
7
|
|
|
8
|
+
function writeStderrFallback(message: string, error: unknown): void {
|
|
9
|
+
process.stderr.write(`${message} ${String(error)}\n`);
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
export function resolveLogDir(): string {
|
|
9
13
|
const logDirEnv = process.env["LOG_DIR"];
|
|
10
14
|
if (logDirEnv !== undefined && logDirEnv !== "") {
|
|
@@ -51,8 +55,7 @@ export async function initLogger(): Promise<void> {
|
|
|
51
55
|
}
|
|
52
56
|
} catch (err) {
|
|
53
57
|
// Log directory initialization errors but don't fail startup
|
|
54
|
-
|
|
55
|
-
console.error("[logger] Failed to initialize log directory:", err);
|
|
58
|
+
writeStderrFallback("[logger] Failed to initialize log directory:", err);
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -74,8 +77,7 @@ async function writeAppLog(message: string): Promise<void> {
|
|
|
74
77
|
await appendFile(logPath, `[${timestamp}] ${message}\n`, "utf-8");
|
|
75
78
|
} catch (err) {
|
|
76
79
|
// Log to stderr since file logging failed
|
|
77
|
-
|
|
78
|
-
console.error(`[logger] Failed to write to ${logPath}:`, err);
|
|
80
|
+
writeStderrFallback(`[logger] Failed to write to ${logPath}:`, err);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -103,6 +105,22 @@ export const logger = {
|
|
|
103
105
|
const MAX_BUFFER_SIZE = 1000;
|
|
104
106
|
let writeBuffer: string[] = [];
|
|
105
107
|
let writeQueue: Promise<void> = Promise.resolve();
|
|
108
|
+
let logWriteLock: Promise<void> = Promise.resolve();
|
|
109
|
+
|
|
110
|
+
export async function runWithLogWriteLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
111
|
+
const previousLock = logWriteLock;
|
|
112
|
+
let releaseLock = (): void => {};
|
|
113
|
+
logWriteLock = new Promise<void>((resolve) => {
|
|
114
|
+
releaseLock = resolve;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await previousLock;
|
|
118
|
+
try {
|
|
119
|
+
return await fn();
|
|
120
|
+
} finally {
|
|
121
|
+
releaseLock();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
106
124
|
|
|
107
125
|
function drainBuffer(): string {
|
|
108
126
|
const toWrite = writeBuffer.join("");
|
|
@@ -110,7 +128,7 @@ function drainBuffer(): string {
|
|
|
110
128
|
return toWrite;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
|
-
async function
|
|
131
|
+
async function flushWriteBufferUnlocked(): Promise<void> {
|
|
114
132
|
let toWrite: string;
|
|
115
133
|
{
|
|
116
134
|
toWrite = drainBuffer();
|
|
@@ -124,8 +142,7 @@ async function flushWriteBuffer(): Promise<void> {
|
|
|
124
142
|
} catch (err) {
|
|
125
143
|
// On failure, re-queue the data so it is not lost
|
|
126
144
|
writeBuffer.unshift(toWrite);
|
|
127
|
-
|
|
128
|
-
console.error("[logger] Failed to flush write buffer, re-queued:", err);
|
|
145
|
+
writeStderrFallback("[logger] Failed to flush write buffer, re-queued:", err);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
|
|
@@ -133,9 +150,9 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
|
|
|
133
150
|
const line = JSON.stringify(entry) + "\n";
|
|
134
151
|
writeBuffer.push(line);
|
|
135
152
|
if (writeBuffer.length >= MAX_BUFFER_SIZE) {
|
|
136
|
-
writeQueue = writeQueue.then(() =>
|
|
153
|
+
writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
|
|
137
154
|
} else if (writeBuffer.length === 1) {
|
|
138
|
-
writeQueue = writeQueue.then(() =>
|
|
155
|
+
writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
|
|
139
156
|
}
|
|
140
157
|
}
|
|
141
158
|
|
|
@@ -146,10 +163,18 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
|
|
|
146
163
|
export async function flushLogBuffer(): Promise<void> {
|
|
147
164
|
await writeQueue;
|
|
148
165
|
if (writeBuffer.length > 0) {
|
|
149
|
-
await
|
|
166
|
+
await runWithLogWriteLock(flushWriteBufferUnlocked);
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
|
|
170
|
+
export async function runWithFlushedLogWriteLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
171
|
+
await flushLogBuffer();
|
|
172
|
+
return await runWithLogWriteLock(async () => {
|
|
173
|
+
await flushWriteBufferUnlocked();
|
|
174
|
+
return await fn();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
// Ensure buffered logs are written before the process exits
|
|
154
179
|
process.on("exit", () => {
|
|
155
180
|
if (writeBuffer.length > 0) {
|
package/src/proxy/providers.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Conf from "conf";
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { maskApiKey } from "../lib/mask";
|
|
6
7
|
import { logger } from "./logger";
|
|
7
8
|
import { getDataDir, hasExplicitDataDir } from "./dataDir";
|
|
8
9
|
import {
|
|
@@ -380,6 +381,17 @@ export function getProvider(id: string): ProviderConfig | undefined {
|
|
|
380
381
|
return providers.find((p) => p.id === id);
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
export function redactProvider(provider: ProviderConfig): ProviderConfig {
|
|
385
|
+
return {
|
|
386
|
+
...provider,
|
|
387
|
+
apiKey: maskApiKey(provider.apiKey),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function redactProviders(providers: readonly ProviderConfig[]): ProviderConfig[] {
|
|
392
|
+
return providers.map(redactProvider);
|
|
393
|
+
}
|
|
394
|
+
|
|
383
395
|
/**
|
|
384
396
|
* Normalizes an API key by stripping "Bearer " prefix if present.
|
|
385
397
|
*/
|
|
@@ -630,11 +642,6 @@ export function importProviders(json: string): { imported: number; errors: strin
|
|
|
630
642
|
return { imported, errors };
|
|
631
643
|
}
|
|
632
644
|
|
|
633
|
-
function maskApiKey(apiKey: string): string {
|
|
634
|
-
if (apiKey.length <= 8) return "••••••••";
|
|
635
|
-
return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
645
|
/**
|
|
639
646
|
* Converts display model name to API usage name based on provider-specific rules.
|
|
640
647
|
* Default behavior: returns model as-is.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, appendFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getDataDir } from "./dataDir";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MEMORY_LIMIT_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export type RawStreamSource =
|
|
10
|
+
| {
|
|
11
|
+
type: "memory";
|
|
12
|
+
rawStream: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
type: "file";
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function resolveMemoryLimitBytes(): number {
|
|
20
|
+
const raw = process.env["AGENT_INSPECTOR_STREAM_MEMORY_LIMIT_BYTES"];
|
|
21
|
+
if (raw === undefined || raw === "") return DEFAULT_MEMORY_LIMIT_BYTES;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed < 0) return DEFAULT_MEMORY_LIMIT_BYTES;
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getRawStreamDir(): string {
|
|
28
|
+
return join(getDataDir(), "raw-streams");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createRawStreamPath(): string {
|
|
32
|
+
return join(getRawStreamDir(), `${Date.now()}-${randomUUID()}.stream.tmp`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RawStreamCapture {
|
|
36
|
+
private readonly memoryLimitBytes: number;
|
|
37
|
+
private chunks: string[] = [];
|
|
38
|
+
private byteLength = 0;
|
|
39
|
+
private filePath: string | null = null;
|
|
40
|
+
private sawChunk = false;
|
|
41
|
+
|
|
42
|
+
constructor(memoryLimitBytes: number = resolveMemoryLimitBytes()) {
|
|
43
|
+
this.memoryLimitBytes = memoryLimitBytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get hasChunks(): boolean {
|
|
47
|
+
return this.sawChunk;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async append(text: string): Promise<void> {
|
|
51
|
+
if (text === "") return;
|
|
52
|
+
this.sawChunk = true;
|
|
53
|
+
|
|
54
|
+
const nextBytes = Buffer.byteLength(text, "utf-8");
|
|
55
|
+
if (this.filePath === null && this.byteLength + nextBytes <= this.memoryLimitBytes) {
|
|
56
|
+
this.chunks.push(text);
|
|
57
|
+
this.byteLength += nextBytes;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.filePath === null) {
|
|
62
|
+
await this.spillToFile();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.filePath !== null) {
|
|
66
|
+
await appendFile(this.filePath, text, "utf-8");
|
|
67
|
+
this.byteLength += nextBytes;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
snapshot(): RawStreamSource {
|
|
72
|
+
if (this.filePath !== null) {
|
|
73
|
+
return { type: "file", path: this.filePath };
|
|
74
|
+
}
|
|
75
|
+
return { type: "memory", rawStream: this.chunks.join("") };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async spillToFile(): Promise<void> {
|
|
79
|
+
const path = createRawStreamPath();
|
|
80
|
+
await mkdir(getRawStreamDir(), { recursive: true });
|
|
81
|
+
await writeFile(path, this.chunks.join(""), "utf-8");
|
|
82
|
+
this.filePath = path;
|
|
83
|
+
this.chunks = [];
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/proxy/schemas.ts
CHANGED
|
@@ -8,8 +8,6 @@ export type { JsonValue };
|
|
|
8
8
|
|
|
9
9
|
export type RequestFormat = "anthropic" | "openai" | "unknown";
|
|
10
10
|
|
|
11
|
-
import { safeGetOwnProperty } from "../lib/objectUtils";
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
12
|
* A single SSE event from a streaming response.
|
|
15
13
|
*/
|
|
@@ -50,6 +48,7 @@ export const CapturedLogSchema = z.object({
|
|
|
50
48
|
headers: z.record(z.string(), z.string()).optional(),
|
|
51
49
|
apiFormat: z.enum(["anthropic", "openai", "unknown"]).default("unknown"),
|
|
52
50
|
isTest: z.boolean().optional().default(false),
|
|
51
|
+
replayOfLogId: z.number().nullable().optional(),
|
|
53
52
|
providerName: z.string().nullable().optional(),
|
|
54
53
|
clientPort: z.number().nullable().optional(),
|
|
55
54
|
clientPid: z.number().nullable().optional(),
|
|
@@ -74,9 +73,6 @@ export type TokenUsage = {
|
|
|
74
73
|
// Import types and schemas from formats
|
|
75
74
|
// ============================================================
|
|
76
75
|
|
|
77
|
-
// Import types and schemas from formats for internal use
|
|
78
|
-
import { InspectorRequestSchema, type InspectorRequest } from "./formats/anthropic/schemas";
|
|
79
|
-
|
|
80
76
|
// Re-export types and schemas from formats for backward compatibility
|
|
81
77
|
export {
|
|
82
78
|
InspectorRequestSchema,
|
|
@@ -84,7 +80,6 @@ export {
|
|
|
84
80
|
SseEventSchema,
|
|
85
81
|
} from "./formats/anthropic/schemas";
|
|
86
82
|
export type {
|
|
87
|
-
InspectorRequest,
|
|
88
83
|
InspectorResponse,
|
|
89
84
|
ResponseContentBlockType,
|
|
90
85
|
} from "./formats/anthropic/schemas";
|
|
@@ -98,35 +93,6 @@ export {
|
|
|
98
93
|
parseOpenAIResponse,
|
|
99
94
|
} from "./formats/openai/schemas";
|
|
100
95
|
|
|
101
|
-
// ============================================================
|
|
102
|
-
// Utility functions
|
|
103
|
-
// ============================================================
|
|
104
|
-
|
|
105
|
-
function detectFormat(rawBody: string | null): RequestFormat {
|
|
106
|
-
if (rawBody === null) return "unknown";
|
|
107
|
-
try {
|
|
108
|
-
const json: unknown = JSON.parse(rawBody);
|
|
109
|
-
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
110
|
-
const hasModel = safeGetOwnProperty(json, "model") !== undefined;
|
|
111
|
-
const hasMessages = safeGetOwnProperty(json, "messages") !== undefined;
|
|
112
|
-
if (hasModel && hasMessages) {
|
|
113
|
-
// Anthropic requests put `system` as a top-level key; OpenAI does not.
|
|
114
|
-
// Both formats can have `tools`, so we check `system` as the discriminator.
|
|
115
|
-
if (safeGetOwnProperty(json, "system") !== undefined) {
|
|
116
|
-
return "anthropic";
|
|
117
|
-
}
|
|
118
|
-
const msgVal = safeGetOwnProperty(json, "messages");
|
|
119
|
-
if (Array.isArray(msgVal)) {
|
|
120
|
-
return "openai";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return "unknown";
|
|
125
|
-
} catch {
|
|
126
|
-
return "unknown";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
96
|
// Schema for model name in request body (used for routing)
|
|
131
97
|
const RequestModelSchema = z.object({
|
|
132
98
|
model: z.string(),
|
|
@@ -191,19 +157,3 @@ export function extractRequestMetadata(body: string | null, headers: Headers): R
|
|
|
191
157
|
}
|
|
192
158
|
return { model: null, sessionId: headerSessionId };
|
|
193
159
|
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Parse a raw request body into an InspectorRequest.
|
|
197
|
-
* @deprecated Use FormatHandler.parseRequest() instead
|
|
198
|
-
*/
|
|
199
|
-
export function parseRequest(rawBody: string | null): InspectorRequest | null {
|
|
200
|
-
if (rawBody === null) return null;
|
|
201
|
-
try {
|
|
202
|
-
const json: unknown = JSON.parse(rawBody);
|
|
203
|
-
const result = InspectorRequestSchema.safeParse(json);
|
|
204
|
-
if (result.success) return result.data;
|
|
205
|
-
return null;
|
|
206
|
-
} catch {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { logger } from "./logger";
|
|
2
2
|
import { executeFinalizeLogJob, type FinalizeLogJob } from "./logFinalizer";
|
|
3
|
-
import type { CapturedLog } from "./schemas";
|
|
4
3
|
import {
|
|
5
4
|
getLogSessionId,
|
|
6
5
|
markSessionTaskFinished,
|
|
@@ -70,14 +69,6 @@ export function enqueueSessionTask<T>(
|
|
|
70
69
|
return current;
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
export function enqueueLogTask<T>(
|
|
74
|
-
log: CapturedLog,
|
|
75
|
-
taskName: SessionTaskName,
|
|
76
|
-
task: SessionTask<T>,
|
|
77
|
-
): Promise<T> {
|
|
78
|
-
return enqueueSessionTask(getLogSessionId(log), taskName, task);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
72
|
export function enqueueFinalizeLogJob(job: FinalizeLogJob): Promise<void> {
|
|
82
73
|
return enqueueSessionTask(getLogSessionId(job.log), "finalize-log", () =>
|
|
83
74
|
executeFinalizeLogJob(job),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exec, execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { logger } from "./logger";
|
|
4
5
|
|
|
5
6
|
const execAsync = promisify(exec);
|
|
@@ -20,6 +21,12 @@ type CacheEntry = ClientInfo & {
|
|
|
20
21
|
const cache = new Map<number, CacheEntry>();
|
|
21
22
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
22
23
|
const MAX_CACHE_SIZE = 200;
|
|
24
|
+
const RemotePortSchema = z.number().int().min(1).max(65_535);
|
|
25
|
+
const SocketSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
remotePort: RemotePortSchema.nullish(),
|
|
28
|
+
})
|
|
29
|
+
.passthrough();
|
|
23
30
|
|
|
24
31
|
// Deduplicate concurrent lookups for the same port
|
|
25
32
|
const inflight = new Map<number, Promise<ClientInfo>>();
|
|
@@ -50,14 +57,14 @@ function setCache(port: number, info: ClientInfo): void {
|
|
|
50
57
|
* Get the remote port from a Request's underlying socket.
|
|
51
58
|
* Works with Bun, Node.js, and Nitro's event.node.req.
|
|
52
59
|
*/
|
|
53
|
-
function extractRemotePort(request: Request): number | null {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (remotePort
|
|
60
|
-
return
|
|
60
|
+
export function extractRemotePort(request: Request): number | null {
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(request, "socket");
|
|
62
|
+
if (descriptor === undefined || descriptor.get !== undefined) return null;
|
|
63
|
+
const parsed = SocketSchema.safeParse(descriptor.value);
|
|
64
|
+
if (!parsed.success) return null;
|
|
65
|
+
const remotePort = parsed.data.remotePort;
|
|
66
|
+
if (remotePort === undefined || remotePort === null) return null;
|
|
67
|
+
return remotePort;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|