@tonyclaw/llm-inspector 1.18.1 → 1.18.2
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-CAhlM_Gq.js → CompareDrawer-C-4ypEWs.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-WRenRpeh.js +101 -0
- package/.output/public/assets/{ReplayDialog-Bqu2f5HE.js → ReplayDialog-CyBKOgba.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CpVNH0CD.js → RequestAnatomy-C0IrVQ3q.js} +1 -1
- package/.output/public/assets/{ResponseView-B_Gg37Lr.js → ResponseView-MogToC4i.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-E2M_SS1A.js → StreamingChunkSequence-ClhUhT-s.js} +1 -1
- package/.output/public/assets/_sessionId-BO47oA3Z.js +1 -0
- package/.output/public/assets/index-BRvz6-L6.css +1 -0
- package/.output/public/assets/index-Btw8ec7-.js +1 -0
- package/.output/public/assets/{json-viewer-DqhA-ODG.js → json-viewer-BicGakI5.js} +1 -1
- package/.output/public/assets/{main-DpH7JlHv.js → main-Be2qqUUW.js} +7 -7
- package/.output/server/_libs/lucide-react.mjs +20 -14
- package/.output/server/{_sessionId-DcJ0RDNl.mjs → _sessionId-DhKJIdQC.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DajC3x7u.mjs → CompareDrawer-BGUgukJ8.mjs} +5 -5
- package/.output/server/_ssr/{ProxyViewerContainer-C2dnFXoC.mjs → ProxyViewerContainer--3K3o3Sm.mjs} +212 -74
- package/.output/server/_ssr/{ReplayDialog-BnCLuA5z.mjs → ReplayDialog-Bo86xZI4.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-OHE3iT-f.mjs → RequestAnatomy-jRU5qgwB.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-NPshHwOv.mjs → ResponseView-DdO_-79a.mjs} +5 -5
- package/.output/server/_ssr/{StreamingChunkSequence-BfukoR7F.mjs → StreamingChunkSequence-BigLwhh4.mjs} +5 -5
- package/.output/server/_ssr/{index-CF8M0tsv.mjs → index-BHG6vOnr.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CHBa-Oas.mjs → json-viewer-B4c_WjXD.mjs} +4 -4
- package/.output/server/_ssr/{router-B5hOtKSn.mjs → router-DVixpJO-.mjs} +32 -17
- package/.output/server/{_tanstack-start-manifest_v-CFyWvIH6.mjs → _tanstack-start-manifest_v-BbvWUF4v.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +109 -59
- package/package.json +1 -1
- package/src/assets/logos/mcp.png +0 -0
- package/src/components/ProxyViewer.tsx +203 -53
- package/src/components/ProxyViewerContainer.tsx +24 -9
- package/src/components/proxy-viewer/ConversationHeader.tsx +7 -22
- package/src/components/ui/mcp-logo.tsx +20 -0
- package/src/lib/sessionUrl.ts +44 -0
- package/src/routes/session/$sessionId.tsx +5 -57
- package/.output/public/assets/ProxyViewerContainer--miVHNPZ.js +0 -101
- package/.output/public/assets/_sessionId-P9LgC1bF.js +0 -1
- package/.output/public/assets/index-C0wv3YP9.css +0 -1
- package/.output/public/assets/index-kboKku6a.js +0 -1
package/README.md
CHANGED
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
# llm-inspector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
透明捕获、解析、重放和治理 AI 编码工具的 LLM API 流量。
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
`llm-inspector` 是一个运行在本机的
|
|
7
|
+
`llm-inspector` 是一个运行在本机的 LLM API 代理调试台,同时提供 **Web UI + Proxy + MCP Server**。把 Claude Code、OpenCode、Cursor、Cody、MiMo Code 或其他兼容客户端的 API Base URL 指向它,就可以在 Web UI 中实时观察请求、响应、SSE 流式事件、工具调用、Thinking/Reasoning、Token 用量、缓存 Token、请求来源和提供商路由结果;也可以让 Coding Agent 通过 MCP 主动读取日志、复盘会话和重放请求。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
它的定位不是一个单纯的抓包工具,而是面向 AI 编码场景的“可观察代理层”:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- 调试 Agent:看清系统提示词、工具定义、消息、响应、错误和慢请求。
|
|
12
|
+
- 治理 Provider:按模型路由到不同 LLM 提供商,集中管理 API Key、模型、协议 URL 和连接测试。
|
|
13
|
+
- 复现问题:重放历史请求,对比代理处理前后的请求差异,排查客户端、代理和上游之间的问题。
|
|
14
|
+
- 辅助 Agent 自诊断:内置 MCP Server,让 Coding Agent 可以直接查询最近日志、会话、模型、Provider 和重放结果。
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
默认入口:
|
|
16
17
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- **Web UI**:`http://localhost:25947`
|
|
19
|
+
- **Proxy**:`http://localhost:25947/proxy`
|
|
20
|
+
- **MCP Server**:`http://localhost:25947/api/mcp`
|
|
21
|
+
|
|
22
|
+
## 核心能力
|
|
23
|
+
|
|
24
|
+
- **双协议代理**:支持 Anthropic Messages API 与 OpenAI Chat Completions API。
|
|
25
|
+
- **路径识别协议**:根据 endpoint 选择协议,而不是猜测 body。
|
|
20
26
|
- `/proxy/v1/messages` -> Anthropic
|
|
21
27
|
- `/proxy/v1/chat/completions` -> OpenAI
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
28
|
+
- `/proxy/chat/completions` -> OpenAI 兼容路径
|
|
29
|
+
- **多模型 Provider 路由**:一个 Provider 可以配置多个模型,同时保存 Anthropic/OpenAI 两套 Base URL。
|
|
30
|
+
- **AI 编码工具适配**:覆盖 Claude Code、OpenCode、Cursor、Cody、MiMo Code 等可配置 Base URL 的工具。
|
|
31
|
+
- **实时日志与 SSE 捕获**:普通响应和流式响应都会被记录,SSE chunks 支持按需加载。
|
|
32
|
+
- **结构化查看器**:按协议解析 Request、Response、Thinking/Reasoning、Answer、Tool Call、Token 和 Cache Token。
|
|
33
|
+
- **会话与 Turn 视图**:按 session 分组,展示多轮对话边界、模型、协议、状态、耗时和 Token 汇总。
|
|
34
|
+
- **请求 Diff 与 Replay**:比较 Raw/Processed Request,也可以比较同会话相邻请求,并用当前 Provider 配置重放。
|
|
35
|
+
- **Provider 迁移与导入**:支持导入/导出 Provider,并可扫描外部工具配置辅助迁移。
|
|
36
|
+
- **运行时代理开关**:通过 Settings 或 `/api/config` 调整代理行为,无需重启。
|
|
37
|
+
- **MCP 原生调试入口**:提供 `inspector_*` 工具,让 Agent 直接读取日志、重放请求、测试 Provider 和管理 Provider。
|
|
38
|
+
- **本地数据持久化**:Provider、运行时配置、日志与 SSE chunks 存在本机数据目录,适合离线排查。
|
|
30
39
|
|
|
31
40
|

|
|
32
41
|
|
|
@@ -39,12 +48,13 @@ npm install -g @tonyclaw/llm-inspector
|
|
|
39
48
|
llm-inspector
|
|
40
49
|
```
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
启动后会自动打开浏览器。常用参数:
|
|
43
52
|
|
|
44
53
|
```bash
|
|
45
54
|
llm-inspector --no-open
|
|
46
55
|
llm-inspector --port 3000
|
|
47
56
|
llm-inspector --config-dir ./local-config
|
|
57
|
+
llm-inspector --providers '[{"name":"OpenAI","apiKey":"sk-...","models":["gpt-4o-mini"],"openaiBaseUrl":"https://api.openai.com/v1","authHeader":"bearer","createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z","id":"demo"}]'
|
|
48
58
|
```
|
|
49
59
|
|
|
50
60
|
### Docker
|
|
@@ -63,19 +73,26 @@ bun install
|
|
|
63
73
|
bun run dev
|
|
64
74
|
```
|
|
65
75
|
|
|
66
|
-
##
|
|
76
|
+
## 配置 Provider
|
|
67
77
|
|
|
68
|
-
打开 Web UI,进入 **Settings**
|
|
78
|
+
打开 Web UI,进入 **Settings** 添加 Provider:
|
|
69
79
|
|
|
70
|
-
1.
|
|
71
|
-
2.
|
|
72
|
-
3.
|
|
80
|
+
1. 填写 Provider 名称和 API Key。
|
|
81
|
+
2. 添加一个或多个模型名称。代理优先用请求体中的 `model` 匹配 Provider。
|
|
82
|
+
3. 按 Provider 能力填写协议 URL:
|
|
73
83
|
- **Anthropic Base URL**:例如 `https://api.anthropic.com`
|
|
74
|
-
- **OpenAI Base URL**:例如 `https://api.openai.com`
|
|
84
|
+
- **OpenAI Base URL**:例如 `https://api.openai.com/v1`
|
|
75
85
|
4. 选择认证方式:`Bearer` 或 `x-api-key`。
|
|
76
|
-
5. 使用 Provider Test
|
|
86
|
+
5. 使用 Provider Test 验证非流式和流式请求。
|
|
87
|
+
|
|
88
|
+
同一个 Provider 可以同时配置 Anthropic 和 OpenAI URL。代理会根据客户端请求路径选择协议和上游地址;模型匹配用于决定走哪个 Provider。
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
Provider 配置支持:
|
|
91
|
+
|
|
92
|
+
- 多模型合并:同一 API Key 和 URL 组合会合并模型列表。
|
|
93
|
+
- 导入/导出:可备份和迁移配置,导出时可选择是否包含 API Key。
|
|
94
|
+
- 外部扫描:从支持的外部工具配置中扫描可迁移的 Provider。
|
|
95
|
+
- 来源标记:区分个人或公司 Provider,便于列表管理。
|
|
79
96
|
|
|
80
97
|
## 接入 AI 编码工具
|
|
81
98
|
|
|
@@ -94,31 +111,33 @@ $env:ANTHROPIC_BASE_URL = "http://localhost:25947/proxy"
|
|
|
94
111
|
claude
|
|
95
112
|
```
|
|
96
113
|
|
|
97
|
-
如果 API Key 已保存在 llm-inspector
|
|
114
|
+
如果 API Key 已保存在 llm-inspector 的 Provider 配置中,客户端令牌可以留空:
|
|
98
115
|
|
|
99
116
|
```powershell
|
|
100
117
|
$env:ANTHROPIC_AUTH_TOKEN = ""
|
|
101
118
|
```
|
|
102
119
|
|
|
103
|
-
### OpenCode
|
|
104
|
-
|
|
105
|
-
不同客户端使用的环境变量名称不同。将其 Anthropic 或 OpenAI Base URL 设置为:
|
|
120
|
+
### OpenCode
|
|
106
121
|
|
|
107
|
-
```
|
|
108
|
-
http://localhost:25947/proxy
|
|
122
|
+
```bash
|
|
123
|
+
LLM_BASE_URL=http://localhost:25947/proxy opencode
|
|
109
124
|
```
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
### MiMo Code / OpenAI 兼容客户端
|
|
112
127
|
|
|
113
128
|
```bash
|
|
114
|
-
|
|
129
|
+
OPENAI_BASE_URL=http://localhost:25947/proxy mimo
|
|
115
130
|
```
|
|
116
131
|
|
|
117
|
-
对于 Cursor、Cody
|
|
132
|
+
对于 Cursor、Cody 等工具,请在其设置中将 Anthropic 或 OpenAI Base URL 改为:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
http://localhost:25947/proxy
|
|
136
|
+
```
|
|
118
137
|
|
|
119
138
|
## 直接发送请求
|
|
120
139
|
|
|
121
|
-
请先在 Settings 中配置与 `model`
|
|
140
|
+
请先在 Settings 中配置与 `model` 匹配的 Provider。
|
|
122
141
|
|
|
123
142
|
### Anthropic 格式
|
|
124
143
|
|
|
@@ -150,27 +169,48 @@ curl http://localhost:25947/proxy/v1/chat/completions \
|
|
|
150
169
|
|
|
151
170
|
### Simple / Full
|
|
152
171
|
|
|
153
|
-
- **Simple
|
|
172
|
+
- **Simple**:聚焦请求、解析后的响应、工具调用、Thinking/Reasoning 和 Token。
|
|
154
173
|
- **Full**:额外显示 Raw Headers、Headers、Raw Request、Raw Response 和 SSE chunks。
|
|
155
174
|
|
|
156
|
-
### Thinking
|
|
175
|
+
### Thinking / Reasoning / Answer
|
|
157
176
|
|
|
158
177
|
查看器按协议分别解析响应:
|
|
159
178
|
|
|
160
|
-
- Anthropic
|
|
161
|
-
- OpenAI
|
|
179
|
+
- Anthropic:解析 `content[].type === "thinking"`、`text`、`tool_use` 等内容块。
|
|
180
|
+
- OpenAI:解析 `reasoning_content`、`thinking`、`think`、`message.content` 和函数/工具调用。
|
|
181
|
+
|
|
182
|
+
如果响应因为 `max_tokens` 提前结束且只返回 Thinking,界面只展示真实存在的内容,不会生成不存在的 Answer。
|
|
183
|
+
|
|
184
|
+
### 会话、Turn 与来源
|
|
162
185
|
|
|
163
|
-
|
|
186
|
+
日志会记录:
|
|
164
187
|
|
|
165
|
-
|
|
188
|
+
- `sessionId`、模型、Provider、API format、状态码和耗时。
|
|
189
|
+
- 输入/输出 Token、cache creation/read Token。
|
|
190
|
+
- 客户端端口、PID、工作目录和项目目录。
|
|
191
|
+
- 流式响应的 SSE chunk 索引、类型和数据。
|
|
192
|
+
|
|
193
|
+
这些信息用于定位“哪一个工具、哪个项目、哪一轮对话、哪个 Provider”产生了问题。
|
|
194
|
+
|
|
195
|
+
### Diff、Replay 与 Export
|
|
166
196
|
|
|
167
197
|
- **Diff with Raw**:比较代理处理前后的 Request 或 Headers。
|
|
168
198
|
- **Diff with Previous**:比较同一会话、同一协议中的相邻请求。
|
|
169
|
-
- **Replay
|
|
170
|
-
- **Export**:将捕获日志导出为 ZIP
|
|
199
|
+
- **Replay**:使用当前 Provider 配置重新发送历史请求。
|
|
200
|
+
- **Export**:将捕获日志导出为 ZIP,供离线分析或问题复盘。
|
|
171
201
|
|
|
172
202
|
## MCP Server
|
|
173
203
|
|
|
204
|
+
MCP 是 llm-inspector 的一等能力。它让 Agent 不只是在 UI 外部“被观察”,还可以主动查询自己刚刚产生的请求链路,完成自诊断和复盘。
|
|
205
|
+
|
|
206
|
+
典型场景:
|
|
207
|
+
|
|
208
|
+
- **最近请求自查**:Agent 可以读取最近几条日志,确认模型、Provider、状态码、Token、SSE chunks 和错误信息。
|
|
209
|
+
- **会话复盘**:按 `sessionId` 找到一次编码任务的请求序列,定位是哪一轮、哪个工具调用或哪个上游响应异常。
|
|
210
|
+
- **Provider 调试**:列出 Provider、查看配置、触发连接测试,并把测试结果写回日志视图。
|
|
211
|
+
- **请求重放**:对历史请求执行 replay,验证问题是否来自客户端、代理配置或上游 Provider。
|
|
212
|
+
- **上下文压缩排查**:读取 last user message preview、response preview、cache token 等摘要,辅助判断 Agent 行为变化。
|
|
213
|
+
|
|
174
214
|
内置 MCP Server 使用 HTTP Streamable transport:
|
|
175
215
|
|
|
176
216
|
```text
|
|
@@ -196,13 +236,13 @@ Claude Code `.mcp.json` 示例:
|
|
|
196
236
|
| --- | --- |
|
|
197
237
|
| 日志查询 | `inspector_list_logs`, `inspector_get_log`, `inspector_get_log_chunks` |
|
|
198
238
|
| 索引查询 | `inspector_list_sessions`, `inspector_list_models` |
|
|
199
|
-
|
|
|
239
|
+
| Provider 查询 | `inspector_list_providers`, `inspector_get_provider` |
|
|
200
240
|
| 操作 | `inspector_replay_log`, `inspector_test_provider` |
|
|
201
|
-
|
|
|
241
|
+
| Provider 写入 | `inspector_add_provider`, `inspector_update_provider` |
|
|
202
242
|
|
|
203
243
|
完整说明见 [MCP Server 文档](docs/MCP-Server.md)。
|
|
204
244
|
|
|
205
|
-
> MCP
|
|
245
|
+
> MCP 接口仅应在可信本机环境使用。Provider 查询工具会返回明文 API Key,请勿将服务暴露到不可信网络。
|
|
206
246
|
|
|
207
247
|
## 数据目录与环境变量
|
|
208
248
|
|
|
@@ -215,39 +255,49 @@ Claude Code `.mcp.json` 示例:
|
|
|
215
255
|
|
|
216
256
|
| 变量 | 默认值 | 说明 |
|
|
217
257
|
| --- | --- | --- |
|
|
218
|
-
| `PORT` | `25947` | Web UI
|
|
219
|
-
| `LLM_INSPECTOR_DATA_DIR` | 系统默认目录 | providers、runtime config 等数据目录 |
|
|
258
|
+
| `PORT` | `25947` | Web UI、代理和 MCP 端口 |
|
|
259
|
+
| `LLM_INSPECTOR_DATA_DIR` | 系统默认目录 | providers、runtime config、logs、chunks 等数据目录 |
|
|
260
|
+
| `LLM_INSPECTOR_CONFIG_PATH` | `<dataDir>/providers.json` | Provider 配置文件路径 |
|
|
261
|
+
| `LLM_INSPECTOR_PROVIDERS_JSON` | 无 | 启动时注入 Provider JSON |
|
|
262
|
+
| `LLM_INSPECTOR_STRIP_CLAUDE_CODE_BILLING_HEADER` | `0` | 初始是否移除 Claude Code billing header 文本块 |
|
|
220
263
|
| `LOG_DIR` | `<dataDir>/logs` | 捕获日志和运行日志目录 |
|
|
221
264
|
| `LOG_RETENTION_DAYS` | `7` | 日志保留天数 |
|
|
222
265
|
| `CHUNKS_DIR` | `<dataDir>/chunks` | SSE chunks 存储目录 |
|
|
266
|
+
| `PROXY_IDENTITY` | `inspector9527@Tony` | 转发请求使用的代理身份标识 |
|
|
223
267
|
|
|
224
|
-
|
|
268
|
+
Provider API Key 以明文 JSON 保存在本机数据目录中。请保护该目录,并避免在共享或不可信主机上运行服务。
|
|
225
269
|
|
|
226
270
|
## HTTP API
|
|
227
271
|
|
|
228
272
|
| Endpoint | Method | 说明 |
|
|
229
273
|
| --- | --- | --- |
|
|
230
274
|
| `/api/health` | GET | 健康检查 |
|
|
231
|
-
| `/api/
|
|
275
|
+
| `/api/config` | GET / PATCH | 查询或更新运行时代理配置 |
|
|
276
|
+
| `/api/config/paths` | GET | 查看配置文件路径 |
|
|
277
|
+
| `/api/logs` | GET / DELETE | 查询、分页筛选或清空日志;DELETE 可按 id 清理 |
|
|
232
278
|
| `/api/logs/:id` | GET | 获取完整日志 |
|
|
233
279
|
| `/api/logs/:id/chunks` | GET | 获取 SSE chunks |
|
|
234
280
|
| `/api/logs/:id/replay` | POST | 重放请求 |
|
|
235
281
|
| `/api/logs/stream` | GET | 实时日志更新 |
|
|
236
282
|
| `/api/sessions` | GET | 会话列表 |
|
|
237
283
|
| `/api/models` | GET | 模型列表 |
|
|
238
|
-
| `/api/providers` | GET / POST |
|
|
239
|
-
| `/api/providers/:id` | GET / PUT / DELETE |
|
|
240
|
-
| `/api/providers/:id/test` | POST |
|
|
284
|
+
| `/api/providers` | GET / POST | 查询或添加 Provider |
|
|
285
|
+
| `/api/providers/:id` | GET / PUT / DELETE | 管理单个 Provider |
|
|
286
|
+
| `/api/providers/:id/test` | POST | 测试 Provider 连接 |
|
|
287
|
+
| `/api/providers/:id/test/log` | POST | 写入 Provider 测试日志 |
|
|
288
|
+
| `/api/providers/export` | GET | 导出 Provider 配置 |
|
|
289
|
+
| `/api/providers/import` | POST | 导入 Provider 配置 |
|
|
290
|
+
| `/api/providers/scan` | GET | 扫描外部 Provider 配置 |
|
|
241
291
|
| `/api/mcp` | POST | MCP HTTP Streamable endpoint |
|
|
242
292
|
|
|
243
293
|
## 开发与验证
|
|
244
294
|
|
|
245
295
|
```bash
|
|
246
296
|
bun test
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
297
|
+
bun run typecheck
|
|
298
|
+
bun run lint
|
|
299
|
+
bun run format:check
|
|
300
|
+
bun run build
|
|
251
301
|
```
|
|
252
302
|
|
|
253
303
|
涉及代理或 UI 的改动还应启动开发服务器,并使用真实 AI 编码工具通过代理完成端到端验证:
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense } from "react";
|
|
2
|
-
import { Download } from "lucide-react";
|
|
2
|
+
import { ArrowLeft, Check, Copy, Download, Plus } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
5
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
@@ -9,7 +9,9 @@ import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
|
|
|
9
9
|
|
|
10
10
|
import { CrabLogo } from "./ui/crab-logo";
|
|
11
11
|
import { crabVariants } from "./ui/crab-variants";
|
|
12
|
+
import { McpLogo } from "./ui/mcp-logo";
|
|
12
13
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
14
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
13
15
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
14
16
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
15
17
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
@@ -31,6 +33,28 @@ function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut:
|
|
|
31
33
|
return { totalIn, totalOut };
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function formatTimeRange(logs: CapturedLog[]): string | null {
|
|
37
|
+
const first = logs[0];
|
|
38
|
+
const last = logs[logs.length - 1];
|
|
39
|
+
if (first === undefined || last === undefined) return null;
|
|
40
|
+
const format = (iso: string): string =>
|
|
41
|
+
new Date(iso).toLocaleTimeString([], {
|
|
42
|
+
hour: "2-digit",
|
|
43
|
+
minute: "2-digit",
|
|
44
|
+
second: "2-digit",
|
|
45
|
+
});
|
|
46
|
+
return `${format(first.timestamp)} - ${format(last.timestamp)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
50
|
+
for (const log of logs) {
|
|
51
|
+
if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
|
|
52
|
+
return log.userAgent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
35
59
|
const [copied, setCopied] = useState(false);
|
|
36
60
|
|
|
@@ -83,6 +107,97 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
|
83
107
|
);
|
|
84
108
|
}
|
|
85
109
|
|
|
110
|
+
function McpReadyBadge(): JSX.Element {
|
|
111
|
+
return (
|
|
112
|
+
<TooltipProvider>
|
|
113
|
+
<Tooltip>
|
|
114
|
+
<TooltipTrigger asChild>
|
|
115
|
+
<span className="inline-flex h-7 items-center gap-2 rounded-md border border-cyan-400/30 bg-cyan-500/10 px-2.5 font-mono text-[11px] font-medium text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.08)]">
|
|
116
|
+
<span className="size-1.5 rounded-full bg-emerald-300 shadow-[0_0_8px_rgba(110,231,183,0.8)]" />
|
|
117
|
+
MCP Ready
|
|
118
|
+
<span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
|
|
119
|
+
</span>
|
|
120
|
+
</TooltipTrigger>
|
|
121
|
+
<TooltipContent sideOffset={8} className="max-w-[320px] text-left leading-relaxed">
|
|
122
|
+
Coding agents can inspect logs, replay requests, test providers, and debug sessions
|
|
123
|
+
through MCP at /api/mcp.
|
|
124
|
+
</TooltipContent>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
</TooltipProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SessionContextBar({
|
|
131
|
+
sessionId,
|
|
132
|
+
logs,
|
|
133
|
+
totalIn,
|
|
134
|
+
totalOut,
|
|
135
|
+
}: {
|
|
136
|
+
sessionId: string;
|
|
137
|
+
logs: CapturedLog[];
|
|
138
|
+
totalIn: number;
|
|
139
|
+
totalOut: number;
|
|
140
|
+
}): JSX.Element {
|
|
141
|
+
const [copied, setCopied] = useState(false);
|
|
142
|
+
const timeRange = useMemo(() => formatTimeRange(logs), [logs]);
|
|
143
|
+
const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
|
|
144
|
+
|
|
145
|
+
const handleCopyLink = useCallback(() => {
|
|
146
|
+
void window.navigator.clipboard.writeText(window.location.href).then(() => {
|
|
147
|
+
setCopied(true);
|
|
148
|
+
setTimeout(() => setCopied(false), 2000);
|
|
149
|
+
});
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="mb-4 flex items-center gap-3 border border-border rounded-md bg-muted/20 px-3 py-2 text-xs">
|
|
154
|
+
<a
|
|
155
|
+
href="/"
|
|
156
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
157
|
+
aria-label="Back to all sessions"
|
|
158
|
+
title="Back to all sessions"
|
|
159
|
+
>
|
|
160
|
+
<ArrowLeft className="size-3.5" />
|
|
161
|
+
</a>
|
|
162
|
+
<div className="min-w-0 flex-1">
|
|
163
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
164
|
+
<span className="font-mono font-semibold text-purple-400/90 truncate" title={sessionId}>
|
|
165
|
+
{truncateSessionId(sessionId)}
|
|
166
|
+
</span>
|
|
167
|
+
{userAgent !== null && (
|
|
168
|
+
<span
|
|
169
|
+
className="font-mono text-muted-foreground truncate max-w-[220px]"
|
|
170
|
+
title={userAgent}
|
|
171
|
+
>
|
|
172
|
+
{userAgent}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
|
|
177
|
+
<span>
|
|
178
|
+
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
179
|
+
</span>
|
|
180
|
+
{timeRange !== null && <span>{timeRange}</span>}
|
|
181
|
+
{(totalIn > 0 || totalOut > 0) && (
|
|
182
|
+
<span className="font-mono">
|
|
183
|
+
{formatTokens(totalIn)} in / {formatTokens(totalOut)} out
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={handleCopyLink}
|
|
191
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
192
|
+
aria-label={copied ? "Copied session link" : "Copy session link"}
|
|
193
|
+
title={copied ? "Copied session link" : "Copy session link"}
|
|
194
|
+
>
|
|
195
|
+
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
86
201
|
export type ProxyViewerProps = {
|
|
87
202
|
logs: CapturedLog[];
|
|
88
203
|
sessions: string[];
|
|
@@ -105,6 +220,8 @@ export type ProxyViewerProps = {
|
|
|
105
220
|
* the session is already pinned by the URL and the dropdown would just
|
|
106
221
|
* fight the URL state. */
|
|
107
222
|
hideSessionFilter?: boolean;
|
|
223
|
+
/** Session id pinned by a `/session/$id` route. Enables session-page chrome. */
|
|
224
|
+
pinnedSessionId?: string;
|
|
108
225
|
};
|
|
109
226
|
|
|
110
227
|
export function ProxyViewer({
|
|
@@ -122,6 +239,7 @@ export function ProxyViewer({
|
|
|
122
239
|
strip,
|
|
123
240
|
slowResponseThresholdSeconds,
|
|
124
241
|
hideSessionFilter = false,
|
|
242
|
+
pinnedSessionId,
|
|
125
243
|
}: ProxyViewerProps): JSX.Element {
|
|
126
244
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
127
245
|
const [exporting, setExporting] = useState(false);
|
|
@@ -145,6 +263,15 @@ export function ProxyViewer({
|
|
|
145
263
|
};
|
|
146
264
|
}, []);
|
|
147
265
|
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (pinnedSessionId === undefined) {
|
|
268
|
+
document.title = "LLM Inspector";
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const requestLabel = logs.length === 1 ? "1 req" : `${logs.length} req`;
|
|
272
|
+
document.title = `${truncateSessionId(pinnedSessionId)} - ${requestLabel} - LLM Inspector`;
|
|
273
|
+
}, [logs.length, pinnedSessionId]);
|
|
274
|
+
|
|
148
275
|
const handleExport = useCallback(async () => {
|
|
149
276
|
setExporting(true);
|
|
150
277
|
try {
|
|
@@ -177,58 +304,75 @@ export function ProxyViewer({
|
|
|
177
304
|
return (
|
|
178
305
|
<div className="max-w-[1400px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto px-6 pb-6">
|
|
179
306
|
{/* Brand row */}
|
|
180
|
-
<div className="
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
<span className="flex items-end gap-
|
|
184
|
-
|
|
185
|
-
<span className="flex items-end gap-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
? "
|
|
206
|
-
: ""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
307
|
+
<div className="grid grid-cols-[1fr_auto_1fr] items-start gap-3 pt-6 pb-8">
|
|
308
|
+
<div />
|
|
309
|
+
<h1 className="flex min-w-0 flex-col items-center gap-2 text-center">
|
|
310
|
+
<span className="flex max-w-[calc(100vw-7rem)] items-end gap-2 whitespace-nowrap">
|
|
311
|
+
{/* Crab family — hover to animate together */}
|
|
312
|
+
<span className="flex shrink-0 items-end gap-1 group cursor-default" aria-hidden="true">
|
|
313
|
+
<CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
|
|
314
|
+
<span className="hidden items-end gap-0.5 sm:flex">
|
|
315
|
+
{crabVariants.map((Crab, i) => {
|
|
316
|
+
const color = [
|
|
317
|
+
"text-amber-500",
|
|
318
|
+
"text-rose-500",
|
|
319
|
+
"text-sky-500",
|
|
320
|
+
"text-emerald-500",
|
|
321
|
+
"text-violet-500",
|
|
322
|
+
"text-orange-500",
|
|
323
|
+
"text-cyan-500",
|
|
324
|
+
"text-pink-500",
|
|
325
|
+
"text-lime-500",
|
|
326
|
+
"text-blue-500",
|
|
327
|
+
"text-yellow-500",
|
|
328
|
+
"text-fuchsia-500",
|
|
329
|
+
][i];
|
|
330
|
+
const entranceClass =
|
|
331
|
+
crabEntrancePhase === "hidden"
|
|
332
|
+
? "opacity-0 scale-0"
|
|
333
|
+
: crabEntrancePhase === "playing"
|
|
334
|
+
? "animate-crab-piano-pop"
|
|
335
|
+
: "";
|
|
336
|
+
return (
|
|
337
|
+
<Crab
|
|
338
|
+
key={i}
|
|
339
|
+
className={`size-5 ${color} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1 ${entranceClass}`}
|
|
340
|
+
style={{
|
|
341
|
+
transitionDelay: `${i * 50}ms`,
|
|
342
|
+
...(crabEntrancePhase === "playing"
|
|
343
|
+
? { animationDelay: `${i * 400}ms` }
|
|
344
|
+
: {}),
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
})}
|
|
349
|
+
</span>
|
|
220
350
|
</span>
|
|
351
|
+
<span className="flex min-w-0 items-baseline gap-2 pl-1">
|
|
352
|
+
<span className="truncate text-lg font-bold">LLM Inspector</span>
|
|
353
|
+
<span className="shrink-0 font-mono text-xs font-semibold text-muted-foreground">
|
|
354
|
+
v{packageJson.version}
|
|
355
|
+
</span>
|
|
356
|
+
</span>
|
|
357
|
+
<Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
|
|
358
|
+
<McpLogo className="size-10 shrink-0" />
|
|
221
359
|
</span>
|
|
222
|
-
<
|
|
223
|
-
LLM Inspector
|
|
224
|
-
<span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
|
|
225
|
-
</span>
|
|
360
|
+
<McpReadyBadge />
|
|
226
361
|
</h1>
|
|
227
|
-
<div className="
|
|
362
|
+
<div className="justify-self-end">
|
|
228
363
|
<SettingsDialog />
|
|
229
364
|
</div>
|
|
230
365
|
</div>
|
|
231
366
|
|
|
367
|
+
{pinnedSessionId !== undefined && (
|
|
368
|
+
<SessionContextBar
|
|
369
|
+
sessionId={pinnedSessionId}
|
|
370
|
+
logs={logs}
|
|
371
|
+
totalIn={totalIn}
|
|
372
|
+
totalOut={totalOut}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
|
|
232
376
|
{/* Controls + Filters */}
|
|
233
377
|
<div className="flex items-center gap-3 mb-4">
|
|
234
378
|
{!hideSessionFilter && (
|
|
@@ -331,13 +475,19 @@ export function ProxyViewer({
|
|
|
331
475
|
</p>
|
|
332
476
|
<p className="text-xs">
|
|
333
477
|
This session may have been cleared or never existed.{" "}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
478
|
+
{hideSessionFilter ? (
|
|
479
|
+
<a href="/" className="underline hover:text-foreground transition-colors">
|
|
480
|
+
Back to all sessions
|
|
481
|
+
</a>
|
|
482
|
+
) : (
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
onClick={() => onSessionChange("__all__")}
|
|
486
|
+
className="underline hover:text-foreground transition-colors"
|
|
487
|
+
>
|
|
488
|
+
Show all sessions
|
|
489
|
+
</button>
|
|
490
|
+
)}
|
|
341
491
|
</p>
|
|
342
492
|
</div>
|
|
343
493
|
) : (
|