@tonyclaw/agent-inspector 2.0.28 → 2.0.29
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/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CicCP3Hb.js → CompareDrawer-DcqxlgSQ.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D650kQlv.js +115 -0
- package/.output/public/assets/{ReplayDialog-Bc8q3ujm.js → ReplayDialog-CBcnPgx1.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Coyy3zcH.js → RequestAnatomy-DSgSqCYt.js} +1 -1
- package/.output/public/assets/{ResponseView-BWJzgh0c.js → ResponseView-CjqzBSoF.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BSC3uuE5.js → StreamingChunkSequence-DdBBKolI.js} +1 -1
- package/.output/public/assets/_sessionId-wctOoGh_.js +1 -0
- package/.output/public/assets/index-BhQGSdhG.js +1 -0
- package/.output/public/assets/{index-DsiKfWCp.css → index-CwlHPmgL.css} +1 -1
- package/.output/public/assets/{main-Brj0Gn91.js → main-B6OLZCp9.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +13 -13
- package/.output/server/{_sessionId-DDdyKVG-.mjs → _sessionId-CCfKJJ19.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-XSExe2ml.mjs → CompareDrawer-uaPI5R6e.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-B5pIiBZN.mjs → ProxyViewerContainer-BV2nIxXy.mjs} +83 -20
- package/.output/server/_ssr/{ReplayDialog-BOAu0ric.mjs → ReplayDialog-eE2Hy2Nl.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-C8Q5lozD.mjs → RequestAnatomy-B7vwWo57.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-a_iJfPUF.mjs → ResponseView-Bm6Qp8mo.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-YMKC0j8U.mjs → StreamingChunkSequence-CbAJKJQx.mjs} +2 -2
- package/.output/server/_ssr/{index-CkDwBPzI.mjs → index-u14_Aj60.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DXOXdFFM.mjs → router-qgeGUp1k.mjs} +161 -18
- package/.output/server/{_tanstack-start-manifest_v-DPf3uJys.mjs → _tanstack-start-manifest_v-BrfUXnre.mjs} +1 -1
- package/.output/server/index.mjs +66 -66
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +3 -0
- package/src/components/providers/ProviderCard.tsx +32 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -1
- package/src/lib/providerTestContract.ts +3 -0
- package/src/lib/sessionInfoContract.ts +3 -0
- package/src/mcp/toolHandlers.ts +3 -0
- package/src/proxy/evidenceAnalysis.ts +5 -0
- package/src/proxy/evidenceExporter.ts +8 -3
- package/src/proxy/handler.ts +8 -1
- package/src/proxy/logFinalizer.ts +36 -0
- package/src/proxy/schemas.ts +3 -0
- package/src/proxy/sessionInfo.ts +3 -0
- package/src/proxy/store.ts +3 -0
- package/src/proxy/streamTiming.ts +31 -0
- package/src/routes/api/logs.$id.replay.ts +25 -2
- package/src/routes/api/providers.$providerId.test.log.ts +34 -2
- package/.output/public/assets/ProxyViewerContainer-BccuA6p5.js +0 -115
- package/.output/public/assets/_sessionId-9rEF0uSE.js +0 -1
- package/.output/public/assets/index-B9_VaAWl.js +0 -1
package/.output/server/index.mjs
CHANGED
|
@@ -35,103 +35,103 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
-
"/assets/index-B9_VaAWl.js": {
|
|
39
|
-
"type": "text/javascript; charset=utf-8",
|
|
40
|
-
"etag": '"74-wegwMejcMaCe2qDEma7y6qUh/6U"',
|
|
41
|
-
"mtime": "2026-06-30T03:21:19.568Z",
|
|
42
|
-
"size": 116,
|
|
43
|
-
"path": "../public/assets/index-B9_VaAWl.js"
|
|
44
|
-
},
|
|
45
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
46
39
|
"type": "image/svg+xml",
|
|
47
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
48
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
49
42
|
"size": 5915,
|
|
50
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
51
44
|
},
|
|
52
|
-
"/assets/
|
|
45
|
+
"/assets/index-BhQGSdhG.js": {
|
|
53
46
|
"type": "text/javascript; charset=utf-8",
|
|
54
|
-
"etag": '"
|
|
55
|
-
"mtime": "2026-06-
|
|
56
|
-
"size":
|
|
57
|
-
"path": "../public/assets/
|
|
47
|
+
"etag": '"74-1e7gU3IOVaEjblxPQfgH61GRjlw"',
|
|
48
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
49
|
+
"size": 116,
|
|
50
|
+
"path": "../public/assets/index-BhQGSdhG.js"
|
|
58
51
|
},
|
|
59
|
-
"/assets/
|
|
60
|
-
"type": "text/
|
|
61
|
-
"etag": '"
|
|
62
|
-
"mtime": "2026-06-
|
|
63
|
-
"size":
|
|
64
|
-
"path": "../public/assets/
|
|
52
|
+
"/assets/CompareDrawer-DcqxlgSQ.js": {
|
|
53
|
+
"type": "text/javascript; charset=utf-8",
|
|
54
|
+
"etag": '"49fe-+D2I+/9sd7EYksKARn3HVY2eJf0"',
|
|
55
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
56
|
+
"size": 18942,
|
|
57
|
+
"path": "../public/assets/CompareDrawer-DcqxlgSQ.js"
|
|
65
58
|
},
|
|
66
59
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
67
60
|
"type": "image/jpeg",
|
|
68
61
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
69
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
70
63
|
"size": 6918,
|
|
71
64
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
72
65
|
},
|
|
73
|
-
"/assets/
|
|
74
|
-
"type": "
|
|
75
|
-
"etag": '"
|
|
76
|
-
"mtime": "2026-06-
|
|
77
|
-
"size":
|
|
78
|
-
"path": "../public/assets/
|
|
66
|
+
"/assets/index-CwlHPmgL.css": {
|
|
67
|
+
"type": "text/css; charset=utf-8",
|
|
68
|
+
"etag": '"1a3f0-E2f0kOdSBi1RhjoK38+CSij/apg"',
|
|
69
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
70
|
+
"size": 107504,
|
|
71
|
+
"path": "../public/assets/index-CwlHPmgL.css"
|
|
79
72
|
},
|
|
80
|
-
"/assets/ReplayDialog-
|
|
73
|
+
"/assets/ReplayDialog-CBcnPgx1.js": {
|
|
81
74
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"318c-
|
|
83
|
-
"mtime": "2026-06-
|
|
75
|
+
"etag": '"318c-Wq35vSCO67O3pnw6riReoXT9gOg"',
|
|
76
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
84
77
|
"size": 12684,
|
|
85
|
-
"path": "../public/assets/ReplayDialog-
|
|
78
|
+
"path": "../public/assets/ReplayDialog-CBcnPgx1.js"
|
|
86
79
|
},
|
|
87
|
-
"/assets/
|
|
88
|
-
"type": "
|
|
89
|
-
"etag": '"
|
|
90
|
-
"mtime": "2026-06-
|
|
91
|
-
"size":
|
|
92
|
-
"path": "../public/assets/
|
|
80
|
+
"/assets/RequestAnatomy-DSgSqCYt.js": {
|
|
81
|
+
"type": "text/javascript; charset=utf-8",
|
|
82
|
+
"etag": '"4485-rBai6wnmYHf662t3omQFALcwHSw"',
|
|
83
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
84
|
+
"size": 17541,
|
|
85
|
+
"path": "../public/assets/RequestAnatomy-DSgSqCYt.js"
|
|
93
86
|
},
|
|
94
|
-
"/assets/
|
|
87
|
+
"/assets/ResponseView-CjqzBSoF.js": {
|
|
95
88
|
"type": "text/javascript; charset=utf-8",
|
|
96
|
-
"etag": '"
|
|
97
|
-
"mtime": "2026-06-
|
|
98
|
-
"size":
|
|
99
|
-
"path": "../public/assets/
|
|
89
|
+
"etag": '"30d0-13nPwe9jIZSSl43vTEhXlswXwn8"',
|
|
90
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
91
|
+
"size": 12496,
|
|
92
|
+
"path": "../public/assets/ResponseView-CjqzBSoF.js"
|
|
100
93
|
},
|
|
101
|
-
"/assets/
|
|
94
|
+
"/assets/StreamingChunkSequence-DdBBKolI.js": {
|
|
102
95
|
"type": "text/javascript; charset=utf-8",
|
|
103
|
-
"etag": '"
|
|
104
|
-
"mtime": "2026-06-
|
|
105
|
-
"size":
|
|
106
|
-
"path": "../public/assets/
|
|
96
|
+
"etag": '"d57-9H1QMwesXGWHkYNTPJ/9zDoslX0"',
|
|
97
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
98
|
+
"size": 3415,
|
|
99
|
+
"path": "../public/assets/StreamingChunkSequence-DdBBKolI.js"
|
|
107
100
|
},
|
|
108
|
-
"/assets/_sessionId-
|
|
101
|
+
"/assets/_sessionId-wctOoGh_.js": {
|
|
109
102
|
"type": "text/javascript; charset=utf-8",
|
|
110
|
-
"etag": '"d2
|
|
111
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"d2-fiIvvYyX9jF4fd1W7HpowwQ2Zj4"',
|
|
104
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
112
105
|
"size": 210,
|
|
113
|
-
"path": "../public/assets/_sessionId-
|
|
106
|
+
"path": "../public/assets/_sessionId-wctOoGh_.js"
|
|
114
107
|
},
|
|
115
|
-
"/assets/
|
|
108
|
+
"/assets/main-B6OLZCp9.js": {
|
|
116
109
|
"type": "text/javascript; charset=utf-8",
|
|
117
|
-
"etag": '"
|
|
118
|
-
"mtime": "2026-06-
|
|
119
|
-
"size":
|
|
120
|
-
"path": "../public/assets/
|
|
110
|
+
"etag": '"51726-MaBaigWv6OLiiYg6fusFyN758Hw"',
|
|
111
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
112
|
+
"size": 333606,
|
|
113
|
+
"path": "../public/assets/main-B6OLZCp9.js"
|
|
121
114
|
},
|
|
122
|
-
"/assets/
|
|
123
|
-
"type": "
|
|
124
|
-
"etag": '"
|
|
125
|
-
"mtime": "2026-06-
|
|
126
|
-
"size":
|
|
127
|
-
"path": "../public/assets/
|
|
115
|
+
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
116
|
+
"type": "image/svg+xml",
|
|
117
|
+
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
118
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
119
|
+
"size": 11256,
|
|
120
|
+
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
121
|
+
},
|
|
122
|
+
"/assets/qwen-CONDcHqt.png": {
|
|
123
|
+
"type": "image/png",
|
|
124
|
+
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
125
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
126
|
+
"size": 357059,
|
|
127
|
+
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
128
128
|
},
|
|
129
|
-
"/assets/ProxyViewerContainer-
|
|
129
|
+
"/assets/ProxyViewerContainer-D650kQlv.js": {
|
|
130
130
|
"type": "text/javascript; charset=utf-8",
|
|
131
|
-
"etag": '"
|
|
132
|
-
"mtime": "2026-06-
|
|
133
|
-
"size":
|
|
134
|
-
"path": "../public/assets/ProxyViewerContainer-
|
|
131
|
+
"etag": '"af8bd-kLHfI4KjZ80Xpnq5/VoFdzyBH10"',
|
|
132
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
133
|
+
"size": 719037,
|
|
134
|
+
"path": "../public/assets/ProxyViewerContainer-D650kQlv.js"
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -783,6 +783,9 @@ export function ProxyViewer({
|
|
|
783
783
|
<Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
|
|
784
784
|
<McpLogo className="size-10 shrink-0" />
|
|
785
785
|
</span>
|
|
786
|
+
<span className="max-w-[calc(100vw-7rem)] text-sm font-semibold text-muted-foreground">
|
|
787
|
+
Local First. Evidence First.
|
|
788
|
+
</span>
|
|
786
789
|
<McpReadyBadge />
|
|
787
790
|
</h1>
|
|
788
791
|
<div className="justify-self-end">
|
|
@@ -87,6 +87,16 @@ function hasHeaderDetails(headers: Record<string, string> | undefined): boolean
|
|
|
87
87
|
return formatHeaders(headers) !== undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function formatElapsed(ms: number): string {
|
|
91
|
+
if (ms < 1000) return `${ms}ms`;
|
|
92
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTokenRate(tokensPerSecond: number): string {
|
|
96
|
+
if (tokensPerSecond >= 10) return `${tokensPerSecond.toFixed(0)}/s`;
|
|
97
|
+
return `${tokensPerSecond.toFixed(1)}/s`;
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
function hasDiagnosticDetails(result: TestResult): boolean {
|
|
91
101
|
const debug = result.debug;
|
|
92
102
|
return (
|
|
@@ -255,6 +265,7 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
255
265
|
}
|
|
256
266
|
if (result.success) {
|
|
257
267
|
const tokenParts: ReactNode[] = [];
|
|
268
|
+
const timingParts: ReactNode[] = [];
|
|
258
269
|
if (result.inputTokens !== undefined) {
|
|
259
270
|
tokenParts.push(<span key="in">{result.inputTokens} in</span>);
|
|
260
271
|
}
|
|
@@ -275,11 +286,29 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
275
286
|
</span>,
|
|
276
287
|
);
|
|
277
288
|
}
|
|
289
|
+
if (result.firstChunkMs !== undefined && result.firstChunkMs !== null) {
|
|
290
|
+
timingParts.push(<span key="first">first {formatElapsed(result.firstChunkMs)}</span>);
|
|
291
|
+
}
|
|
292
|
+
if (result.totalStreamMs !== undefined && result.totalStreamMs !== null) {
|
|
293
|
+
timingParts.push(<span key="total">total {formatElapsed(result.totalStreamMs)}</span>);
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
result.tokensPerSecond !== undefined &&
|
|
297
|
+
result.tokensPerSecond !== null &&
|
|
298
|
+
result.tokensPerSecond > 0
|
|
299
|
+
) {
|
|
300
|
+
timingParts.push(<span key="rate">{formatTokenRate(result.tokensPerSecond)} tok/s</span>);
|
|
301
|
+
}
|
|
278
302
|
const displayTokens: ReactNode[] = [];
|
|
279
303
|
for (let i = 0; i < tokenParts.length; i++) {
|
|
280
304
|
if (i > 0) displayTokens.push(", ");
|
|
281
305
|
displayTokens.push(tokenParts[i]);
|
|
282
306
|
}
|
|
307
|
+
const displayTiming: ReactNode[] = [];
|
|
308
|
+
for (let i = 0; i < timingParts.length; i++) {
|
|
309
|
+
if (i > 0) displayTiming.push(", ");
|
|
310
|
+
displayTiming.push(timingParts[i]);
|
|
311
|
+
}
|
|
283
312
|
return (
|
|
284
313
|
<TooltipProvider>
|
|
285
314
|
<Tooltip>
|
|
@@ -290,6 +319,9 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
290
319
|
{tokenParts.length > 0 && (
|
|
291
320
|
<span className="text-muted-foreground">({displayTokens})</span>
|
|
292
321
|
)}
|
|
322
|
+
{timingParts.length > 0 && (
|
|
323
|
+
<span className="hidden text-muted-foreground lg:inline">({displayTiming})</span>
|
|
324
|
+
)}
|
|
293
325
|
</div>
|
|
294
326
|
</TooltipTrigger>
|
|
295
327
|
<TooltipContent>Connection test passed</TooltipContent>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Copy,
|
|
12
12
|
FileDiff,
|
|
13
13
|
FileTerminal,
|
|
14
|
+
Gauge,
|
|
14
15
|
Globe,
|
|
15
16
|
History,
|
|
16
17
|
Loader2,
|
|
@@ -45,6 +46,11 @@ function formatElapsed(ms: number): string {
|
|
|
45
46
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function formatTokenRate(tokensPerSecond: number): string {
|
|
50
|
+
if (tokensPerSecond >= 10) return `${tokensPerSecond.toFixed(0)}/s`;
|
|
51
|
+
return `${tokensPerSecond.toFixed(1)}/s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Inline trend indicator: small arrow (green up / red down) plus the absolute
|
|
50
56
|
* delta in compact form. Returns `null` when there is no trend to display.
|
|
@@ -148,6 +154,11 @@ export const LogEntryHeader = memo(function ({
|
|
|
148
154
|
log.elapsedMs > slowResponseThresholdSeconds * 1000;
|
|
149
155
|
|
|
150
156
|
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
157
|
+
const firstChunkMs = log.firstChunkMs ?? null;
|
|
158
|
+
const tokensPerSecond = log.tokensPerSecond ?? null;
|
|
159
|
+
const firstChunkLabel = firstChunkMs === null ? null : formatElapsed(firstChunkMs);
|
|
160
|
+
const tokenRateLabel =
|
|
161
|
+
tokensPerSecond === null || tokensPerSecond <= 0 ? null : formatTokenRate(tokensPerSecond);
|
|
151
162
|
|
|
152
163
|
return (
|
|
153
164
|
<TooltipProvider>
|
|
@@ -238,11 +249,37 @@ export const LogEntryHeader = memo(function ({
|
|
|
238
249
|
? `Slow response: ${formatElapsed(log.elapsedMs)} exceeds ${formatElapsed(
|
|
239
250
|
slowResponseThresholdSeconds * 1000,
|
|
240
251
|
)}`
|
|
241
|
-
:
|
|
252
|
+
: log.streaming
|
|
253
|
+
? "Total streaming response time"
|
|
254
|
+
: "Elapsed response time"}
|
|
242
255
|
</TooltipContent>
|
|
243
256
|
</Tooltip>
|
|
244
257
|
)}
|
|
245
258
|
|
|
259
|
+
{/* Streaming timing */}
|
|
260
|
+
{log.streaming && firstChunkLabel !== null && (
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<span className="flex items-center gap-1 text-xs text-cyan-400 shrink-0">
|
|
264
|
+
<Radio className="size-3" />
|
|
265
|
+
<span className="font-mono tabular-nums">first {firstChunkLabel}</span>
|
|
266
|
+
</span>
|
|
267
|
+
</TooltipTrigger>
|
|
268
|
+
<TooltipContent>Time to first streaming chunk</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
270
|
+
)}
|
|
271
|
+
{log.streaming && tokenRateLabel !== null && (
|
|
272
|
+
<Tooltip>
|
|
273
|
+
<TooltipTrigger asChild>
|
|
274
|
+
<span className="flex items-center gap-1 text-xs text-emerald-400 shrink-0">
|
|
275
|
+
<Gauge className="size-3" />
|
|
276
|
+
<span className="font-mono tabular-nums">{tokenRateLabel}</span>
|
|
277
|
+
</span>
|
|
278
|
+
</TooltipTrigger>
|
|
279
|
+
<TooltipContent>Output token throughput after first chunk</TooltipContent>
|
|
280
|
+
</Tooltip>
|
|
281
|
+
)}
|
|
282
|
+
|
|
246
283
|
{/* Token counts */}
|
|
247
284
|
{hasTokens && (
|
|
248
285
|
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
@@ -45,6 +45,9 @@ export const ProviderTestResultSchema = z.object({
|
|
|
45
45
|
cacheCreationInputTokens: z.number().optional(),
|
|
46
46
|
cacheReadInputTokens: z.number().optional(),
|
|
47
47
|
latencyMs: z.number().optional(),
|
|
48
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
49
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
50
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
48
51
|
content: z.array(ProviderTestContentSchema).optional(),
|
|
49
52
|
rawResponse: z.string().optional(),
|
|
50
53
|
streaming: z.boolean().optional(),
|
|
@@ -21,6 +21,9 @@ export const SessionLogSummarySchema = z.object({
|
|
|
21
21
|
hasError: z.boolean(),
|
|
22
22
|
error: z.string().nullable(),
|
|
23
23
|
latencyMs: z.number().nullable(),
|
|
24
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
25
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
26
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
24
27
|
tokens: z.object({
|
|
25
28
|
input: z.number().nullable(),
|
|
26
29
|
output: z.number().nullable(),
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -132,6 +132,9 @@ export function buildLogSummary(log: CapturedLog) {
|
|
|
132
132
|
isStreaming: log.streaming,
|
|
133
133
|
hasError,
|
|
134
134
|
latencyMs: log.elapsedMs,
|
|
135
|
+
firstChunkMs: log.firstChunkMs ?? null,
|
|
136
|
+
totalStreamMs: log.totalStreamMs ?? null,
|
|
137
|
+
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
135
138
|
tokens: {
|
|
136
139
|
input: log.inputTokens,
|
|
137
140
|
output: log.outputTokens,
|
|
@@ -318,6 +318,11 @@ function logDetails(log: SessionLogSummary): string {
|
|
|
318
318
|
`streaming=${log.isStreaming ? "yes" : "no"}`,
|
|
319
319
|
`latencyMs=${formatNullable(log.latencyMs)}`,
|
|
320
320
|
];
|
|
321
|
+
if (log.isStreaming) {
|
|
322
|
+
parts.push(`firstChunkMs=${formatNullable(log.firstChunkMs ?? null)}`);
|
|
323
|
+
parts.push(`totalStreamMs=${formatNullable(log.totalStreamMs ?? null)}`);
|
|
324
|
+
parts.push(`tokensPerSecond=${formatNullable(log.tokensPerSecond ?? null)}`);
|
|
325
|
+
}
|
|
321
326
|
if (log.error !== null) {
|
|
322
327
|
parts.push(`error=${log.error}`);
|
|
323
328
|
}
|
|
@@ -112,14 +112,19 @@ function buildMarkdown(document: EvidenceDocument): string {
|
|
|
112
112
|
lines.push("No compact logs were available.", "");
|
|
113
113
|
} else {
|
|
114
114
|
lines.push(
|
|
115
|
-
"| ID | Status | Model | Latency | Path | Error |",
|
|
116
|
-
"| --- | --- | --- | --- | --- | --- |",
|
|
115
|
+
"| ID | Status | Model | Latency | Stream | Path | Error |",
|
|
116
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
117
117
|
);
|
|
118
118
|
for (const log of session.latestLogs) {
|
|
119
|
+
const streamTiming = log.isStreaming
|
|
120
|
+
? `first=${log.firstChunkMs ?? "n/a"}ms total=${log.totalStreamMs ?? "n/a"}ms tps=${
|
|
121
|
+
log.tokensPerSecond ?? "n/a"
|
|
122
|
+
}`
|
|
123
|
+
: "n/a";
|
|
119
124
|
lines.push(
|
|
120
125
|
`| ${log.id} | ${log.status ?? "n/a"} | ${log.model ?? "n/a"} | ${
|
|
121
126
|
log.latencyMs ?? "n/a"
|
|
122
|
-
} | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
|
|
127
|
+
} | ${streamTiming} | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
|
|
123
128
|
);
|
|
124
129
|
}
|
|
125
130
|
lines.push("");
|
package/src/proxy/handler.ts
CHANGED
|
@@ -142,12 +142,14 @@ function handleStreamingResponse(
|
|
|
142
142
|
const capture = new RawStreamCapture();
|
|
143
143
|
const decoder = new TextDecoder();
|
|
144
144
|
let finalizationScheduled = false;
|
|
145
|
+
let firstChunkMs: number | null = null;
|
|
145
146
|
|
|
146
147
|
const scheduleStreamingFinalization = (type: "streaming" | "stream-abort"): void => {
|
|
147
148
|
if (finalizationScheduled) return;
|
|
148
149
|
finalizationScheduled = true;
|
|
149
150
|
|
|
150
|
-
const
|
|
151
|
+
const totalStreamMs = Date.now() - startTime;
|
|
152
|
+
const elapsedMs = totalStreamMs;
|
|
151
153
|
const rawStream = capture.snapshot();
|
|
152
154
|
switch (type) {
|
|
153
155
|
case "streaming":
|
|
@@ -159,6 +161,8 @@ function handleStreamingResponse(
|
|
|
159
161
|
responseStatus: upstreamRes.status,
|
|
160
162
|
rawStream,
|
|
161
163
|
collectStreamingChunks,
|
|
164
|
+
firstChunkMs,
|
|
165
|
+
totalStreamMs,
|
|
162
166
|
});
|
|
163
167
|
return;
|
|
164
168
|
case "stream-abort":
|
|
@@ -170,6 +174,8 @@ function handleStreamingResponse(
|
|
|
170
174
|
rawStream,
|
|
171
175
|
hasChunks: capture.hasChunks,
|
|
172
176
|
collectStreamingChunks,
|
|
177
|
+
firstChunkMs,
|
|
178
|
+
totalStreamMs,
|
|
173
179
|
});
|
|
174
180
|
return;
|
|
175
181
|
}
|
|
@@ -177,6 +183,7 @@ function handleStreamingResponse(
|
|
|
177
183
|
|
|
178
184
|
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
179
185
|
async transform(chunk, controller) {
|
|
186
|
+
if (firstChunkMs === null) firstChunkMs = Date.now() - startTime;
|
|
180
187
|
controller.enqueue(chunk);
|
|
181
188
|
await capture.append(decoder.decode(chunk, { stream: true }));
|
|
182
189
|
},
|
|
@@ -8,6 +8,7 @@ import type { RawStreamSource } from "./rawStreamCapture";
|
|
|
8
8
|
import type { CapturedLog } from "./schemas";
|
|
9
9
|
import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
|
|
10
10
|
import { finalizeLogUpdate } from "./store";
|
|
11
|
+
import { computeTokensPerSecond } from "./streamTiming";
|
|
11
12
|
|
|
12
13
|
type BaseFinalizeLogJob = {
|
|
13
14
|
log: CapturedLog;
|
|
@@ -26,6 +27,8 @@ export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
|
26
27
|
responseStatus: number;
|
|
27
28
|
rawStream: string | RawStreamSource;
|
|
28
29
|
collectStreamingChunks: boolean;
|
|
30
|
+
firstChunkMs: number | null;
|
|
31
|
+
totalStreamMs: number;
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
@@ -33,6 +36,8 @@ export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
|
33
36
|
rawStream: string | RawStreamSource;
|
|
34
37
|
hasChunks: boolean;
|
|
35
38
|
collectStreamingChunks: boolean;
|
|
39
|
+
firstChunkMs: number | null;
|
|
40
|
+
totalStreamMs: number;
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
export type FinalizeLogJob =
|
|
@@ -73,6 +78,9 @@ export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record
|
|
|
73
78
|
cacheCreationInputTokens: log.cacheCreationInputTokens,
|
|
74
79
|
cacheReadInputTokens: log.cacheReadInputTokens,
|
|
75
80
|
elapsedMs: log.elapsedMs,
|
|
81
|
+
firstChunkMs: log.firstChunkMs,
|
|
82
|
+
totalStreamMs: log.totalStreamMs,
|
|
83
|
+
tokensPerSecond: log.tokensPerSecond,
|
|
76
84
|
streaming: log.streaming,
|
|
77
85
|
userAgent: log.userAgent,
|
|
78
86
|
origin: log.origin,
|
|
@@ -119,10 +127,31 @@ function finalizeWithError(
|
|
|
119
127
|
log.responseStatus = log.responseStatus ?? fallbackStatus;
|
|
120
128
|
log.responseText = log.responseText ?? fallbackResponseText;
|
|
121
129
|
log.elapsedMs = job.elapsedMs;
|
|
130
|
+
applyOptionalStreamingTiming(log, job);
|
|
122
131
|
log.error = message;
|
|
123
132
|
return { log, upstreamUrl: job.upstreamUrl, error: message, cleanupRawStreamPath };
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
function applyOptionalStreamingTiming(log: CapturedLog, job: FinalizeLogJob): void {
|
|
136
|
+
switch (job.type) {
|
|
137
|
+
case "non-streaming":
|
|
138
|
+
log.firstChunkMs = null;
|
|
139
|
+
log.totalStreamMs = null;
|
|
140
|
+
log.tokensPerSecond = null;
|
|
141
|
+
return;
|
|
142
|
+
case "streaming":
|
|
143
|
+
case "stream-abort":
|
|
144
|
+
log.firstChunkMs = job.firstChunkMs;
|
|
145
|
+
log.totalStreamMs = job.totalStreamMs;
|
|
146
|
+
log.tokensPerSecond = computeTokensPerSecond({
|
|
147
|
+
outputTokens: log.outputTokens,
|
|
148
|
+
firstChunkMs: job.firstChunkMs,
|
|
149
|
+
totalStreamMs: job.totalStreamMs,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
function finalizeNonStreaming(
|
|
127
156
|
job: FinalizeNonStreamingLogJob,
|
|
128
157
|
log: CapturedLog,
|
|
@@ -136,6 +165,9 @@ function finalizeNonStreaming(
|
|
|
136
165
|
const tokens = formatHandler.extractTokens(job.responseBody);
|
|
137
166
|
|
|
138
167
|
log.elapsedMs = job.elapsedMs;
|
|
168
|
+
log.firstChunkMs = null;
|
|
169
|
+
log.totalStreamMs = null;
|
|
170
|
+
log.tokensPerSecond = null;
|
|
139
171
|
log.responseStatus = job.responseStatus;
|
|
140
172
|
log.responseText = job.responseBody;
|
|
141
173
|
log.inputTokens = tokens.inputTokens;
|
|
@@ -238,6 +270,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
|
|
|
238
270
|
const oversized = getOversizedRawStream(job.rawStream);
|
|
239
271
|
if (oversized !== null) {
|
|
240
272
|
log.elapsedMs = job.elapsedMs;
|
|
273
|
+
applyOptionalStreamingTiming(log, job);
|
|
241
274
|
log.responseStatus = job.responseStatus;
|
|
242
275
|
log.responseText = oversizedRawStreamText(oversized);
|
|
243
276
|
return {
|
|
@@ -262,6 +295,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
|
|
|
262
295
|
log.model ?? undefined,
|
|
263
296
|
job.collectStreamingChunks,
|
|
264
297
|
);
|
|
298
|
+
applyOptionalStreamingTiming(log, job);
|
|
265
299
|
persistStreamingChunks(log);
|
|
266
300
|
|
|
267
301
|
return { log, upstreamUrl: job.upstreamUrl, error: null, cleanupRawStreamPath };
|
|
@@ -294,6 +328,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
294
328
|
let cleanupRawStreamPath: string | null = null;
|
|
295
329
|
try {
|
|
296
330
|
log.elapsedMs = job.elapsedMs;
|
|
331
|
+
applyOptionalStreamingTiming(log, job);
|
|
297
332
|
if (job.hasChunks && formatHandler !== null) {
|
|
298
333
|
const oversized = getOversizedRawStream(job.rawStream);
|
|
299
334
|
if (oversized !== null) {
|
|
@@ -314,6 +349,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
314
349
|
log.model ?? undefined,
|
|
315
350
|
job.collectStreamingChunks,
|
|
316
351
|
);
|
|
352
|
+
applyOptionalStreamingTiming(log, job);
|
|
317
353
|
persistStreamingChunks(log);
|
|
318
354
|
} else {
|
|
319
355
|
log.responseText = "Client aborted";
|
package/src/proxy/schemas.ts
CHANGED
|
@@ -40,6 +40,9 @@ export const CapturedLogSchema = z.object({
|
|
|
40
40
|
cacheCreationInputTokens: z.number().nullable(),
|
|
41
41
|
cacheReadInputTokens: z.number().nullable(),
|
|
42
42
|
elapsedMs: z.number().nullable(),
|
|
43
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
44
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
45
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
43
46
|
streaming: z.boolean(),
|
|
44
47
|
userAgent: z.string().nullable(),
|
|
45
48
|
origin: z.string().nullable(),
|
package/src/proxy/sessionInfo.ts
CHANGED
|
@@ -55,6 +55,9 @@ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
|
|
|
55
55
|
hasError: hasLogError(log),
|
|
56
56
|
error: truncateError(log.error),
|
|
57
57
|
latencyMs: log.elapsedMs,
|
|
58
|
+
firstChunkMs: log.firstChunkMs ?? null,
|
|
59
|
+
totalStreamMs: log.totalStreamMs ?? null,
|
|
60
|
+
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
58
61
|
tokens: {
|
|
59
62
|
input: log.inputTokens,
|
|
60
63
|
output: log.outputTokens,
|
package/src/proxy/store.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StreamingChunk } from "./schemas";
|
|
2
|
+
|
|
3
|
+
export function computeTokensPerSecond({
|
|
4
|
+
outputTokens,
|
|
5
|
+
firstChunkMs,
|
|
6
|
+
totalStreamMs,
|
|
7
|
+
}: {
|
|
8
|
+
outputTokens: number | null | undefined;
|
|
9
|
+
firstChunkMs: number | null | undefined;
|
|
10
|
+
totalStreamMs: number | null | undefined;
|
|
11
|
+
}): number | null {
|
|
12
|
+
if (outputTokens === null || outputTokens === undefined || outputTokens <= 0) return null;
|
|
13
|
+
if (totalStreamMs === null || totalStreamMs === undefined || totalStreamMs <= 0) return null;
|
|
14
|
+
|
|
15
|
+
const generationMs =
|
|
16
|
+
firstChunkMs === null || firstChunkMs === undefined
|
|
17
|
+
? totalStreamMs
|
|
18
|
+
: totalStreamMs - firstChunkMs;
|
|
19
|
+
if (!Number.isFinite(generationMs) || generationMs <= 0) return null;
|
|
20
|
+
|
|
21
|
+
return Number((outputTokens / (generationMs / 1000)).toFixed(2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function firstChunkMsFromChunks(
|
|
25
|
+
chunks: readonly StreamingChunk[] | undefined,
|
|
26
|
+
): number | null {
|
|
27
|
+
const first = chunks?.[0];
|
|
28
|
+
if (first === undefined) return null;
|
|
29
|
+
if (!Number.isFinite(first.timestamp) || first.timestamp < 0) return null;
|
|
30
|
+
return Math.floor(first.timestamp);
|
|
31
|
+
}
|