@tonyclaw/llm-inspector 1.14.8 → 1.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-Dv-dj1xH.js +105 -0
- package/.output/public/assets/index-bqeypwJB.css +1 -0
- package/.output/public/assets/{main-CJ4MreBr.js → main-C8OUJKbz.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +3 -3
- package/.output/server/_ssr/{index-9uTJ4xYR.mjs → index-_9xcAkkw.mjs} +193 -103
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BKnjB_zi.mjs → router-CmanwZJc.mjs} +45 -14
- package/.output/server/{_tanstack-start-manifest_v-IsglLVKy.mjs → _tanstack-start-manifest_v-BVIiyDeJ.mjs} +1 -1
- package/.output/server/index.mjs +24 -24
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +24 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +42 -19
- package/src/components/proxy-viewer/ConversationHeader.tsx +15 -0
- package/src/components/proxy-viewer/LogEntry.tsx +68 -9
- package/src/components/proxy-viewer/LogEntryHeader.tsx +59 -72
- package/src/components/proxy-viewer/ThreadConnector.tsx +36 -47
- package/src/proxy/formats/anthropic/handler.ts +2 -5
- package/src/proxy/formats/openai/handler.ts +33 -7
- package/src/proxy/formats/openai/schemas.ts +1 -0
- package/src/proxy/formats/openai/stream.ts +24 -0
- package/src/proxy/handler.ts +8 -2
- package/src/proxy/schemas.ts +6 -3
- package/.output/public/assets/index-CdnotuLh.js +0 -105
- package/.output/public/assets/index-vP91146S.css +0 -1
package/.output/server/index.mjs
CHANGED
|
@@ -38,51 +38,51 @@ const assets = {
|
|
|
38
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
39
|
"type": "image/svg+xml",
|
|
40
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
|
-
"/assets/index-
|
|
45
|
+
"/assets/index-bqeypwJB.css": {
|
|
46
46
|
"type": "text/css; charset=utf-8",
|
|
47
|
-
"etag": '"
|
|
48
|
-
"mtime": "2026-06-
|
|
49
|
-
"size":
|
|
50
|
-
"path": "../public/assets/index-
|
|
51
|
-
},
|
|
52
|
-
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
53
|
-
"type": "image/svg+xml",
|
|
54
|
-
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
55
|
-
"mtime": "2026-06-11T12:14:10.996Z",
|
|
56
|
-
"size": 11256,
|
|
57
|
-
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
47
|
+
"etag": '"14d2d-fciDL/LvvG4KiFnlq0WNx0FqBmw"',
|
|
48
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
49
|
+
"size": 85293,
|
|
50
|
+
"path": "../public/assets/index-bqeypwJB.css"
|
|
58
51
|
},
|
|
59
52
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
60
53
|
"type": "image/jpeg",
|
|
61
54
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
62
|
-
"mtime": "2026-06-
|
|
55
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
63
56
|
"size": 6918,
|
|
64
57
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
65
58
|
},
|
|
66
|
-
"/assets/
|
|
59
|
+
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
60
|
+
"type": "image/svg+xml",
|
|
61
|
+
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
62
|
+
"mtime": "2026-06-11T13:44:56.122Z",
|
|
63
|
+
"size": 11256,
|
|
64
|
+
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
65
|
+
},
|
|
66
|
+
"/assets/main-C8OUJKbz.js": {
|
|
67
67
|
"type": "text/javascript; charset=utf-8",
|
|
68
|
-
"etag": '"50599-
|
|
69
|
-
"mtime": "2026-06-
|
|
68
|
+
"etag": '"50599-x4LjDXM7jigK2tdjHCfXl0STwVQ"',
|
|
69
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
70
70
|
"size": 329113,
|
|
71
|
-
"path": "../public/assets/main-
|
|
71
|
+
"path": "../public/assets/main-C8OUJKbz.js"
|
|
72
72
|
},
|
|
73
73
|
"/assets/qwen-CONDcHqt.png": {
|
|
74
74
|
"type": "image/png",
|
|
75
75
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
77
77
|
"size": 357059,
|
|
78
78
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
79
|
},
|
|
80
|
-
"/assets/index-
|
|
80
|
+
"/assets/index-Dv-dj1xH.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"9600f-3dnGI92f5ekK1BS9qp5iLg0Map4"',
|
|
83
|
+
"mtime": "2026-06-11T13:44:56.127Z",
|
|
84
|
+
"size": 614415,
|
|
85
|
+
"path": "../public/assets/index-Dv-dj1xH.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
3
|
import { Download, GitBranch, LayoutGrid, List } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
4
5
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
5
6
|
import type { CapturedLog } from "../proxy/schemas";
|
|
6
7
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
@@ -156,6 +157,18 @@ export function ProxyViewer({
|
|
|
156
157
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
157
158
|
const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
|
|
158
159
|
|
|
160
|
+
const turnIndices = useMemo(() => {
|
|
161
|
+
const indices: number[] = [];
|
|
162
|
+
let turn = 0;
|
|
163
|
+
for (let i = 0; i < stopReasons.length; i++) {
|
|
164
|
+
if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
|
|
165
|
+
turn++;
|
|
166
|
+
}
|
|
167
|
+
indices.push(turn);
|
|
168
|
+
}
|
|
169
|
+
return indices;
|
|
170
|
+
}, [stopReasons]);
|
|
171
|
+
|
|
159
172
|
// Determine what items to render (groups or individual logs)
|
|
160
173
|
const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
|
|
161
174
|
|
|
@@ -402,8 +415,18 @@ export function ProxyViewer({
|
|
|
402
415
|
isPending={log.responseStatus === null}
|
|
403
416
|
isFirst={idx === 0}
|
|
404
417
|
isLast={idx === logs.length - 1}
|
|
418
|
+
isTurnStart={
|
|
419
|
+
idx === 0 ||
|
|
420
|
+
stopReasons[idx - 1] === "end_turn" ||
|
|
421
|
+
stopReasons[idx - 1] === "stop"
|
|
422
|
+
}
|
|
405
423
|
/>
|
|
406
|
-
<div
|
|
424
|
+
<div
|
|
425
|
+
className={cn(
|
|
426
|
+
"flex-1 min-w-0 mb-2 rounded-lg",
|
|
427
|
+
(turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
407
430
|
<LogEntry
|
|
408
431
|
log={log}
|
|
409
432
|
viewMode={viewMode}
|
|
@@ -2,6 +2,7 @@ import { useState, memo, useMemo, useEffect } from "react";
|
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
4
|
import { extractStopReason } from "../../lib/stopReason";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
5
6
|
import {
|
|
6
7
|
ConversationHeader,
|
|
7
8
|
getConversationId,
|
|
@@ -68,6 +69,19 @@ export const ConversationGroup = memo(function ({
|
|
|
68
69
|
// Pre-compute stop reasons for each log — used by ThreadConnector
|
|
69
70
|
const stopReasons = useMemo(() => group.logs.map((log) => extractStopReason(log)), [group.logs]);
|
|
70
71
|
|
|
72
|
+
// Compute turn indices for alternating background colours
|
|
73
|
+
const turnIndices = useMemo(() => {
|
|
74
|
+
const indices: number[] = [];
|
|
75
|
+
let turn = 0;
|
|
76
|
+
for (let i = 0; i < stopReasons.length; i++) {
|
|
77
|
+
if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
|
|
78
|
+
turn++;
|
|
79
|
+
}
|
|
80
|
+
indices.push(turn);
|
|
81
|
+
}
|
|
82
|
+
return indices;
|
|
83
|
+
}, [stopReasons]);
|
|
84
|
+
|
|
71
85
|
const displayId =
|
|
72
86
|
group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
|
|
73
87
|
? group.conversationId
|
|
@@ -89,6 +103,7 @@ export const ConversationGroup = memo(function ({
|
|
|
89
103
|
onToggle={() => setExpanded(!expanded)}
|
|
90
104
|
hideApiFormat={mixed}
|
|
91
105
|
isLoading={isLoading}
|
|
106
|
+
userAgent={group.logs[0]?.userAgent ?? null}
|
|
92
107
|
viewMode={groupViewMode}
|
|
93
108
|
onToggleViewMode={() => setGroupViewMode((prev) => (prev === "thread" ? "flat" : "thread"))}
|
|
94
109
|
/>
|
|
@@ -100,7 +115,6 @@ export const ConversationGroup = memo(function ({
|
|
|
100
115
|
key={log.id}
|
|
101
116
|
log={log}
|
|
102
117
|
viewMode={viewMode}
|
|
103
|
-
suppressApiFormatBadge={!mixed}
|
|
104
118
|
strip={strip}
|
|
105
119
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
106
120
|
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
@@ -111,26 +125,35 @@ export const ConversationGroup = memo(function ({
|
|
|
111
125
|
|
|
112
126
|
{expanded && groupViewMode === "thread" && (
|
|
113
127
|
<div className="ml-3">
|
|
114
|
-
{group.logs.map((log, idx) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
viewMode={viewMode}
|
|
126
|
-
suppressApiFormatBadge={!mixed}
|
|
127
|
-
strip={strip}
|
|
128
|
-
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
129
|
-
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
128
|
+
{group.logs.map((log, idx) => {
|
|
129
|
+
const isTurnStart =
|
|
130
|
+
idx === 0 || stopReasons[idx - 1] === "end_turn" || stopReasons[idx - 1] === "stop";
|
|
131
|
+
return (
|
|
132
|
+
<div key={log.id} className="flex items-stretch">
|
|
133
|
+
<ThreadConnector
|
|
134
|
+
stopReason={stopReasons[idx] ?? null}
|
|
135
|
+
isPending={log.responseStatus === null}
|
|
136
|
+
isFirst={idx === 0}
|
|
137
|
+
isLast={idx === group.logs.length - 1}
|
|
138
|
+
isTurnStart={isTurnStart}
|
|
130
139
|
/>
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
"flex-1 min-w-0 mb-2 rounded-lg",
|
|
143
|
+
(turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<LogEntry
|
|
147
|
+
log={log}
|
|
148
|
+
viewMode={viewMode}
|
|
149
|
+
strip={strip}
|
|
150
|
+
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
151
|
+
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
131
154
|
</div>
|
|
132
|
-
|
|
133
|
-
)
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
134
157
|
</div>
|
|
135
158
|
)}
|
|
136
159
|
</div>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
GitBranch,
|
|
6
6
|
Loader2,
|
|
7
7
|
MessageSquare,
|
|
8
|
+
User,
|
|
8
9
|
Zap,
|
|
9
10
|
} from "lucide-react";
|
|
10
11
|
import type { JSX } from "react";
|
|
@@ -40,6 +41,8 @@ export type ConversationHeaderProps = {
|
|
|
40
41
|
viewMode?: ViewMode;
|
|
41
42
|
/** Toggle between flat and thread display modes for this group. */
|
|
42
43
|
onToggleViewMode?: () => void;
|
|
44
|
+
/** User-Agent string from the first log in the group. */
|
|
45
|
+
userAgent?: string | null;
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
function formatTimestamp(iso: string): string {
|
|
@@ -61,6 +64,7 @@ export function ConversationHeader({
|
|
|
61
64
|
isLoading = false,
|
|
62
65
|
viewMode,
|
|
63
66
|
onToggleViewMode,
|
|
67
|
+
userAgent,
|
|
64
68
|
}: ConversationHeaderProps): JSX.Element {
|
|
65
69
|
return (
|
|
66
70
|
<div
|
|
@@ -123,6 +127,17 @@ export function ConversationHeader({
|
|
|
123
127
|
: conversationId}
|
|
124
128
|
</span>
|
|
125
129
|
|
|
130
|
+
{/* User-Agent */}
|
|
131
|
+
{userAgent !== null && userAgent !== undefined && userAgent !== "" && (
|
|
132
|
+
<span
|
|
133
|
+
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
134
|
+
title={userAgent}
|
|
135
|
+
>
|
|
136
|
+
<User className="size-3" />
|
|
137
|
+
<span className="font-mono tabular-nums truncate max-w-[120px]">{userAgent}</span>
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
|
|
126
141
|
{/* API Format Badge */}
|
|
127
142
|
{!hideApiFormat && (
|
|
128
143
|
<Badge
|
|
@@ -2,7 +2,13 @@ import { Check, Copy, GitCompareArrows, RotateCcw } from "lucide-react";
|
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import { useMemo, useState, memo } from "react";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CapturedLog,
|
|
7
|
+
parseRequest,
|
|
8
|
+
InspectorResponseSchema,
|
|
9
|
+
parseOpenAIResponse,
|
|
10
|
+
OpenAIRequestSchema,
|
|
11
|
+
} from "../../proxy/schemas";
|
|
6
12
|
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
|
|
7
13
|
import { Button } from "../ui/button";
|
|
8
14
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
@@ -18,10 +24,7 @@ import type { CacheTrendEntry } from "./cacheTrend";
|
|
|
18
24
|
export type LogEntryProps = {
|
|
19
25
|
log: CapturedLog;
|
|
20
26
|
viewMode?: "simple" | "full";
|
|
21
|
-
/**
|
|
22
|
-
suppressApiFormatBadge?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Live "strip Claude Code billing header" flag, sourced once at the viewer
|
|
27
|
+
/** Live "strip Claude Code billing header" flag, sourced once at the viewer
|
|
25
28
|
* container. Hoisted out of `LogEntry` so a single SWR subscription serves
|
|
26
29
|
* the whole virtualized list (N logs == N subscriptions is the previous
|
|
27
30
|
* cost).
|
|
@@ -146,7 +149,6 @@ function DiffToggleButton({
|
|
|
146
149
|
export const LogEntry = memo(function ({
|
|
147
150
|
log,
|
|
148
151
|
viewMode = "simple",
|
|
149
|
-
suppressApiFormatBadge = false,
|
|
150
152
|
strip,
|
|
151
153
|
cacheTrend = null,
|
|
152
154
|
onCompareWithPrevious,
|
|
@@ -158,7 +160,63 @@ export const LogEntry = memo(function ({
|
|
|
158
160
|
const [replayOpen, setReplayOpen] = useState<boolean>(false);
|
|
159
161
|
const [headersDiff, setHeadersDiff] = useState<boolean>(false);
|
|
160
162
|
const [requestDiff, setRequestDiff] = useState<boolean>(false);
|
|
161
|
-
const
|
|
163
|
+
const messageCount = useMemo(() => {
|
|
164
|
+
if (log.rawRequestBody === null) return null;
|
|
165
|
+
if (log.apiFormat === "anthropic") {
|
|
166
|
+
const parsed = parseRequest(log.rawRequestBody);
|
|
167
|
+
if (parsed !== null) return parsed.messages.length;
|
|
168
|
+
} else if (log.apiFormat === "openai") {
|
|
169
|
+
try {
|
|
170
|
+
const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
|
|
171
|
+
if (result.success) return result.data.messages.length;
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}, [log.rawRequestBody, log.apiFormat]);
|
|
178
|
+
const toolCount = useMemo(() => {
|
|
179
|
+
if (log.rawRequestBody === null) return null;
|
|
180
|
+
if (log.apiFormat === "anthropic") {
|
|
181
|
+
const parsed = parseRequest(log.rawRequestBody);
|
|
182
|
+
if (parsed !== null && parsed.tools !== undefined && parsed.tools.length > 0) {
|
|
183
|
+
return parsed.tools.length;
|
|
184
|
+
}
|
|
185
|
+
} else if (log.apiFormat === "openai") {
|
|
186
|
+
try {
|
|
187
|
+
const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
|
|
188
|
+
if (result.success && result.data.tools !== undefined && result.data.tools.length > 0) {
|
|
189
|
+
return result.data.tools.length;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// ignore
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}, [log.rawRequestBody, log.apiFormat]);
|
|
197
|
+
const responseToolNames = useMemo(() => {
|
|
198
|
+
if (log.responseText === null) return null;
|
|
199
|
+
if (log.apiFormat === "openai") {
|
|
200
|
+
const parsed = parseOpenAIResponse(log.responseText);
|
|
201
|
+
if (parsed !== null) {
|
|
202
|
+
const toolCalls = parsed.choices[0]?.message?.tool_calls;
|
|
203
|
+
if (toolCalls !== undefined && toolCalls !== null && toolCalls.length > 0) {
|
|
204
|
+
return toolCalls.map((tc) => tc.function?.name ?? "?").filter((n) => n !== "");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (log.apiFormat === "anthropic") {
|
|
208
|
+
try {
|
|
209
|
+
const result = InspectorResponseSchema.safeParse(JSON.parse(log.responseText));
|
|
210
|
+
if (result.success) {
|
|
211
|
+
const names = result.data.content.filter((c) => c.type === "tool_use").map((c) => c.name);
|
|
212
|
+
if (names.length > 0) return names;
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// JSON parse failed, ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}, [log.responseText, log.apiFormat]);
|
|
162
220
|
const strippedRequestBody = useMemo(() => {
|
|
163
221
|
if (!strip || log.apiFormat !== "anthropic" || log.rawRequestBody === null) {
|
|
164
222
|
return null;
|
|
@@ -208,10 +266,11 @@ export const LogEntry = memo(function ({
|
|
|
208
266
|
<div className="border border-border rounded-lg mb-3 overflow-hidden">
|
|
209
267
|
<LogEntryHeader
|
|
210
268
|
log={log}
|
|
211
|
-
|
|
269
|
+
messageCount={messageCount}
|
|
270
|
+
toolCount={toolCount}
|
|
212
271
|
expanded={expanded}
|
|
213
272
|
onToggle={() => setExpanded(!expanded)}
|
|
214
|
-
|
|
273
|
+
responseToolNames={responseToolNames}
|
|
215
274
|
cacheTrend={cacheTrend}
|
|
216
275
|
/>
|
|
217
276
|
|
|
@@ -9,14 +9,13 @@ import {
|
|
|
9
9
|
Loader2,
|
|
10
10
|
MessageSquare,
|
|
11
11
|
Radio,
|
|
12
|
-
User,
|
|
13
12
|
Wrench,
|
|
14
13
|
Zap,
|
|
15
14
|
} from "lucide-react";
|
|
16
15
|
import type { JSX } from "react";
|
|
17
16
|
import { memo } from "react";
|
|
18
17
|
import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
|
|
19
|
-
import type { CapturedLog
|
|
18
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
20
19
|
import { Badge } from "../ui/badge";
|
|
21
20
|
import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
|
|
22
21
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
@@ -56,13 +55,15 @@ function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Eleme
|
|
|
56
55
|
|
|
57
56
|
export type LogEntryHeaderProps = {
|
|
58
57
|
log: CapturedLog;
|
|
59
|
-
|
|
58
|
+
/** Number of messages in the request (supports both Anthropic and OpenAI formats). */
|
|
59
|
+
messageCount?: number | null;
|
|
60
|
+
/** Number of tools defined in the request (supports both Anthropic and OpenAI formats). */
|
|
61
|
+
toolCount?: number | null;
|
|
60
62
|
expanded: boolean;
|
|
61
63
|
onToggle: () => void;
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Per-log cache token trend (creation + read) relative to the previous log
|
|
64
|
+
/** Tool call names extracted from the model response (e.g., ["read_file", "grep"]). */
|
|
65
|
+
responseToolNames?: string[] | null;
|
|
66
|
+
/** Per-log cache token trend (creation + read) relative to the previous log
|
|
66
67
|
* in the same conversation group. When `undefined` or a field is `null`,
|
|
67
68
|
* the corresponding cache span renders as it did before — no arrow.
|
|
68
69
|
*/
|
|
@@ -71,23 +72,17 @@ export type LogEntryHeaderProps = {
|
|
|
71
72
|
|
|
72
73
|
export const LogEntryHeader = memo(function ({
|
|
73
74
|
log,
|
|
74
|
-
|
|
75
|
+
messageCount = null,
|
|
76
|
+
toolCount = null,
|
|
75
77
|
expanded,
|
|
76
78
|
onToggle,
|
|
77
|
-
|
|
79
|
+
responseToolNames = null,
|
|
78
80
|
cacheTrend = null,
|
|
79
81
|
}: LogEntryHeaderProps): JSX.Element {
|
|
80
82
|
const statusCategory = getStatusCategory(log.responseStatus);
|
|
81
83
|
|
|
82
84
|
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
83
85
|
|
|
84
|
-
const messageCount = parsedRequest !== null ? parsedRequest.messages.length : null;
|
|
85
|
-
|
|
86
|
-
const toolCount =
|
|
87
|
-
parsedRequest !== null && parsedRequest.tools !== undefined && parsedRequest.tools.length > 0
|
|
88
|
-
? parsedRequest.tools.length
|
|
89
|
-
: null;
|
|
90
|
-
|
|
91
86
|
return (
|
|
92
87
|
<div
|
|
93
88
|
role="button"
|
|
@@ -124,50 +119,38 @@ export const LogEntryHeader = memo(function ({
|
|
|
124
119
|
</TooltipProvider>
|
|
125
120
|
)}
|
|
126
121
|
|
|
127
|
-
{/*
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)}
|
|
158
|
-
>
|
|
159
|
-
<Loader2 className="size-3 animate-spin" />
|
|
160
|
-
</Badge>
|
|
161
|
-
) : (
|
|
162
|
-
<Badge
|
|
163
|
-
variant="outline"
|
|
164
|
-
className={cn(
|
|
165
|
-
"text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
|
|
166
|
-
STATUS_BADGE_CLASSES[statusCategory],
|
|
122
|
+
{/* Response Status — only shown for non-200 or pending */}
|
|
123
|
+
{statusCategory !== "success" && (
|
|
124
|
+
<>
|
|
125
|
+
{statusCategory === "server_error" ? (
|
|
126
|
+
<Badge
|
|
127
|
+
variant="destructive"
|
|
128
|
+
className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums"
|
|
129
|
+
>
|
|
130
|
+
{log.responseStatus}
|
|
131
|
+
</Badge>
|
|
132
|
+
) : statusCategory === "pending" ? (
|
|
133
|
+
<Badge
|
|
134
|
+
variant="outline"
|
|
135
|
+
className={cn(
|
|
136
|
+
"text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
|
|
137
|
+
STATUS_BADGE_CLASSES[statusCategory],
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
<Loader2 className="size-3 animate-spin" />
|
|
141
|
+
</Badge>
|
|
142
|
+
) : (
|
|
143
|
+
<Badge
|
|
144
|
+
variant="outline"
|
|
145
|
+
className={cn(
|
|
146
|
+
"text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
|
|
147
|
+
STATUS_BADGE_CLASSES[statusCategory],
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{log.responseStatus}
|
|
151
|
+
</Badge>
|
|
167
152
|
)}
|
|
168
|
-
|
|
169
|
-
{log.responseStatus}
|
|
170
|
-
</Badge>
|
|
153
|
+
</>
|
|
171
154
|
)}
|
|
172
155
|
|
|
173
156
|
{/* Elapsed time */}
|
|
@@ -257,6 +240,23 @@ export const LogEntryHeader = memo(function ({
|
|
|
257
240
|
</TooltipProvider>
|
|
258
241
|
)}
|
|
259
242
|
|
|
243
|
+
{/* Response tool calls — tool names the model requested to invoke */}
|
|
244
|
+
{responseToolNames !== null && responseToolNames.length > 0 && (
|
|
245
|
+
<TooltipProvider>
|
|
246
|
+
<Tooltip>
|
|
247
|
+
<TooltipTrigger asChild>
|
|
248
|
+
<span className="flex items-center gap-1 text-amber-400/80 text-xs shrink-0">
|
|
249
|
+
<Wrench className="size-3" />
|
|
250
|
+
<span className="font-mono tabular-nums truncate max-w-[160px]">
|
|
251
|
+
{responseToolNames.join(", ")}
|
|
252
|
+
</span>
|
|
253
|
+
</span>
|
|
254
|
+
</TooltipTrigger>
|
|
255
|
+
<TooltipContent>Tools called by model: {responseToolNames.join(", ")}</TooltipContent>
|
|
256
|
+
</Tooltip>
|
|
257
|
+
</TooltipProvider>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
260
|
{/* Origin */}
|
|
261
261
|
{log.origin !== null && (
|
|
262
262
|
<span
|
|
@@ -270,19 +270,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
270
270
|
</span>
|
|
271
271
|
)}
|
|
272
272
|
|
|
273
|
-
{/* User-Agent */}
|
|
274
|
-
{log.userAgent !== null && (
|
|
275
|
-
<span
|
|
276
|
-
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
277
|
-
title={`User-Agent: ${log.userAgent}`}
|
|
278
|
-
>
|
|
279
|
-
<User className="size-3" />
|
|
280
|
-
<span className="font-mono tabular-nums truncate max-w-[150px]" title={log.userAgent}>
|
|
281
|
-
{log.userAgent}
|
|
282
|
-
</span>
|
|
283
|
-
</span>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
273
|
{/* Client info (PID + project folder) */}
|
|
287
274
|
{(log.clientPid !== null || log.clientProjectFolder !== null) && (
|
|
288
275
|
<TooltipProvider>
|