@tonyclaw/agent-inspector 2.0.17 → 2.0.19
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 +149 -40
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DDg7Cs4N.js → CompareDrawer-D-90PIPw.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CxRDWacg.js +114 -0
- package/.output/public/assets/ReplayDialog-DzlLYoTO.js +1 -0
- package/.output/public/assets/{RequestAnatomy-DR8OICOJ.js → RequestAnatomy-0hbyf6oa.js} +1 -1
- package/.output/public/assets/ResponseView-CKe0tNqr.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-enhfdeNh.js +1 -0
- package/.output/public/assets/_sessionId-CJ-SusES.js +1 -0
- package/.output/public/assets/index-BbFsmQRC.js +1 -0
- package/.output/public/assets/index-BfBgrkgU.css +1 -0
- package/.output/public/assets/{main-bwZlEXw2.js → main-o9-hg8g1.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-BEuJJhqT.mjs → _sessionId-F8lm6PIb.mjs} +3 -2
- package/.output/server/_ssr/{CompareDrawer-yTO93GMz.mjs → CompareDrawer-Cf1yfwfd.mjs} +4 -3
- package/.output/server/_ssr/{ProxyViewerContainer-C8qCkHGB.mjs → ProxyViewerContainer-Bmu9867L.mjs} +349 -74
- package/.output/server/_ssr/{ReplayDialog-DRF9PG7Z.mjs → ReplayDialog-DxZfLBOe.mjs} +199 -17
- package/.output/server/_ssr/{RequestAnatomy-DBcuV_jV.mjs → RequestAnatomy-CbdERGZd.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-JJm78HZT.mjs → ResponseView-CvLd6qxI.mjs} +4 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DyGKjBYx.mjs → StreamingChunkSequence-B_lkRB6o.mjs} +4 -3
- package/.output/server/_ssr/{index-T7JG28t6.mjs → index-ejrSItfz.mjs} +3 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BJob8RN3.mjs → router-SbCnbDc2.mjs} +896 -240
- package/.output/server/{_tanstack-start-manifest_v-CuiFi3XC.mjs → _tanstack-start-manifest_v-yx8ftqWz.mjs} +1 -1
- package/.output/server/index.mjs +60 -60
- package/README.md +26 -109
- package/package.json +2 -1
- package/src/cli/doctor.ts +160 -8
- package/src/cli/templates/skill-onboard.ts +2 -20
- 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/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 +37 -15
- 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-DbbK1y7N.js +0 -114
- package/.output/public/assets/ReplayDialog-C1zWNkoz.js +0 -1
- package/.output/public/assets/ResponseView-Dh9iSj6h.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-B_xElyH3.js +0 -1
- package/.output/public/assets/_sessionId-gv1a_NNu.js +0 -1
- package/.output/public/assets/index-D_nZj9Vt.css +0 -1
- package/.output/public/assets/index-u-LwDaeH.js +0 -1
- package/src/components/ui/json-expansion-button.tsx +0 -56
package/src/proxy/dataDir.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
const CURRENT_DATA_DIR_NAME = ".agent-inspector";
|
|
5
|
-
const
|
|
5
|
+
const LEGACY_DATA_DIR_NAMES = [".llm-inspector", ".llm-agent"];
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Compute the directory where agent-inspector stores its on-disk state
|
|
@@ -10,14 +10,12 @@ const LEGACY_DATA_DIR_NAME = ".llm-inspector";
|
|
|
10
10
|
* so every store agrees on the same path.
|
|
11
11
|
*
|
|
12
12
|
* Resolution order (first match wins):
|
|
13
|
-
* 1. `AGENT_INSPECTOR_DATA_DIR` env var
|
|
13
|
+
* 1. `AGENT_INSPECTOR_DATA_DIR` env var - absolute path used as-is,
|
|
14
14
|
* relative path joined to the OS-specific base below.
|
|
15
|
-
* 2. `AGENT_INSPECTOR_CONFIG_DIR` env var
|
|
15
|
+
* 2. `AGENT_INSPECTOR_CONFIG_DIR` env var - kept for backward compat
|
|
16
16
|
* with the runtime config; treated as a directory *name* (relative
|
|
17
17
|
* to the OS-specific base) when not absolute.
|
|
18
18
|
* 3. OS-specific base + `.agent-inspector`.
|
|
19
|
-
* 4. Backward compatibility: if `.agent-inspector` has no known state
|
|
20
|
-
* files but `.llm-inspector` does, reuse `.llm-inspector`.
|
|
21
19
|
*
|
|
22
20
|
* Windows base is `%USERPROFILE%` (falling back to `%APPDATA%`, then `C:\`).
|
|
23
21
|
* macOS / Linux base is `$HOME` (falling back to `/tmp`).
|
|
@@ -26,6 +24,7 @@ const LEGACY_DATA_DIR_NAME = ".llm-inspector";
|
|
|
26
24
|
* directory after this module has been imported.
|
|
27
25
|
*/
|
|
28
26
|
export function getDataDir(): string {
|
|
27
|
+
migrateLegacyDataDirs();
|
|
29
28
|
return resolveDataDir(existsSync);
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -41,15 +40,21 @@ function hasDataDirState(pathExists: PathExists, dir: string): boolean {
|
|
|
41
40
|
);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
pathExists: PathExists,
|
|
43
|
+
function defaultBaseDir(
|
|
46
44
|
env: DataDirEnv = process.env,
|
|
47
45
|
platform: typeof process.platform = process.platform,
|
|
48
46
|
): string {
|
|
49
|
-
|
|
50
|
-
const base = isWindows
|
|
47
|
+
return platform === "win32"
|
|
51
48
|
? (env["USERPROFILE"] ?? env["APPDATA"] ?? "C:\\")
|
|
52
49
|
: (env["HOME"] ?? "/tmp");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveDataDir(
|
|
53
|
+
_pathExists: PathExists,
|
|
54
|
+
env: DataDirEnv = process.env,
|
|
55
|
+
platform: typeof process.platform = process.platform,
|
|
56
|
+
): string {
|
|
57
|
+
const base = defaultBaseDir(env, platform);
|
|
53
58
|
|
|
54
59
|
const dirEnv = env["AGENT_INSPECTOR_DATA_DIR"];
|
|
55
60
|
if (dirEnv !== undefined && dirEnv !== "") {
|
|
@@ -64,11 +69,7 @@ export function resolveDataDir(
|
|
|
64
69
|
return isAbsolute(legacyDirEnv) ? legacyDirEnv : join(base, legacyDirEnv);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
const legacyDir = join(base, LEGACY_DATA_DIR_NAME);
|
|
69
|
-
return !hasDataDirState(pathExists, currentDir) && hasDataDirState(pathExists, legacyDir)
|
|
70
|
-
? legacyDir
|
|
71
|
-
: currentDir;
|
|
72
|
+
return join(base, CURRENT_DATA_DIR_NAME);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -84,3 +85,24 @@ export function hasExplicitDataDir(): boolean {
|
|
|
84
85
|
process.env["AGENT_INSPECTOR_CONFIG_DIR"] !== "")
|
|
85
86
|
);
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
export function migrateLegacyDataDirs(): void {
|
|
90
|
+
if (hasExplicitDataDir()) return;
|
|
91
|
+
|
|
92
|
+
const base = defaultBaseDir();
|
|
93
|
+
const currentDir = join(base, CURRENT_DATA_DIR_NAME);
|
|
94
|
+
if (hasDataDirState(existsSync, currentDir)) return;
|
|
95
|
+
|
|
96
|
+
for (const legacyName of LEGACY_DATA_DIR_NAMES) {
|
|
97
|
+
const legacyDir = join(base, legacyName);
|
|
98
|
+
if (!existsSync(legacyDir)) continue;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
cpSync(legacyDir, currentDir, { recursive: true, force: true });
|
|
102
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
103
|
+
} catch {
|
|
104
|
+
// Best-effort migration: startup must continue even when the old
|
|
105
|
+
// directory is locked or partially unreadable. The next call can retry.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -226,12 +226,10 @@ export const SseEventSchema = z.discriminatedUnion("type", [
|
|
|
226
226
|
SseErrorEvent,
|
|
227
227
|
]);
|
|
228
228
|
|
|
229
|
-
export type AnthropicRequest = z.infer<typeof AnthropicRequestSchema>;
|
|
230
229
|
export type AnthropicResponse = z.infer<typeof AnthropicResponseSchema>;
|
|
231
230
|
export type ResponseContentBlockType = z.infer<typeof ResponseContentBlock>;
|
|
232
231
|
|
|
233
232
|
// Backward-compatible aliases (originally named Inspector*)
|
|
234
233
|
export const InspectorRequestSchema = AnthropicRequestSchema;
|
|
235
234
|
export const InspectorResponseSchema = AnthropicResponseSchema;
|
|
236
|
-
export type InspectorRequest = AnthropicRequest;
|
|
237
235
|
export type InspectorResponse = AnthropicResponse;
|
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
|
+
}
|