@tonyclaw/llm-inspector 1.7.9 → 1.9.0
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-DdJSLfxK.css +1 -0
- package/.output/public/assets/index-DyKLPMPn.js +97 -0
- package/.output/public/assets/main-Cu0oTDfX.js +17 -0
- package/.output/server/_libs/dequal.mjs +27 -0
- package/.output/server/_libs/swr.mjs +938 -0
- package/.output/server/_libs/use-sync-external-store.mjs +64 -1
- package/.output/server/_ssr/{index-CAIDMqNv.mjs → index-COIATcfa.mjs} +186 -88
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CsCLdrXq.mjs → router-CwmgKXBJ.mjs} +259 -74
- package/.output/server/{_tanstack-start-manifest_v-BF6ge6dS.mjs → _tanstack-start-manifest_v-C7hQOzvX.mjs} +1 -1
- package/.output/server/index.mjs +23 -23
- package/README.md +8 -209
- package/package.json +2 -1
- package/src/components/ProxyViewer.tsx +2 -0
- package/src/components/ProxyViewerContainer.tsx +10 -1
- package/src/components/providers/ProviderCard.tsx +57 -48
- package/src/components/providers/ProviderForm.tsx +21 -0
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +29 -34
- package/src/components/providers/SettingsDialog.tsx +5 -3
- package/src/components/proxy-viewer/LogEntry.tsx +7 -0
- package/src/components/proxy-viewer/ResponseView.tsx +32 -6
- package/src/lib/useProviders.ts +30 -0
- package/src/proxy/chunkStorage.ts +4 -6
- package/src/proxy/formats/anthropic/schemas.ts +9 -0
- package/src/proxy/formats/anthropic/stream.ts +11 -0
- package/src/proxy/formats/openai/stream.ts +15 -0
- package/src/proxy/handler.ts +34 -27
- package/src/proxy/logIndex.ts +52 -7
- package/src/proxy/logger.ts +60 -10
- package/src/proxy/providers.ts +5 -0
- package/src/proxy/schemas.ts +2 -0
- package/src/proxy/socketTracker.ts +90 -36
- package/src/proxy/store.ts +24 -14
- package/src/routes/__root.tsx +4 -1
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/index-B3RwBPLW.css +0 -1
- package/.output/public/assets/index-CB8ZIeEk.js +0 -97
- package/.output/public/assets/main-BrU8NdGQ.js +0 -17
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-Cu0oTDfX.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DyKLPMPn.js"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts" } }, "clientEntry": "/assets/main-Cu0oTDfX.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -97,54 +97,54 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
99
|
const assets = {
|
|
100
|
-
"/assets/index-B3RwBPLW.css": {
|
|
101
|
-
"type": "text/css; charset=utf-8",
|
|
102
|
-
"etag": '"10c74-aXacU4DRFVsUwcC5jHnjoPRSlTA"',
|
|
103
|
-
"mtime": "2026-06-04T00:12:14.535Z",
|
|
104
|
-
"size": 68724,
|
|
105
|
-
"path": "../public/assets/index-B3RwBPLW.css"
|
|
106
|
-
},
|
|
107
100
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
108
101
|
"type": "image/svg+xml",
|
|
109
102
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
110
|
-
"mtime": "2026-06-
|
|
103
|
+
"mtime": "2026-06-04T03:52:13.283Z",
|
|
111
104
|
"size": 5915,
|
|
112
105
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
113
106
|
},
|
|
107
|
+
"/assets/index-DdJSLfxK.css": {
|
|
108
|
+
"type": "text/css; charset=utf-8",
|
|
109
|
+
"etag": '"10da0-LYeZ5d/vwqh4bAnuP/9hr6Wka6g"',
|
|
110
|
+
"mtime": "2026-06-04T03:52:13.283Z",
|
|
111
|
+
"size": 69024,
|
|
112
|
+
"path": "../public/assets/index-DdJSLfxK.css"
|
|
113
|
+
},
|
|
114
114
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
115
115
|
"type": "image/jpeg",
|
|
116
116
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
117
|
-
"mtime": "2026-06-
|
|
117
|
+
"mtime": "2026-06-04T03:52:13.278Z",
|
|
118
118
|
"size": 6918,
|
|
119
119
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
120
120
|
},
|
|
121
|
-
"/assets/main-BrU8NdGQ.js": {
|
|
122
|
-
"type": "text/javascript; charset=utf-8",
|
|
123
|
-
"etag": '"4db57-Aw3zM46XAKLPcPLc9vLSzFsuUjI"',
|
|
124
|
-
"mtime": "2026-06-04T00:12:14.537Z",
|
|
125
|
-
"size": 318295,
|
|
126
|
-
"path": "../public/assets/main-BrU8NdGQ.js"
|
|
127
|
-
},
|
|
128
121
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
129
122
|
"type": "image/svg+xml",
|
|
130
123
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
131
|
-
"mtime": "2026-06-
|
|
124
|
+
"mtime": "2026-06-04T03:52:13.283Z",
|
|
132
125
|
"size": 11256,
|
|
133
126
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
134
127
|
},
|
|
128
|
+
"/assets/main-Cu0oTDfX.js": {
|
|
129
|
+
"type": "text/javascript; charset=utf-8",
|
|
130
|
+
"etag": '"50591-/K5L0AnXTcumA7dHm7kmA5HXlCE"',
|
|
131
|
+
"mtime": "2026-06-04T03:52:13.283Z",
|
|
132
|
+
"size": 329105,
|
|
133
|
+
"path": "../public/assets/main-Cu0oTDfX.js"
|
|
134
|
+
},
|
|
135
135
|
"/assets/qwen-CONDcHqt.png": {
|
|
136
136
|
"type": "image/png",
|
|
137
137
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
138
|
-
"mtime": "2026-06-
|
|
138
|
+
"mtime": "2026-06-04T03:52:13.283Z",
|
|
139
139
|
"size": 357059,
|
|
140
140
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
141
141
|
},
|
|
142
|
-
"/assets/index-
|
|
142
|
+
"/assets/index-DyKLPMPn.js": {
|
|
143
143
|
"type": "text/javascript; charset=utf-8",
|
|
144
|
-
"etag": '"
|
|
145
|
-
"mtime": "2026-06-
|
|
146
|
-
"size":
|
|
147
|
-
"path": "../public/assets/index-
|
|
144
|
+
"etag": '"84a31-iF3/oGIdY77bqBWoGeM9Ctiyids"',
|
|
145
|
+
"mtime": "2026-06-04T03:52:13.284Z",
|
|
146
|
+
"size": 543281,
|
|
147
|
+
"path": "../public/assets/index-DyKLPMPn.js"
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
function readAsset(id) {
|
package/README.md
CHANGED
|
@@ -10,63 +10,22 @@ llm-inspector 是一个 LLM API 透明代理,能够捕获 AI 编程工具(Cl
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 文档目录
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
### 实时 SSE 流更新 / Real-Time SSE Streaming
|
|
22
|
-
|
|
23
|
-
SSE 流式响应实时推送,无需轮询,即时查看每个数据块。
|
|
24
|
-
|
|
25
|
-
### 健康检查 / Health Check
|
|
26
|
-
|
|
27
|
-
`GET /api/health` 端点,支持 Docker Compose 健康检查和容器编排。
|
|
28
|
-
|
|
29
|
-
### 支持多种 AI 编程工具 / Multiple AI Coding Tools
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
# Claude Code
|
|
33
|
-
ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
34
|
-
|
|
35
|
-
# OpenCode
|
|
36
|
-
LLM_BASE_URL=http://localhost:25947/proxy opencode
|
|
37
|
-
|
|
38
|
-
# Cursor, Cody 等 / etc.
|
|
39
|
-
ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### 支持双 API 格式 / Dual API Format Support
|
|
43
|
-
|
|
44
|
-
| API 格式 | 端点 | 工具 |
|
|
45
|
-
|----------|------|------|
|
|
46
|
-
| Anthropic | `/v1/messages` | Claude Code 等 |
|
|
47
|
-
| OpenAI | `/v1/chat/completions` | OpenAI、DeepSeek 等 |
|
|
48
|
-
|
|
49
|
-
### 多 LLM 提供商支持 / Multi-Provider Support
|
|
50
|
-
|
|
51
|
-
| 提供商 | 模型前缀 | API 格式 |
|
|
52
|
-
|--------|---------|----------|
|
|
53
|
-
| Anthropic | `claude-*` | Anthropic |
|
|
54
|
-
| OpenAI | `gpt-*`, `o1-*`, `o3-*` | OpenAI |
|
|
55
|
-
| DeepSeek | `deepseek-*` | OpenAI |
|
|
56
|
-
| MiniMax | `MiniMax-*` | Anthropic/OpenAI |
|
|
57
|
-
| Qwen | `qwen-*` | OpenAI |
|
|
58
|
-
| ZhipuAI | `glm-*` | OpenAI |
|
|
15
|
+
- [特性 (Features)](docs/Features.md) - 核心功能介绍
|
|
16
|
+
- [安装 (Installation)](docs/Installation.md) - 安装指南
|
|
17
|
+
- [使用 (Usage)](docs/Usage.md) - 详细使用说明
|
|
18
|
+
- [架构 (Architecture)](docs/Architecture.md) - 系统架构
|
|
19
|
+
- [开发 (Development)](docs/Development.md) - 开发指南
|
|
59
20
|
|
|
60
21
|
---
|
|
61
22
|
|
|
62
|
-
## 快速开始
|
|
23
|
+
## 快速开始
|
|
63
24
|
|
|
64
|
-
### npm
|
|
25
|
+
### npm 安装
|
|
65
26
|
|
|
66
27
|
```bash
|
|
67
28
|
npm install -g @tonyclaw/llm-inspector
|
|
68
|
-
|
|
69
|
-
# 启动
|
|
70
29
|
llm-inspector
|
|
71
30
|
```
|
|
72
31
|
|
|
@@ -79,170 +38,10 @@ docker-compose up -d
|
|
|
79
38
|
### 源码运行
|
|
80
39
|
|
|
81
40
|
```bash
|
|
82
|
-
# 安装依赖
|
|
83
41
|
bun install
|
|
84
|
-
|
|
85
|
-
# 启动开发服务器
|
|
86
42
|
bun run dev
|
|
87
43
|
```
|
|
88
44
|
|
|
89
|
-
设置环境变量并启动 AI 编程工具:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
# Claude Code
|
|
93
|
-
ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
94
|
-
|
|
95
|
-
# OpenCode
|
|
96
|
-
LLM_BASE_URL=http://localhost:25947/proxy opencode
|
|
97
|
-
```
|
|
98
|
-
|
|
99
45
|
打开浏览器访问 http://localhost:25947 查看实时捕获的请求。
|
|
100
46
|
|
|
101
47
|

|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Docker 部署 / Docker Deployment
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
# 构建并启动
|
|
109
|
-
docker-compose up -d
|
|
110
|
-
|
|
111
|
-
# 查看日志
|
|
112
|
-
docker-compose logs -f
|
|
113
|
-
|
|
114
|
-
# 停止
|
|
115
|
-
docker-compose down
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
容器默认暴露 `25947` 端口,包含健康检查,可直接接入 Docker Compose 或 Kubernetes。
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## 实时仪表盘 / Real-Time Dashboard
|
|
123
|
-
|
|
124
|
-
### 日志列表 / Log List
|
|
125
|
-
|
|
126
|
-
虚拟滚动列表展示每个被代理的请求,每条记录显示:
|
|
127
|
-
|
|
128
|
-
- **模型标签** — 如 `claude-sonnet-4-20250514`
|
|
129
|
-
- **API 格式** — Anthropic(橙色)/ OpenAI(蓝色)
|
|
130
|
-
- **响应状态** — 绿色 2xx / 黄色 4xx / 红色 5xx
|
|
131
|
-
- **Token 用量** — 输入 / 输出,自动 K/M 格式化
|
|
132
|
-
- **流式标识** — 区分流式与非流式响应
|
|
133
|
-
|
|
134
|
-
### 解析响应视图 / Parsed Response View
|
|
135
|
-
|
|
136
|
-
**Anthropic 响应:**
|
|
137
|
-
- 模型、停止原因、Token 用量
|
|
138
|
-
- 内容块分类渲染:文本(Markdown)、思考(可折叠紫色块)、工具调用(可折叠蓝色块)
|
|
139
|
-
|
|
140
|
-
**OpenAI 响应:**
|
|
141
|
-
- 模型、结束原因、Token 用量
|
|
142
|
-
- 推理内容(紫色块)、文本、函数调用
|
|
143
|
-
|
|
144
|
-
### SSE 流式数据 / SSE Streaming
|
|
145
|
-
|
|
146
|
-

|
|
147
|
-
|
|
148
|
-
流式响应显示:
|
|
149
|
-
- **SSE 数据块面板** — 懒加载,按索引分组
|
|
150
|
-
- **JSON 树查看器** — 类型颜色编码(字符串绿/数字黄/布尔蓝)
|
|
151
|
-
- **重建响应** — 从 SSE 片段重建完整 JSON
|
|
152
|
-
|
|
153
|
-
### 提供商测试 / Provider Testing
|
|
154
|
-
|
|
155
|
-

|
|
156
|
-
|
|
157
|
-
内置连接测试功能,同时测试非流式和流式连接,测试结果直接显示在日志列表中。
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 架构 / Architecture
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
AI Coding Tool → llm-inspector (:25947/proxy/*) → LLM Provider
|
|
165
|
-
↓
|
|
166
|
-
Web UI (:25947)
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
| 模块 | 路径 | 职责 |
|
|
170
|
-
|------|------|------|
|
|
171
|
-
| 代理处理器 | `src/proxy/handler.ts` | HTTP 代理、请求/响应捕获、SSE 流式 |
|
|
172
|
-
| 格式处理器 | `src/proxy/formats/` | Anthropic/OpenAI 格式抽象 |
|
|
173
|
-
| 提供商管理 | `src/proxy/providers.ts` | 多提供商配置存储 |
|
|
174
|
-
| 格式协议 | `src/proxy/formats/anthropic/` `src/proxy/formats/openai/` | 格式特定解析器 |
|
|
175
|
-
| 客户端追踪 | `src/proxy/socketTracker.ts` | PID、CWD、项目名 |
|
|
176
|
-
| 日志存储 | `src/proxy/logger.ts`, `store.ts` | 内存缓冲区 + 磁盘持久化 |
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## 命令行 / CLI
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
llm-inspector # 启动服务
|
|
184
|
-
llm-inspector --port 3000 # 指定端口
|
|
185
|
-
llm-inspector --no-open # 禁止自动打开浏览器
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
| 参数 | 默认值 | 说明 |
|
|
189
|
-
|------|--------|------|
|
|
190
|
-
| `--port`, `-p` | `25947` | 监听端口 |
|
|
191
|
-
| `--open` | `true` | 启动时自动打开浏览器 |
|
|
192
|
-
|
|
193
|
-
### 环境变量
|
|
194
|
-
|
|
195
|
-
| 变量 | 默认值 | 说明 |
|
|
196
|
-
|------|--------|------|
|
|
197
|
-
| `PORT` | `25947` | 服务器端口 |
|
|
198
|
-
| `LOG_DIR` | `~/.llm-inspector/logs/` | 日志存储目录 |
|
|
199
|
-
| `LOG_RETENTION_DAYS` | `7` | 日志保留天数 |
|
|
200
|
-
| `CHUNKS_DIR` | `~/.llm-inspector/chunks/` | SSE 数据块存储 |
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## 直接测试 / Direct Testing
|
|
205
|
-
|
|
206
|
-
不通过 AI 编程工具,直接用 curl 测试提供商连接:
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
# Anthropic
|
|
210
|
-
curl http://localhost:25947/proxy/v1/messages \
|
|
211
|
-
-H 'content-type: application/json' \
|
|
212
|
-
-d '{"model":"claude-sonnet-4-20250514","max_tokens":100,"messages":[{"role":"user","content":"hello"}]}'
|
|
213
|
-
|
|
214
|
-
# OpenAI
|
|
215
|
-
curl http://localhost:25947/proxy/v1/chat/completions \
|
|
216
|
-
-H 'content-type: application/json' \
|
|
217
|
-
-d '{"model":"gpt-4o-mini","max_tokens":100,"messages":[{"role":"user","content":"hello"}]}'
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## 开发 / Development
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
# 类型检查、Lint、格式化
|
|
226
|
-
bun run check
|
|
227
|
-
|
|
228
|
-
# 生产构建
|
|
229
|
-
bun run build
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## 发布 / Publishing
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
# 1. 更新版本号
|
|
238
|
-
git commit -m "chore: bump version"
|
|
239
|
-
git tag v<version>
|
|
240
|
-
|
|
241
|
-
# 2. 推送代码和标签
|
|
242
|
-
git push origin main --tags
|
|
243
|
-
|
|
244
|
-
# 3. 发布到 npm
|
|
245
|
-
npm publish --access public
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
包名:`@tonyclaw/llm-inspector`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonyclaw/llm-inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"react": "^19",
|
|
62
62
|
"react-dom": "^19",
|
|
63
63
|
"react-markdown": "^10.1.0",
|
|
64
|
+
"swr": "^2.4.1",
|
|
64
65
|
"tailwind-merge": "^3.4.0",
|
|
65
66
|
"tw-animate-css": "^1.4.0",
|
|
66
67
|
"zod": "^4.3.6"
|
|
@@ -3,6 +3,7 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
|
|
3
3
|
import { Download, LayoutGrid, List } from "lucide-react";
|
|
4
4
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
5
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
6
|
+
import packageJson from "../../package.json";
|
|
6
7
|
import {
|
|
7
8
|
ConversationGroup,
|
|
8
9
|
groupLogsByConversation,
|
|
@@ -142,6 +143,7 @@ export function ProxyViewer({
|
|
|
142
143
|
{/* Header */}
|
|
143
144
|
<div className="flex items-center gap-4 mb-4 px-6 pt-6">
|
|
144
145
|
<h1 className="text-lg font-bold flex-1">LLM Proxy Inspector</h1>
|
|
146
|
+
<span className="text-muted-foreground text-xs font-mono">v{packageJson.version}</span>
|
|
145
147
|
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
146
148
|
<button
|
|
147
149
|
type="button"
|
|
@@ -63,6 +63,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
63
63
|
const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
|
|
64
64
|
const [error, setError] = useState<string | null>(null);
|
|
65
65
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
66
|
+
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
66
67
|
|
|
67
68
|
const fetchSessionsAndModels = useCallback(async () => {
|
|
68
69
|
try {
|
|
@@ -152,8 +153,12 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
152
153
|
es.onerror = () => {
|
|
153
154
|
setError("SSE connection lost, reconnecting...");
|
|
154
155
|
es.close();
|
|
156
|
+
// Clear any existing reconnect timeout
|
|
157
|
+
if (reconnectTimeoutRef.current !== null) {
|
|
158
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
159
|
+
}
|
|
155
160
|
// Reconnect after 3 seconds
|
|
156
|
-
setTimeout(connectSSE, 3000);
|
|
161
|
+
reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
|
|
157
162
|
};
|
|
158
163
|
|
|
159
164
|
void fetchSessionsAndModels();
|
|
@@ -166,6 +171,10 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
166
171
|
eventSourceRef.current.close();
|
|
167
172
|
eventSourceRef.current = null;
|
|
168
173
|
}
|
|
174
|
+
if (reconnectTimeoutRef.current !== null) {
|
|
175
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
176
|
+
reconnectTimeoutRef.current = null;
|
|
177
|
+
}
|
|
169
178
|
};
|
|
170
179
|
}, [connectSSE]);
|
|
171
180
|
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "lucide-react";
|
|
21
21
|
import type { ProviderConfig } from "../../proxy/providers";
|
|
22
22
|
|
|
23
|
-
// Known provider documentation links
|
|
23
|
+
// Known provider documentation links (fallback when apiDocsUrl is not configured)
|
|
24
24
|
const KNOWN_PROVIDER_DOCS: Record<string, string> = {
|
|
25
25
|
deepseek: "https://api-docs.deepseek.com/zh-cn/",
|
|
26
26
|
};
|
|
@@ -49,9 +49,11 @@ type TestResult = {
|
|
|
49
49
|
|
|
50
50
|
type NotConfigured = { notConfigured: true };
|
|
51
51
|
|
|
52
|
+
type Testing = { testing: true };
|
|
53
|
+
|
|
52
54
|
type StreamingTestResults = {
|
|
53
|
-
nonStreaming: TestResult | NotConfigured;
|
|
54
|
-
streaming: TestResult | NotConfigured;
|
|
55
|
+
nonStreaming: TestResult | NotConfigured | Testing;
|
|
56
|
+
streaming: TestResult | NotConfigured | Testing;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
type TestResults = {
|
|
@@ -74,10 +76,17 @@ function maskApiKey(apiKey: string): string {
|
|
|
74
76
|
return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function hasSuccessField(result: TestResult | NotConfigured): result is TestResult {
|
|
79
|
+
function hasSuccessField(result: TestResult | NotConfigured | Testing): result is TestResult {
|
|
78
80
|
return Object.prototype.hasOwnProperty.call(result, "success");
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
// Using Object.prototype.hasOwnProperty.call to avoid 'in' operator
|
|
84
|
+
function isNotConfiguredState(
|
|
85
|
+
result: TestResult | NotConfigured | Testing,
|
|
86
|
+
): result is NotConfigured {
|
|
87
|
+
return Object.prototype.hasOwnProperty.call(result, "notConfigured");
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
function getErrorIcon(type: ErrorType): JSX.Element {
|
|
82
91
|
const iconProps = { className: "size-3", strokeWidth: 2 };
|
|
83
92
|
switch (type) {
|
|
@@ -102,24 +111,28 @@ function getErrorIcon(type: ErrorType): JSX.Element {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
function TestStatus({
|
|
106
|
-
result,
|
|
107
|
-
isTesting,
|
|
108
|
-
}: {
|
|
109
|
-
result: TestResult | NotConfigured;
|
|
110
|
-
isTesting?: boolean;
|
|
111
|
-
}): JSX.Element {
|
|
114
|
+
function TestStatus({ result }: { result: TestResult | NotConfigured | Testing }): JSX.Element {
|
|
112
115
|
if (!hasSuccessField(result)) {
|
|
116
|
+
// Not TestResult - check if NotConfigured or Testing
|
|
117
|
+
if (isNotConfiguredState(result)) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
|
|
120
|
+
<Minus className="size-3" />
|
|
121
|
+
<span>Not configured</span>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
// Must be Testing state
|
|
113
126
|
return (
|
|
114
|
-
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
115
|
-
<
|
|
116
|
-
<span>
|
|
127
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
|
|
128
|
+
<RotateCw className="size-3 animate-spin" />
|
|
129
|
+
<span>Testing...</span>
|
|
117
130
|
</div>
|
|
118
131
|
);
|
|
119
132
|
}
|
|
120
133
|
if (result.success) {
|
|
121
134
|
return (
|
|
122
|
-
<div className="flex items-center gap-1 text-xs text-green-600">
|
|
135
|
+
<div className="flex items-center gap-1 text-xs text-green-600 shrink-0">
|
|
123
136
|
<CheckCircle className="size-3" />
|
|
124
137
|
<span>Connected</span>
|
|
125
138
|
</div>
|
|
@@ -131,20 +144,16 @@ function TestStatus({
|
|
|
131
144
|
const errorHint = error?.hint;
|
|
132
145
|
const errorType = error?.type ?? "unknown";
|
|
133
146
|
|
|
147
|
+
// Combine message and hint in a single line for consistent layout
|
|
148
|
+
const fullMessage = errorHint !== undefined ? `${errorMessage} — ${errorHint}` : errorMessage;
|
|
149
|
+
|
|
134
150
|
return (
|
|
135
|
-
<div
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<span className="truncate">{errorMessage}</span>
|
|
142
|
-
</div>
|
|
143
|
-
{errorHint !== undefined && (
|
|
144
|
-
<div className="text-xs text-muted-foreground pl-4 truncate" title={errorHint}>
|
|
145
|
-
{errorHint}
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
151
|
+
<div
|
|
152
|
+
className="flex items-center gap-1 text-xs text-red-600 shrink-0 max-w-[200px]"
|
|
153
|
+
title={error?.details ?? fullMessage}
|
|
154
|
+
>
|
|
155
|
+
{getErrorIcon(errorType)}
|
|
156
|
+
<span className="truncate">{errorMessage}</span>
|
|
148
157
|
</div>
|
|
149
158
|
);
|
|
150
159
|
}
|
|
@@ -160,6 +169,13 @@ export function ProviderCard({
|
|
|
160
169
|
}: ProviderCardProps): JSX.Element {
|
|
161
170
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
162
171
|
|
|
172
|
+
// Get docs URL: use provider.apiDocsUrl if configured, otherwise check KNOWN_PROVIDER_DOCS
|
|
173
|
+
const docsUrl =
|
|
174
|
+
provider.apiDocsUrl ??
|
|
175
|
+
Object.entries(KNOWN_PROVIDER_DOCS).find(([keyword]) =>
|
|
176
|
+
provider.name.toLowerCase().includes(keyword),
|
|
177
|
+
)?.[1];
|
|
178
|
+
|
|
163
179
|
return (
|
|
164
180
|
<div className="border rounded-lg p-4 flex flex-col gap-3 bg-card">
|
|
165
181
|
<div className="flex items-start justify-between gap-2">
|
|
@@ -170,20 +186,17 @@ export function ProviderCard({
|
|
|
170
186
|
: provider.name}
|
|
171
187
|
</span>
|
|
172
188
|
</div>
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
|
|
184
|
-
<span className="sr-only">Docs</span>
|
|
185
|
-
</a>
|
|
186
|
-
) : null,
|
|
189
|
+
{docsUrl !== undefined && (
|
|
190
|
+
<a
|
|
191
|
+
href={docsUrl}
|
|
192
|
+
target="_blank"
|
|
193
|
+
rel="noopener noreferrer"
|
|
194
|
+
className="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1 text-xs"
|
|
195
|
+
title="View API documentation"
|
|
196
|
+
>
|
|
197
|
+
<ExternalLink className="size-3" />
|
|
198
|
+
<span className="sr-only">Docs</span>
|
|
199
|
+
</a>
|
|
187
200
|
)}
|
|
188
201
|
</div>
|
|
189
202
|
|
|
@@ -207,9 +220,7 @@ export function ProviderCard({
|
|
|
207
220
|
<span className="font-medium">Anthropic:</span>{" "}
|
|
208
221
|
<span className="truncate">{provider.anthropicBaseUrl}</span>
|
|
209
222
|
</div>
|
|
210
|
-
{testResults &&
|
|
211
|
-
<TestStatus result={testResults.anthropic.nonStreaming} isTesting={isTesting} />
|
|
212
|
-
)}
|
|
223
|
+
{testResults && <TestStatus result={testResults.anthropic.nonStreaming} />}
|
|
213
224
|
</div>
|
|
214
225
|
)}
|
|
215
226
|
|
|
@@ -219,9 +230,7 @@ export function ProviderCard({
|
|
|
219
230
|
<span className="font-medium">OpenAI:</span>{" "}
|
|
220
231
|
<span className="truncate">{provider.openaiBaseUrl}</span>
|
|
221
232
|
</div>
|
|
222
|
-
{testResults &&
|
|
223
|
-
<TestStatus result={testResults.openai.nonStreaming} isTesting={isTesting} />
|
|
224
|
-
)}
|
|
233
|
+
{testResults && <TestStatus result={testResults.openai.nonStreaming} />}
|
|
225
234
|
</div>
|
|
226
235
|
)}
|
|
227
236
|
|
|
@@ -41,6 +41,7 @@ type ProviderFormProps = {
|
|
|
41
41
|
model?: string;
|
|
42
42
|
format: "anthropic" | "openai";
|
|
43
43
|
baseUrl?: string;
|
|
44
|
+
apiDocsUrl?: string;
|
|
44
45
|
}) => void;
|
|
45
46
|
onCancel: () => void;
|
|
46
47
|
};
|
|
@@ -51,6 +52,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
51
52
|
const [model, setModel] = useState(provider?.model ?? "");
|
|
52
53
|
const [format, setFormat] = useState<"anthropic" | "openai">(provider?.format ?? "anthropic");
|
|
53
54
|
const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? "");
|
|
55
|
+
const [apiDocsUrl, setApiDocsUrl] = useState(provider?.apiDocsUrl ?? "");
|
|
54
56
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
55
57
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
56
58
|
|
|
@@ -69,6 +71,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
69
71
|
setModel(provider.model ?? "");
|
|
70
72
|
setFormat(provider.format ?? "anthropic");
|
|
71
73
|
setBaseUrl(provider.baseUrl ?? "");
|
|
74
|
+
setApiDocsUrl(provider.apiDocsUrl ?? "");
|
|
72
75
|
setManualBaseUrlOverride(false);
|
|
73
76
|
}
|
|
74
77
|
}, [provider]);
|
|
@@ -135,6 +138,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
135
138
|
model: model.trim() || undefined,
|
|
136
139
|
format,
|
|
137
140
|
baseUrl: baseUrl.trim() || undefined,
|
|
141
|
+
apiDocsUrl: apiDocsUrl.trim() || undefined,
|
|
138
142
|
});
|
|
139
143
|
} finally {
|
|
140
144
|
setIsSubmitting(false);
|
|
@@ -246,6 +250,23 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
246
250
|
<p className="text-xs text-muted-foreground">Base URL for the provider API.</p>
|
|
247
251
|
</div>
|
|
248
252
|
|
|
253
|
+
<div className="space-y-2">
|
|
254
|
+
<label htmlFor="provider-api-docs-url" className="text-sm font-medium">
|
|
255
|
+
API Docs URL
|
|
256
|
+
</label>
|
|
257
|
+
<input
|
|
258
|
+
id="provider-api-docs-url"
|
|
259
|
+
type="text"
|
|
260
|
+
value={apiDocsUrl}
|
|
261
|
+
onChange={(e) => setApiDocsUrl(e.target.value)}
|
|
262
|
+
placeholder="https://api.example.com/docs"
|
|
263
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
|
264
|
+
/>
|
|
265
|
+
<p className="text-xs text-muted-foreground">
|
|
266
|
+
Optional API documentation URL. If not set, uses known provider docs.
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
249
270
|
<div className="flex gap-2 justify-end pt-2">
|
|
250
271
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
|
251
272
|
Cancel
|
|
@@ -50,7 +50,12 @@ const AnthropicLogo = React.memo(
|
|
|
50
50
|
|
|
51
51
|
const OpenAILogo = React.memo(
|
|
52
52
|
({ className }: { className?: string }): JSX.Element => (
|
|
53
|
-
<img
|
|
53
|
+
<img
|
|
54
|
+
src={OpenAILogoSvg}
|
|
55
|
+
alt="OpenAI"
|
|
56
|
+
className={className}
|
|
57
|
+
style={{ ...sizeStyle, background: "white", borderRadius: "4px" }}
|
|
58
|
+
/>
|
|
54
59
|
),
|
|
55
60
|
);
|
|
56
61
|
|