@tonyclaw/agent-inspector 2.0.14 → 2.0.15
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 +84 -0
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BR_kCD7Q.js → CompareDrawer-B6WCFRJq.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-vc50msJj.js → ProxyViewerContainer-CofGRJoO.js} +31 -31
- package/.output/public/assets/{ReplayDialog-DsAYQBHZ.js → ReplayDialog-O_53zkX8.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-B4uqyUt8.js → RequestAnatomy-CqBSMZv5.js} +1 -1
- package/.output/public/assets/{ResponseView-C1vQL-fM.js → ResponseView-BvbA6zot.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BECMV-xf.js → StreamingChunkSequence-Bnf6djjc.js} +1 -1
- package/.output/public/assets/_sessionId-9ICCw6YA.js +1 -0
- package/.output/public/assets/index-DVkMYH8H.js +1 -0
- package/.output/public/assets/{main-D6wegveF.js → main-Biir9ZGe.js} +2 -2
- package/.output/server/{_sessionId-CeHY-3xa.mjs → _sessionId-B77Gh4IL.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-blWGaGiT.mjs → CompareDrawer-D4-RHFqW.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-Dt_9BuM9.mjs → ProxyViewerContainer-YP28-_2H.mjs} +31 -10
- package/.output/server/_ssr/{ReplayDialog-mzsq7AjW.mjs → ReplayDialog-D9rQlrcY.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-CDAcXMBK.mjs → RequestAnatomy-BXCRWD9H.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-B__xXCsZ.mjs → ResponseView-DEwWMqSS.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-BnJkTdXF.mjs → StreamingChunkSequence-CdrTPf5v.mjs} +2 -2
- package/.output/server/_ssr/{index-B2-uF9XT.mjs → index-Chf1DYqT.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BpEBhA7S.mjs → router-BHXU5jWm.mjs} +89 -46
- package/.output/server/{_tanstack-start-manifest_v-CHekFSzj.mjs → _tanstack-start-manifest_v-D1dtacWy.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +31 -1
- package/package.json +1 -1
- package/src/cli.ts +91 -0
- package/src/components/ProxyViewer.tsx +32 -24
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/providers/SettingsDialog.tsx +10 -0
- package/src/lib/runtimeConfig.ts +4 -0
- package/src/lib/useStripConfig.ts +10 -2
- package/src/proxy/config.ts +33 -3
- package/src/proxy/handler.ts +30 -13
- package/src/proxy/logFinalizer.ts +4 -2
- package/src/routes/api/providers.$providerId.test.log.ts +17 -7
- package/.output/public/assets/_sessionId-3bjQPmHl.js +0 -1
- package/.output/public/assets/index-_7blCOK0.js +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The npm package and CLI remain `agent-inspector`; the platform name is Agent Ins
|
|
|
7
7
|
## What It Does
|
|
8
8
|
|
|
9
9
|
- Runs a local transparent proxy for Anthropic and OpenAI-compatible APIs.
|
|
10
|
-
- Captures model requests, responses,
|
|
10
|
+
- Captures model requests, responses, tool definitions, token usage, and errors.
|
|
11
11
|
- Provides a web UI for browsing sessions and logs in real time.
|
|
12
12
|
- Includes a Chrome companion extension for quick capture status and log navigation.
|
|
13
13
|
- Generates reviewable knowledge candidates from agent sessions.
|
|
@@ -71,6 +71,36 @@ ANTHROPIC_BASE_URL=http://localhost:25947/proxy <tool>
|
|
|
71
71
|
|
|
72
72
|
The web UI runs at http://localhost:25947.
|
|
73
73
|
|
|
74
|
+
## Capture Modes
|
|
75
|
+
|
|
76
|
+
Agent Inspector starts in `simple` capture mode by default:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
agent-inspector
|
|
80
|
+
agent-inspector --mode simple
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Simple mode keeps the proxy lightweight for everyday use. It records the data
|
|
84
|
+
needed for the session timeline, request/response views, token usage, provider
|
|
85
|
+
routing, errors, and knowledge workflows, but it does not retain raw request
|
|
86
|
+
headers, upstream headers, or detailed streaming chunk artifacts.
|
|
87
|
+
|
|
88
|
+
Use `full` mode when you need protocol-level diagnostics:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
agent-inspector --mode full
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Full mode captures the additional raw details used by the `Raw Headers`,
|
|
95
|
+
`Headers`, and `Raw Response` views. It is useful for provider debugging,
|
|
96
|
+
header transformation checks, and SSE stream inspection, but it uses more CPU,
|
|
97
|
+
memory, and disk.
|
|
98
|
+
|
|
99
|
+
The mode is decided at process start. To change modes, restart the server. You
|
|
100
|
+
can also set `AGENT_INSPECTOR_MODE=full` or
|
|
101
|
+
`AGENT_INSPECTOR_CAPTURE_MODE=full`; the CLI flag takes precedence over the
|
|
102
|
+
environment.
|
|
103
|
+
|
|
74
104
|
## Jenkins Release Webhook
|
|
75
105
|
|
|
76
106
|
The `agent-inspector-release` Jenkins pipeline can be triggered from GitCode by
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,9 @@ const __dirname = dirname(__filename);
|
|
|
11
11
|
const DEFAULT_PORT = 25947;
|
|
12
12
|
const LOCAL_PROBE_TIMEOUT_MS = 2000;
|
|
13
13
|
const BRANDED_WINDOWS_RUNTIME_EXE = "agent-inspector.exe";
|
|
14
|
+
const DEFAULT_CAPTURE_MODE = "simple";
|
|
15
|
+
|
|
16
|
+
type CaptureMode = "simple" | "full";
|
|
14
17
|
|
|
15
18
|
process.title = "Agent Inspector";
|
|
16
19
|
|
|
@@ -56,6 +59,24 @@ async function isInspectorHealthy(port: number): Promise<boolean> {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
async function getRunningCaptureMode(port: number): Promise<CaptureMode | null> {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/config`, {
|
|
67
|
+
cache: "no-store",
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) return null;
|
|
71
|
+
const raw: unknown = await response.json();
|
|
72
|
+
return readCaptureMode(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
function isPortAcceptingConnections(port: number): Promise<boolean> {
|
|
60
81
|
return new Promise((resolve) => {
|
|
61
82
|
const socket = createConnection({ host: "127.0.0.1", port });
|
|
@@ -138,9 +159,31 @@ function resolveServerCommand(outputDir: string, serverPath: string): ServerComm
|
|
|
138
159
|
: { command: process.execPath, args: [serverPath] };
|
|
139
160
|
}
|
|
140
161
|
|
|
162
|
+
function parseCaptureMode(value: string | undefined): CaptureMode | null {
|
|
163
|
+
switch (value) {
|
|
164
|
+
case undefined:
|
|
165
|
+
return null;
|
|
166
|
+
case "simple":
|
|
167
|
+
return "simple";
|
|
168
|
+
case "full":
|
|
169
|
+
return "full";
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readCaptureMode(raw: unknown): CaptureMode | null {
|
|
176
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
177
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, "captureMode");
|
|
178
|
+
const value: unknown = descriptor?.value;
|
|
179
|
+
return typeof value === "string" ? parseCaptureMode(value) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
async function runStart(args: string[]): Promise<void> {
|
|
142
183
|
const envPort = process.env["PORT"];
|
|
143
184
|
const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
|
|
185
|
+
const envMode =
|
|
186
|
+
process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
|
|
144
187
|
|
|
145
188
|
let port = portDefault;
|
|
146
189
|
let open = true;
|
|
@@ -149,9 +192,34 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
149
192
|
let forceRestart = false;
|
|
150
193
|
let configDir: string | undefined;
|
|
151
194
|
let providersJson: string | undefined;
|
|
195
|
+
let captureMode = DEFAULT_CAPTURE_MODE;
|
|
196
|
+
let captureModeWasSpecified = false;
|
|
197
|
+
|
|
198
|
+
if (envMode !== undefined && envMode !== "") {
|
|
199
|
+
const parsedMode = parseCaptureMode(envMode);
|
|
200
|
+
if (parsedMode === null) {
|
|
201
|
+
console.error(`Invalid capture mode: ${envMode}. Use simple or full.`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
captureMode = parsedMode;
|
|
206
|
+
captureModeWasSpecified = true;
|
|
207
|
+
}
|
|
152
208
|
|
|
153
209
|
for (let i = 0; i < args.length; i++) {
|
|
154
210
|
const arg = args[i] ?? "";
|
|
211
|
+
if (arg.startsWith("--mode=") || arg.startsWith("--capture-mode=")) {
|
|
212
|
+
const value = arg.slice(arg.indexOf("=") + 1);
|
|
213
|
+
const parsedMode = parseCaptureMode(value);
|
|
214
|
+
if (parsedMode === null) {
|
|
215
|
+
console.error(`Invalid capture mode: ${value}. Use simple or full.`);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
captureMode = parsedMode;
|
|
220
|
+
captureModeWasSpecified = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
155
223
|
switch (arg) {
|
|
156
224
|
case "--port":
|
|
157
225
|
case "-p":
|
|
@@ -181,6 +249,20 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
181
249
|
providersJson = args[i + 1];
|
|
182
250
|
i++;
|
|
183
251
|
break;
|
|
252
|
+
case "--mode":
|
|
253
|
+
case "--capture-mode": {
|
|
254
|
+
const value = args[i + 1];
|
|
255
|
+
const parsedMode = parseCaptureMode(value);
|
|
256
|
+
if (parsedMode === null) {
|
|
257
|
+
console.error(`Invalid capture mode: ${String(value)}. Use simple or full.`);
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
captureMode = parsedMode;
|
|
262
|
+
captureModeWasSpecified = true;
|
|
263
|
+
i++;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
184
266
|
default:
|
|
185
267
|
break;
|
|
186
268
|
}
|
|
@@ -270,6 +352,13 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
270
352
|
|
|
271
353
|
if (!forceRestart && (await isInspectorHealthy(port))) {
|
|
272
354
|
console.log(`agent-inspector is already running at ${url}`);
|
|
355
|
+
if (captureModeWasSpecified) {
|
|
356
|
+
const runningMode = await getRunningCaptureMode(port);
|
|
357
|
+
if (runningMode !== null && runningMode !== captureMode) {
|
|
358
|
+
console.log(`Existing instance capture mode is ${runningMode}; requested ${captureMode}.`);
|
|
359
|
+
console.log(`Use --force-restart to restart with ${captureMode} mode.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
273
362
|
console.log(`Use --force-restart to restart the existing instance.`);
|
|
274
363
|
if (open && openWasSpecified) {
|
|
275
364
|
openBrowser(url);
|
|
@@ -289,6 +378,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
289
378
|
}
|
|
290
379
|
|
|
291
380
|
console.log(`Server running at ${url}`);
|
|
381
|
+
console.log(` Capture mode: ${captureMode}`);
|
|
292
382
|
console.log(` Proxy: ${url}/proxy`);
|
|
293
383
|
console.log(``);
|
|
294
384
|
console.log(`Route AI coding tools through the proxy:`);
|
|
@@ -339,6 +429,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
339
429
|
if (providersJson !== undefined) {
|
|
340
430
|
serverEnv["AGENT_INSPECTOR_PROVIDERS_JSON"] = providersJson;
|
|
341
431
|
}
|
|
432
|
+
serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
|
|
342
433
|
const serverProcess = spawn(serverCommand.command, serverCommand.args, {
|
|
343
434
|
stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
344
435
|
detached: background,
|
|
@@ -3,7 +3,7 @@ import { ArrowDownRight, ArrowLeft, ArrowUpRight, Check, Copy, Download, Plus }
|
|
|
3
3
|
|
|
4
4
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
5
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
6
|
-
import type { TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
6
|
+
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
7
7
|
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
8
8
|
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
9
9
|
import { useProviders } from "../lib/useProviders";
|
|
@@ -599,6 +599,7 @@ export type ProxyViewerProps = {
|
|
|
599
599
|
onClearGroup: (ids: number[]) => void;
|
|
600
600
|
viewMode: "simple" | "full";
|
|
601
601
|
onViewModeChange: (mode: "simple" | "full") => void;
|
|
602
|
+
captureMode: CaptureMode;
|
|
602
603
|
/** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
|
|
603
604
|
strip: boolean;
|
|
604
605
|
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
@@ -625,6 +626,7 @@ export function ProxyViewer({
|
|
|
625
626
|
onClearGroup,
|
|
626
627
|
viewMode,
|
|
627
628
|
onViewModeChange,
|
|
629
|
+
captureMode,
|
|
628
630
|
strip,
|
|
629
631
|
slowResponseThresholdSeconds,
|
|
630
632
|
timeDisplayFormat,
|
|
@@ -821,30 +823,36 @@ export function ProxyViewer({
|
|
|
821
823
|
))}
|
|
822
824
|
</SelectContent>
|
|
823
825
|
</Select>
|
|
824
|
-
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
826
|
+
{captureMode === "full" ? (
|
|
827
|
+
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
828
|
+
<button
|
|
829
|
+
type="button"
|
|
830
|
+
onClick={() => onViewModeChange("simple")}
|
|
831
|
+
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
832
|
+
viewMode === "simple"
|
|
833
|
+
? "bg-muted text-foreground"
|
|
834
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
835
|
+
}`}
|
|
836
|
+
>
|
|
837
|
+
Simple
|
|
838
|
+
</button>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
onClick={() => onViewModeChange("full")}
|
|
842
|
+
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
843
|
+
viewMode === "full"
|
|
844
|
+
? "bg-muted text-foreground"
|
|
845
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
846
|
+
}`}
|
|
847
|
+
>
|
|
848
|
+
Full
|
|
849
|
+
</button>
|
|
850
|
+
</div>
|
|
851
|
+
) : (
|
|
852
|
+
<div className="flex h-8 items-center rounded-md border border-border bg-muted px-3 text-xs text-foreground">
|
|
834
853
|
Simple
|
|
835
|
-
</
|
|
836
|
-
|
|
837
|
-
type="button"
|
|
838
|
-
onClick={() => onViewModeChange("full")}
|
|
839
|
-
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
840
|
-
viewMode === "full"
|
|
841
|
-
? "bg-muted text-foreground"
|
|
842
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
843
|
-
}`}
|
|
844
|
-
>
|
|
845
|
-
Full
|
|
846
|
-
</button>
|
|
847
|
-
</div>
|
|
854
|
+
</div>
|
|
855
|
+
)}
|
|
848
856
|
<div className="flex-1" />
|
|
849
857
|
{!hasSessionContext && (
|
|
850
858
|
<span className="text-muted-foreground text-xs font-mono">
|
|
@@ -330,7 +330,7 @@ export function ProxyViewerContainer({
|
|
|
330
330
|
|
|
331
331
|
// Read the strip config once at the container so the virtualized list does
|
|
332
332
|
// not need N independent SWR subscriptions per row.
|
|
333
|
-
const { strip, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
|
|
333
|
+
const { strip, captureMode, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
|
|
334
334
|
|
|
335
335
|
return (
|
|
336
336
|
<>
|
|
@@ -352,6 +352,7 @@ export function ProxyViewerContainer({
|
|
|
352
352
|
onClearGroup={handleClearGroup}
|
|
353
353
|
viewMode={viewMode}
|
|
354
354
|
onViewModeChange={setViewMode}
|
|
355
|
+
captureMode={captureMode}
|
|
355
356
|
strip={strip}
|
|
356
357
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
357
358
|
timeDisplayFormat={timeDisplayFormat}
|
|
@@ -196,6 +196,7 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
196
196
|
function ProxySettingsTab(): JSX.Element {
|
|
197
197
|
const {
|
|
198
198
|
strip,
|
|
199
|
+
captureMode,
|
|
199
200
|
slowResponseThresholdSeconds,
|
|
200
201
|
timeDisplayFormat,
|
|
201
202
|
isLoading,
|
|
@@ -205,6 +206,7 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
205
206
|
} = useStripConfig();
|
|
206
207
|
const [error, setError] = useState<string | null>(null);
|
|
207
208
|
const [pending, setPending] = useState(false);
|
|
209
|
+
const alternateCaptureMode = captureMode === "full" ? "simple" : "full";
|
|
208
210
|
|
|
209
211
|
const handleToggle = useCallback(
|
|
210
212
|
async (next: boolean) => {
|
|
@@ -253,6 +255,14 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
253
255
|
|
|
254
256
|
return (
|
|
255
257
|
<div className="space-y-4">
|
|
258
|
+
<div className="space-y-1">
|
|
259
|
+
<h3 className="text-sm font-semibold">Capture mode</h3>
|
|
260
|
+
<p className="text-xs text-muted-foreground">
|
|
261
|
+
This process is running in <code>{captureMode}</code> mode. Restart with{" "}
|
|
262
|
+
<code>agent-inspector --mode {alternateCaptureMode}</code> to change it.
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
256
266
|
<div className="space-y-1">
|
|
257
267
|
<h3 className="text-sm font-semibold">Claude Code billing header</h3>
|
|
258
268
|
<p className="text-xs text-muted-foreground">
|
package/src/lib/runtimeConfig.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
|
|
4
4
|
export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
|
|
5
5
|
export const DEFAULT_TIME_DISPLAY_FORMAT = "time";
|
|
6
|
+
export const DEFAULT_CAPTURE_MODE = "simple";
|
|
6
7
|
|
|
7
8
|
export const TimeDisplayFormatSchema = z.enum(["time", "full"]);
|
|
9
|
+
export const CaptureModeSchema = z.enum(["simple", "full"]);
|
|
8
10
|
|
|
9
11
|
export type TimeDisplayFormat = z.infer<typeof TimeDisplayFormatSchema>;
|
|
12
|
+
export type CaptureMode = z.infer<typeof CaptureModeSchema>;
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Schema for the runtime proxy config. Shared between server
|
|
@@ -26,6 +29,7 @@ export const RuntimeConfigSchema = z.object({
|
|
|
26
29
|
.max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
|
|
27
30
|
.default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
|
|
28
31
|
timeDisplayFormat: TimeDisplayFormatSchema.default(DEFAULT_TIME_DISPLAY_FORMAT),
|
|
32
|
+
captureMode: CaptureModeSchema.default(DEFAULT_CAPTURE_MODE),
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import useSWR, { type SWRResponse, useSWRConfig } from "swr";
|
|
2
2
|
import { fetchJson } from "./apiClient";
|
|
3
3
|
import {
|
|
4
|
+
DEFAULT_CAPTURE_MODE,
|
|
4
5
|
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
6
|
DEFAULT_TIME_DISPLAY_FORMAT,
|
|
7
|
+
type CaptureMode,
|
|
6
8
|
RuntimeConfigSchema,
|
|
7
9
|
type TimeDisplayFormat,
|
|
8
10
|
type RuntimeConfig,
|
|
@@ -22,7 +24,9 @@ async function fetcher(url: string): Promise<RuntimeConfig> {
|
|
|
22
24
|
/**
|
|
23
25
|
* PATCH the runtime config. Exported separately for testability.
|
|
24
26
|
*/
|
|
25
|
-
|
|
27
|
+
type RuntimeConfigPatch = Partial<Omit<RuntimeConfig, "captureMode">>;
|
|
28
|
+
|
|
29
|
+
export async function setRuntimeConfig(patch: RuntimeConfigPatch): Promise<RuntimeConfig> {
|
|
26
30
|
return fetchJson(
|
|
27
31
|
STRIP_CONFIG_SWR_KEY,
|
|
28
32
|
RuntimeConfigSchema,
|
|
@@ -37,6 +41,7 @@ export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<R
|
|
|
37
41
|
|
|
38
42
|
export type UseStripConfig = {
|
|
39
43
|
strip: boolean;
|
|
44
|
+
captureMode: CaptureMode;
|
|
40
45
|
slowResponseThresholdSeconds: number;
|
|
41
46
|
timeDisplayFormat: TimeDisplayFormat;
|
|
42
47
|
isLoading: boolean;
|
|
@@ -66,16 +71,18 @@ export function useStripConfig(): UseStripConfig {
|
|
|
66
71
|
const { mutate: globalMutate } = useSWRConfig();
|
|
67
72
|
|
|
68
73
|
const strip = response.data?.stripClaudeCodeBillingHeader ?? false;
|
|
74
|
+
const captureMode = response.data?.captureMode ?? DEFAULT_CAPTURE_MODE;
|
|
69
75
|
const slowResponseThresholdSeconds =
|
|
70
76
|
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
|
|
71
77
|
const timeDisplayFormat = response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT;
|
|
72
78
|
|
|
73
|
-
const optimisticConfig = (patch:
|
|
79
|
+
const optimisticConfig = (patch: RuntimeConfigPatch): RuntimeConfig => ({
|
|
74
80
|
stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
|
|
75
81
|
hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
|
|
76
82
|
slowResponseThresholdSeconds:
|
|
77
83
|
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
78
84
|
timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
|
|
85
|
+
captureMode,
|
|
79
86
|
...patch,
|
|
80
87
|
});
|
|
81
88
|
|
|
@@ -113,6 +120,7 @@ export function useStripConfig(): UseStripConfig {
|
|
|
113
120
|
|
|
114
121
|
return {
|
|
115
122
|
strip,
|
|
123
|
+
captureMode,
|
|
116
124
|
slowResponseThresholdSeconds,
|
|
117
125
|
timeDisplayFormat,
|
|
118
126
|
isLoading: response.isLoading,
|
package/src/proxy/config.ts
CHANGED
|
@@ -11,9 +11,12 @@ import { join } from "node:path";
|
|
|
11
11
|
import { logger } from "./logger";
|
|
12
12
|
import { getDataDir } from "./dataDir";
|
|
13
13
|
import {
|
|
14
|
+
CaptureModeSchema,
|
|
15
|
+
DEFAULT_CAPTURE_MODE,
|
|
14
16
|
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
15
17
|
DEFAULT_TIME_DISPLAY_FORMAT,
|
|
16
18
|
RuntimeConfigSchema,
|
|
19
|
+
type CaptureMode,
|
|
17
20
|
type RuntimeConfig,
|
|
18
21
|
} from "../lib/runtimeConfig";
|
|
19
22
|
|
|
@@ -52,7 +55,22 @@ function getConfigTempPath(): string {
|
|
|
52
55
|
// Internal: writeable singleton. Read via getConfig(), mutate via setConfig().
|
|
53
56
|
let currentConfig: RuntimeConfig = resolveInitialConfig();
|
|
54
57
|
|
|
58
|
+
type MutableRuntimeConfig = Omit<RuntimeConfig, "captureMode">;
|
|
59
|
+
|
|
60
|
+
function resolveInitialCaptureMode(): CaptureMode {
|
|
61
|
+
const value = process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
|
|
62
|
+
if (value === undefined || value === "") return DEFAULT_CAPTURE_MODE;
|
|
63
|
+
|
|
64
|
+
const result = CaptureModeSchema.safeParse(value);
|
|
65
|
+
if (result.success) return result.data;
|
|
66
|
+
|
|
67
|
+
logger.warn(`[config] Invalid capture mode ${value}; falling back to ${DEFAULT_CAPTURE_MODE}`);
|
|
68
|
+
return DEFAULT_CAPTURE_MODE;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
function resolveInitialConfig(): RuntimeConfig {
|
|
72
|
+
const captureMode = resolveInitialCaptureMode();
|
|
73
|
+
|
|
56
74
|
// 1. Persisted file
|
|
57
75
|
const filePath = getConfigFilePath();
|
|
58
76
|
if (existsSync(filePath)) {
|
|
@@ -62,7 +80,7 @@ function resolveInitialConfig(): RuntimeConfig {
|
|
|
62
80
|
const result = RuntimeConfigSchema.safeParse(parsed);
|
|
63
81
|
if (result.success) {
|
|
64
82
|
logger.info(`[config] Loaded persisted config from ${filePath}`);
|
|
65
|
-
return result.data;
|
|
83
|
+
return { ...result.data, captureMode };
|
|
66
84
|
}
|
|
67
85
|
logger.warn(
|
|
68
86
|
`[config] Persisted config at ${filePath} is invalid; falling back to env/default`,
|
|
@@ -81,6 +99,7 @@ function resolveInitialConfig(): RuntimeConfig {
|
|
|
81
99
|
hasSeenOnboarding: false,
|
|
82
100
|
slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
83
101
|
timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
|
|
102
|
+
captureMode,
|
|
84
103
|
};
|
|
85
104
|
}
|
|
86
105
|
|
|
@@ -90,6 +109,7 @@ function resolveInitialConfig(): RuntimeConfig {
|
|
|
90
109
|
hasSeenOnboarding: false,
|
|
91
110
|
slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
92
111
|
timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
|
|
112
|
+
captureMode,
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
115
|
|
|
@@ -106,10 +126,11 @@ export function getConfig(): RuntimeConfig {
|
|
|
106
126
|
* do not throw — the in-memory value is always updated so the caller
|
|
107
127
|
* sees a consistent response.
|
|
108
128
|
*/
|
|
109
|
-
export function setConfig(patch: Partial<
|
|
129
|
+
export function setConfig(patch: Partial<MutableRuntimeConfig>): RuntimeConfig {
|
|
110
130
|
const next: RuntimeConfig = {
|
|
111
131
|
...currentConfig,
|
|
112
132
|
...patch,
|
|
133
|
+
captureMode: currentConfig.captureMode,
|
|
113
134
|
};
|
|
114
135
|
currentConfig = next;
|
|
115
136
|
persistConfig(next);
|
|
@@ -152,7 +173,7 @@ function persistConfig(value: RuntimeConfig): boolean {
|
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
try {
|
|
155
|
-
writeFileSync(tempPath, JSON.stringify(value), "utf-8");
|
|
176
|
+
writeFileSync(tempPath, JSON.stringify(toPersistedConfig(value)), "utf-8");
|
|
156
177
|
} catch (err) {
|
|
157
178
|
logger.error(`[config] Failed to write config temp file: ${String(err)}`);
|
|
158
179
|
return false;
|
|
@@ -173,3 +194,12 @@ function persistConfig(value: RuntimeConfig): boolean {
|
|
|
173
194
|
}
|
|
174
195
|
}
|
|
175
196
|
}
|
|
197
|
+
|
|
198
|
+
function toPersistedConfig(value: RuntimeConfig): Record<string, unknown> {
|
|
199
|
+
return {
|
|
200
|
+
stripClaudeCodeBillingHeader: value.stripClaudeCodeBillingHeader,
|
|
201
|
+
hasSeenOnboarding: value.hasSeenOnboarding,
|
|
202
|
+
slowResponseThresholdSeconds: value.slowResponseThresholdSeconds,
|
|
203
|
+
timeDisplayFormat: value.timeDisplayFormat,
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/proxy/handler.ts
CHANGED
|
@@ -36,16 +36,21 @@ import {
|
|
|
36
36
|
* Strips all custom/non-standard headers from the request and replaces with
|
|
37
37
|
* unified proxy identity. Only preserves standard HTTP headers needed for API calls.
|
|
38
38
|
*/
|
|
39
|
-
function buildProxyHeaders(
|
|
39
|
+
function buildProxyHeaders(
|
|
40
|
+
originalHeaders: Headers,
|
|
41
|
+
captureRawHeaders: boolean,
|
|
42
|
+
): {
|
|
40
43
|
headers: Headers;
|
|
41
|
-
rawHeaders: Record<string, string
|
|
44
|
+
rawHeaders: Record<string, string> | undefined;
|
|
42
45
|
} {
|
|
43
|
-
const rawHeaders: Record<string, string> = {};
|
|
46
|
+
const rawHeaders: Record<string, string> | undefined = captureRawHeaders ? {} : undefined;
|
|
44
47
|
const headers = new Headers();
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
if (rawHeaders !== undefined) {
|
|
50
|
+
originalHeaders.forEach((value, key) => {
|
|
51
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
49
54
|
|
|
50
55
|
headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
|
|
51
56
|
headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
|
|
@@ -128,6 +133,7 @@ function handleStreamingResponse(
|
|
|
128
133
|
startTime: number,
|
|
129
134
|
upstreamUrl: string,
|
|
130
135
|
log: CapturedLog,
|
|
136
|
+
collectStreamingChunks: boolean,
|
|
131
137
|
): Response {
|
|
132
138
|
log.streaming = true;
|
|
133
139
|
log.responseStatus = upstreamRes.status;
|
|
@@ -151,6 +157,7 @@ function handleStreamingResponse(
|
|
|
151
157
|
elapsedMs,
|
|
152
158
|
responseStatus: upstreamRes.status,
|
|
153
159
|
rawStream: full,
|
|
160
|
+
collectStreamingChunks,
|
|
154
161
|
});
|
|
155
162
|
},
|
|
156
163
|
});
|
|
@@ -175,6 +182,7 @@ function handleStreamingResponse(
|
|
|
175
182
|
elapsedMs,
|
|
176
183
|
rawStream: full,
|
|
177
184
|
hasChunks,
|
|
185
|
+
collectStreamingChunks,
|
|
178
186
|
});
|
|
179
187
|
}
|
|
180
188
|
});
|
|
@@ -208,8 +216,9 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
208
216
|
// Important: the original `requestBody` must be preserved so the log stores
|
|
209
217
|
// the unmodified request (the "Raw Request" tab needs the true bytes the
|
|
210
218
|
// client sent). The rewritten body is only used for the upstream fetch.
|
|
219
|
+
const config = getConfig();
|
|
211
220
|
let bodyToForward = requestBody;
|
|
212
|
-
if (
|
|
221
|
+
if (config.stripClaudeCodeBillingHeader && requestBody !== null && parsed.isMessages) {
|
|
213
222
|
const stripped = stripClaudeCodeBillingHeader(requestBody);
|
|
214
223
|
if (stripped.removed > 0) {
|
|
215
224
|
logger.info(
|
|
@@ -248,7 +257,11 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
248
257
|
const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
|
|
249
258
|
const startTime = Date.now();
|
|
250
259
|
|
|
251
|
-
const
|
|
260
|
+
const captureFullDetails = config.captureMode === "full";
|
|
261
|
+
const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(
|
|
262
|
+
req.headers,
|
|
263
|
+
captureFullDetails,
|
|
264
|
+
);
|
|
252
265
|
setUpstreamHost(upstreamHeaders, upstreamBase);
|
|
253
266
|
injectProviderAuth(upstreamHeaders, matchedProviderConfig);
|
|
254
267
|
|
|
@@ -277,10 +290,14 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
277
290
|
// full PowerShell round-trip on top of everything else before
|
|
278
291
|
// `fetch` is even issued.
|
|
279
292
|
const [clientInfo, preAcquiredId] = await Promise.all([getClientInfo(req), getNextLogId()]);
|
|
280
|
-
const upstreamHeadersObj: Record<string, string> =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
const upstreamHeadersObj: Record<string, string> | undefined = captureFullDetails
|
|
294
|
+
? {}
|
|
295
|
+
: undefined;
|
|
296
|
+
if (upstreamHeadersObj !== undefined) {
|
|
297
|
+
upstreamHeaders.forEach((value, key) => {
|
|
298
|
+
upstreamHeadersObj[key.toLowerCase()] = value;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
284
301
|
const log = await createLog(
|
|
285
302
|
req.method,
|
|
286
303
|
parsed.apiPath,
|
|
@@ -330,5 +347,5 @@ export async function handleProxy(req: Request): Promise<Response> {
|
|
|
330
347
|
return handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log);
|
|
331
348
|
}
|
|
332
349
|
|
|
333
|
-
return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log);
|
|
350
|
+
return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log, captureFullDetails);
|
|
334
351
|
}
|
|
@@ -22,12 +22,14 @@ export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
|
22
22
|
type: "streaming";
|
|
23
23
|
responseStatus: number;
|
|
24
24
|
rawStream: string;
|
|
25
|
+
collectStreamingChunks: boolean;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
28
29
|
type: "stream-abort";
|
|
29
30
|
rawStream: string;
|
|
30
31
|
hasChunks: boolean;
|
|
32
|
+
collectStreamingChunks: boolean;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
export type FinalizeLogJob =
|
|
@@ -145,7 +147,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
|
|
|
145
147
|
job.rawStream,
|
|
146
148
|
log,
|
|
147
149
|
log.model ?? undefined,
|
|
148
|
-
|
|
150
|
+
job.collectStreamingChunks,
|
|
149
151
|
);
|
|
150
152
|
persistStreamingChunks(log);
|
|
151
153
|
|
|
@@ -168,7 +170,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
168
170
|
job.rawStream,
|
|
169
171
|
log,
|
|
170
172
|
log.model ?? undefined,
|
|
171
|
-
|
|
173
|
+
job.collectStreamingChunks,
|
|
172
174
|
);
|
|
173
175
|
persistStreamingChunks(log);
|
|
174
176
|
} else {
|