@tonyclaw/llm-inspector 1.19.1 → 1.19.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/cli.js +146 -37
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DtERUdIt.js → CompareDrawer-BzTsEelr.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-DfxRK7Nt.js → ProxyViewerContainer-BHm-n-_W.js} +9 -9
- package/.output/public/assets/{ReplayDialog-VMsGnJSI.js → ReplayDialog-Dxxo80xO.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Cx_vluvK.js → RequestAnatomy-D-swiaii.js} +1 -1
- package/.output/public/assets/{ResponseView-5F8Ms5z4.js → ResponseView-DvdH2bGk.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-CKDCWfu9.js → StreamingChunkSequence-D_RzgyKq.js} +1 -1
- package/.output/public/assets/_sessionId-DdODJCYY.js +1 -0
- package/.output/public/assets/{index-DeJyypsp.css → index-Bqi9RAGS.css} +1 -1
- package/.output/public/assets/index-EvnsNPOK.js +1 -0
- package/.output/public/assets/{json-viewer-CztuZ9cT.js → json-viewer-DIHZbEId.js} +1 -1
- package/.output/public/assets/{main-CR9IJlz1.js → main-Br2EjrqZ.js} +2 -2
- package/.output/server/{_sessionId-DvWQaDEm.mjs → _sessionId-CPkCxTP8.mjs} +4 -3
- package/.output/server/_ssr/{CompareDrawer-C5FsxSDS.mjs → CompareDrawer-DKHgXC5-.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-v0cvR8f5.mjs → ProxyViewerContainer-B41D-2Eo.mjs} +57 -9
- package/.output/server/_ssr/{ReplayDialog-C3KOv9OW.mjs → ReplayDialog-D2piRWb0.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-BYRe33eG.mjs → RequestAnatomy-Ce7QdQNP.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-va7yQDeL.mjs → ResponseView-D50UPv-r.mjs} +5 -5
- package/.output/server/_ssr/{StreamingChunkSequence-BJlI-gWl.mjs → StreamingChunkSequence-CDlNFS3Z.mjs} +4 -4
- package/.output/server/_ssr/{index-CS0fA2GT.mjs → index-DhAQxjnZ.mjs} +4 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Dg8rqrxL.mjs → json-viewer-BZRjG_f7.mjs} +4 -4
- package/.output/server/_ssr/{router-D_Boe9Bu.mjs → router-yP98-Gq-.mjs} +126 -105
- package/.output/server/{_tanstack-start-manifest_v-KFXyNRGC.mjs → _tanstack-start-manifest_v-d4a4xlOi.mjs} +1 -1
- package/.output/server/index.mjs +65 -65
- package/README.md +22 -0
- package/package.json +3 -1
- package/src/cli/detect-tools.ts +1 -0
- package/src/cli/templates/skill-onboard.ts +6 -6
- package/src/cli.ts +152 -35
- package/src/components/ProxyViewerContainer.tsx +52 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +1 -0
- package/src/proxy/logFinalizer.ts +7 -3
- package/src/proxy/sessionProcess.ts +14 -7
- package/src/proxy/sessionSupervisor.ts +3 -2
- package/src/proxy/socketTracker.ts +19 -7
- package/styles/globals.css +14 -7
- package/.output/public/assets/_sessionId-C-aKd1Ky.js +0 -1
- package/.output/public/assets/index-B8ttyigz.js +0 -1
package/.output/server/index.mjs
CHANGED
|
@@ -35,110 +35,110 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
-
"/assets/
|
|
38
|
+
"/assets/index-EvnsNPOK.js": {
|
|
39
39
|
"type": "text/javascript; charset=utf-8",
|
|
40
|
-
"etag": '"
|
|
41
|
-
"mtime": "2026-06-
|
|
40
|
+
"etag": '"74-Ttr2CWyviy8DiIX5gb9+i1f50ew"',
|
|
41
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
42
|
+
"size": 116,
|
|
43
|
+
"path": "../public/assets/index-EvnsNPOK.js"
|
|
44
|
+
},
|
|
45
|
+
"/assets/alibaba-TTwafVwX.svg": {
|
|
46
|
+
"type": "image/svg+xml",
|
|
47
|
+
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
48
|
+
"mtime": "2026-06-18T08:23:57.367Z",
|
|
49
|
+
"size": 5915,
|
|
50
|
+
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
51
|
+
},
|
|
52
|
+
"/assets/index-Bqi9RAGS.css": {
|
|
53
|
+
"type": "text/css; charset=utf-8",
|
|
54
|
+
"etag": '"177a9-QMoW/GsYu1jERI0JyFHA9CiB/o0"',
|
|
55
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
56
|
+
"size": 96169,
|
|
57
|
+
"path": "../public/assets/index-Bqi9RAGS.css"
|
|
58
|
+
},
|
|
59
|
+
"/assets/CompareDrawer-BzTsEelr.js": {
|
|
60
|
+
"type": "text/javascript; charset=utf-8",
|
|
61
|
+
"etag": '"4a1f-Qg1+Rt9EXNIgQdrNcwWDBW4GbSo"',
|
|
62
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
42
63
|
"size": 18975,
|
|
43
|
-
"path": "../public/assets/CompareDrawer-
|
|
64
|
+
"path": "../public/assets/CompareDrawer-BzTsEelr.js"
|
|
44
65
|
},
|
|
45
66
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
46
67
|
"type": "image/jpeg",
|
|
47
68
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
48
|
-
"mtime": "2026-06-
|
|
69
|
+
"mtime": "2026-06-18T08:23:57.370Z",
|
|
49
70
|
"size": 6918,
|
|
50
71
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
51
72
|
},
|
|
52
|
-
"/assets/
|
|
73
|
+
"/assets/main-Br2EjrqZ.js": {
|
|
53
74
|
"type": "text/javascript; charset=utf-8",
|
|
54
|
-
"etag": '"
|
|
55
|
-
"mtime": "2026-06-
|
|
56
|
-
"size":
|
|
57
|
-
"path": "../public/assets/
|
|
75
|
+
"etag": '"50a37-vmFQ+AWLZBZB2He0hGozNaRNVJI"',
|
|
76
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
77
|
+
"size": 330295,
|
|
78
|
+
"path": "../public/assets/main-Br2EjrqZ.js"
|
|
58
79
|
},
|
|
59
|
-
"/assets/json-viewer-
|
|
80
|
+
"/assets/json-viewer-DIHZbEId.js": {
|
|
60
81
|
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"1e651-
|
|
62
|
-
"mtime": "2026-06-
|
|
82
|
+
"etag": '"1e651-45Abi6ZVH/q1l0aOWq20PEO9Tcg"',
|
|
83
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
63
84
|
"size": 124497,
|
|
64
|
-
"path": "../public/assets/json-viewer-
|
|
65
|
-
},
|
|
66
|
-
"/assets/index-DeJyypsp.css": {
|
|
67
|
-
"type": "text/css; charset=utf-8",
|
|
68
|
-
"etag": '"17504-zMXz49VysEK/ru01MvYLc8/SiPI"',
|
|
69
|
-
"mtime": "2026-06-18T01:14:51.128Z",
|
|
70
|
-
"size": 95492,
|
|
71
|
-
"path": "../public/assets/index-DeJyypsp.css"
|
|
85
|
+
"path": "../public/assets/json-viewer-DIHZbEId.js"
|
|
72
86
|
},
|
|
73
|
-
"/assets/RequestAnatomy-
|
|
87
|
+
"/assets/RequestAnatomy-D-swiaii.js": {
|
|
74
88
|
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"142a-
|
|
76
|
-
"mtime": "2026-06-
|
|
89
|
+
"etag": '"142a-gnEHcgfEHRgV0NMxY0iXIZHIAC0"',
|
|
90
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
77
91
|
"size": 5162,
|
|
78
|
-
"path": "../public/assets/RequestAnatomy-
|
|
92
|
+
"path": "../public/assets/RequestAnatomy-D-swiaii.js"
|
|
79
93
|
},
|
|
80
|
-
"/assets/ResponseView-
|
|
94
|
+
"/assets/ResponseView-DvdH2bGk.js": {
|
|
81
95
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"6e91-
|
|
83
|
-
"mtime": "2026-06-
|
|
96
|
+
"etag": '"6e91-Q4HU85yuyXxRWZzw+CnDW/vnjAo"',
|
|
97
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
84
98
|
"size": 28305,
|
|
85
|
-
"path": "../public/assets/ResponseView-
|
|
99
|
+
"path": "../public/assets/ResponseView-DvdH2bGk.js"
|
|
86
100
|
},
|
|
87
|
-
"/assets/
|
|
101
|
+
"/assets/ReplayDialog-Dxxo80xO.js": {
|
|
88
102
|
"type": "text/javascript; charset=utf-8",
|
|
89
|
-
"etag": '"
|
|
90
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"11c0-L7iZUpQjkDLpKkR+MgAZO1Q6ns8"',
|
|
104
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
105
|
+
"size": 4544,
|
|
106
|
+
"path": "../public/assets/ReplayDialog-Dxxo80xO.js"
|
|
107
|
+
},
|
|
108
|
+
"/assets/StreamingChunkSequence-D_RzgyKq.js": {
|
|
109
|
+
"type": "text/javascript; charset=utf-8",
|
|
110
|
+
"etag": '"d81-8i4/6r70kba84fXrnFFC/+/S8Sw"',
|
|
111
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
91
112
|
"size": 3457,
|
|
92
|
-
"path": "../public/assets/StreamingChunkSequence-
|
|
113
|
+
"path": "../public/assets/StreamingChunkSequence-D_RzgyKq.js"
|
|
93
114
|
},
|
|
94
115
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
95
116
|
"type": "image/svg+xml",
|
|
96
117
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
97
|
-
"mtime": "2026-06-
|
|
118
|
+
"mtime": "2026-06-18T08:23:57.370Z",
|
|
98
119
|
"size": 11256,
|
|
99
120
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
100
121
|
},
|
|
101
|
-
"/assets/_sessionId-
|
|
122
|
+
"/assets/_sessionId-DdODJCYY.js": {
|
|
102
123
|
"type": "text/javascript; charset=utf-8",
|
|
103
|
-
"etag": '"d2-
|
|
104
|
-
"mtime": "2026-06-
|
|
124
|
+
"etag": '"d2-f13W0xvGzYOxgV34bdQslyMj4WE"',
|
|
125
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
105
126
|
"size": 210,
|
|
106
|
-
"path": "../public/assets/_sessionId-
|
|
107
|
-
},
|
|
108
|
-
"/assets/main-CR9IJlz1.js": {
|
|
109
|
-
"type": "text/javascript; charset=utf-8",
|
|
110
|
-
"etag": '"50a37-XeRUA7+G6lgGxJXRg+juq8ZZbMM"',
|
|
111
|
-
"mtime": "2026-06-18T01:14:51.128Z",
|
|
112
|
-
"size": 330295,
|
|
113
|
-
"path": "../public/assets/main-CR9IJlz1.js"
|
|
114
|
-
},
|
|
115
|
-
"/assets/alibaba-TTwafVwX.svg": {
|
|
116
|
-
"type": "image/svg+xml",
|
|
117
|
-
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
118
|
-
"mtime": "2026-06-18T01:14:51.128Z",
|
|
119
|
-
"size": 5915,
|
|
120
|
-
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
121
|
-
},
|
|
122
|
-
"/assets/ProxyViewerContainer-DfxRK7Nt.js": {
|
|
123
|
-
"type": "text/javascript; charset=utf-8",
|
|
124
|
-
"etag": '"76d0d-d2XlfO92n/Dltmn6NOHQY/Ui+fw"',
|
|
125
|
-
"mtime": "2026-06-18T01:14:51.128Z",
|
|
126
|
-
"size": 486669,
|
|
127
|
-
"path": "../public/assets/ProxyViewerContainer-DfxRK7Nt.js"
|
|
127
|
+
"path": "../public/assets/_sessionId-DdODJCYY.js"
|
|
128
128
|
},
|
|
129
129
|
"/assets/qwen-CONDcHqt.png": {
|
|
130
130
|
"type": "image/png",
|
|
131
131
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
132
|
-
"mtime": "2026-06-
|
|
132
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
133
133
|
"size": 357059,
|
|
134
134
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
135
135
|
},
|
|
136
|
-
"/assets/
|
|
136
|
+
"/assets/ProxyViewerContainer-BHm-n-_W.js": {
|
|
137
137
|
"type": "text/javascript; charset=utf-8",
|
|
138
|
-
"etag": '"
|
|
139
|
-
"mtime": "2026-06-
|
|
140
|
-
"size":
|
|
141
|
-
"path": "../public/assets/
|
|
138
|
+
"etag": '"76fed-fJVWNR0ZLTxGc3xnZxX5o1sFXvc"',
|
|
139
|
+
"mtime": "2026-06-18T08:23:57.371Z",
|
|
140
|
+
"size": 487405,
|
|
141
|
+
"path": "../public/assets/ProxyViewerContainer-BHm-n-_W.js"
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
144
|
function readAsset(id) {
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- **Web UI**:`http://localhost:25947`
|
|
19
19
|
- **Proxy**:`http://localhost:25947/proxy`
|
|
20
20
|
- **MCP Server**:`http://localhost:25947/api/mcp`
|
|
21
|
+
- **Chrome Companion**:`extensions/chrome`(通过 `chrome://extensions` 以 Load unpacked 方式加载)
|
|
21
22
|
|
|
22
23
|
## 核心能力
|
|
23
24
|
|
|
@@ -53,6 +54,8 @@ llm-inspector
|
|
|
53
54
|
```bash
|
|
54
55
|
llm-inspector --no-open
|
|
55
56
|
llm-inspector --port 3000
|
|
57
|
+
llm-inspector --background
|
|
58
|
+
llm-inspector --force-restart
|
|
56
59
|
llm-inspector --config-dir ./local-config
|
|
57
60
|
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"}]'
|
|
58
61
|
```
|
|
@@ -73,6 +76,23 @@ bun install
|
|
|
73
76
|
bun run dev
|
|
74
77
|
```
|
|
75
78
|
|
|
79
|
+
### Chrome Companion 扩展
|
|
80
|
+
|
|
81
|
+
仓库内提供了一个轻量 Chrome 侧边栏扩展,用来连接本机 llm-inspector、查看运行状态和最近请求,并快速打开 Dashboard 或复制代理命令。
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
extensions/chrome
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
启动方式:
|
|
88
|
+
|
|
89
|
+
1. 先运行 `llm-inspector` 或 `bun run dev`。
|
|
90
|
+
2. 打开 `chrome://extensions`。
|
|
91
|
+
3. 开启 **Developer mode**。
|
|
92
|
+
4. 点击 **Load unpacked**。
|
|
93
|
+
5. 选择 `extensions/chrome` 目录。
|
|
94
|
+
6. 点击扩展图标打开侧边栏。
|
|
95
|
+
|
|
76
96
|
## 配置 Provider
|
|
77
97
|
|
|
78
98
|
打开 Web UI,进入 **Settings** 添加 Provider:
|
|
@@ -315,6 +335,8 @@ ANTHROPIC_BASE_URL=http://localhost:25947/proxy claude
|
|
|
315
335
|
- [架构说明](docs/Architecture.md)
|
|
316
336
|
- [开发指南](docs/Development.md)
|
|
317
337
|
- [MCP Server](docs/MCP-Server.md)
|
|
338
|
+
- [CHANGELOG](CHANGELOG.md) — release notes
|
|
339
|
+
- [CONTRIBUTING](CONTRIBUTING.md) — how to land a change
|
|
318
340
|
|
|
319
341
|
## License
|
|
320
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonyclaw/llm-inspector",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
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",
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"lint": "eslint .",
|
|
41
41
|
"format": "biome format --write .",
|
|
42
42
|
"format:check": "biome format .",
|
|
43
|
+
"extension:zip": "node scripts/pack-chrome-extension.mjs",
|
|
44
|
+
"extension:release": "node scripts/release-chrome-extension.mjs",
|
|
43
45
|
"knip": "knip",
|
|
44
46
|
"check": "bun format && bun typecheck && bun lint",
|
|
45
47
|
"prepare": "husky",
|
package/src/cli/detect-tools.ts
CHANGED
|
@@ -47,6 +47,7 @@ function which(bin: string): string | null {
|
|
|
47
47
|
const out = execFileSync("where", [bin], {
|
|
48
48
|
encoding: "utf8",
|
|
49
49
|
timeout: 3000,
|
|
50
|
+
windowsHide: true,
|
|
50
51
|
stdio: ["ignore", "pipe", "ignore"],
|
|
51
52
|
});
|
|
52
53
|
const first = out.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
@@ -257,15 +257,15 @@ New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
|
257
257
|
|
|
258
258
|
## Phase 3: Start proxy
|
|
259
259
|
|
|
260
|
-
**EXPLAIN:** "Time to start the proxy. It binds to port ${port} by default,
|
|
260
|
+
**EXPLAIN:** "Time to start the proxy. It binds to port ${port} by default, reuses an already-running healthy llm-inspector, and prints the URL. Use \`--force-restart\` only when you intentionally want to replace the existing process."
|
|
261
261
|
|
|
262
262
|
**DO:** Skip this phase entirely if the Phase 0 health check already reported \`PROXY: up\` and the user opted to skip done phases.
|
|
263
263
|
|
|
264
|
-
**DO:** Otherwise, start the proxy
|
|
264
|
+
**DO:** Otherwise, start the proxy with the explicit \`--background --no-open\` flags. On Windows, resolve the npm shim first and launch it through \`Start-Process -WindowStyle Hidden\` so setup does not flash a command window.
|
|
265
265
|
|
|
266
266
|
\`\`\`bash
|
|
267
267
|
# Unix / macOS / WSL
|
|
268
|
-
|
|
268
|
+
llm-inspector --background --no-open > /tmp/llm-inspector.log 2>&1
|
|
269
269
|
\`\`\`
|
|
270
270
|
|
|
271
271
|
\`\`\`powershell
|
|
@@ -278,14 +278,14 @@ $err = Join-Path $env:TEMP 'llm-inspector.err.log'
|
|
|
278
278
|
$found = Get-Command llm-inspector -ErrorAction SilentlyContinue
|
|
279
279
|
if ($found) {
|
|
280
280
|
$shim = $found.Source
|
|
281
|
-
$args = '--no-open'
|
|
281
|
+
$args = '--background','--no-open'
|
|
282
282
|
} elseif (Test-Path (Join-Path $env:APPDATA 'npm/llm-inspector.cmd')) {
|
|
283
283
|
$shim = Join-Path $env:APPDATA 'npm/llm-inspector.cmd'
|
|
284
|
-
$args = '--no-open'
|
|
284
|
+
$args = '--background','--no-open'
|
|
285
285
|
} else {
|
|
286
286
|
# bin not on PATH and not at the default npm prefix — let cmd resolve it
|
|
287
287
|
$shim = 'cmd.exe'
|
|
288
|
-
$args = '/c','
|
|
288
|
+
$args = '/c','llm-inspector','--background','--no-open'
|
|
289
289
|
}
|
|
290
290
|
Start-Process -FilePath $shim -ArgumentList $args -RedirectStandardOutput $log -RedirectStandardError $err -WindowStyle Hidden
|
|
291
291
|
\`\`\`
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn, execSync } from "node:child_process";
|
|
2
|
+
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
@@ -8,6 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
10
|
|
|
10
11
|
const DEFAULT_PORT = 25947;
|
|
12
|
+
const LOCAL_PROBE_TIMEOUT_MS = 2000;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Subcommand router. The legacy one-liner UX (`llm-inspector` with no args,
|
|
@@ -23,18 +25,107 @@ if (subcommand === "onboard") {
|
|
|
23
25
|
process.exit(code);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
runStart(process.argv.slice(2));
|
|
28
|
+
await runStart(process.argv.slice(2));
|
|
27
29
|
|
|
28
30
|
// -----------------------------------------------------------------------------
|
|
29
31
|
// Legacy `start` behavior — start the proxy on the configured port. Extracted
|
|
30
32
|
// into a function so the router above can keep the top-level flow readable.
|
|
31
33
|
// -----------------------------------------------------------------------------
|
|
32
|
-
function
|
|
34
|
+
async function isInspectorHealthy(port: number): Promise<boolean> {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
39
|
+
cache: "no-store",
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
return response.ok;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPortAcceptingConnections(port: number): Promise<boolean> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
53
|
+
const finish = (value: boolean): void => {
|
|
54
|
+
socket.removeAllListeners();
|
|
55
|
+
socket.destroy();
|
|
56
|
+
resolve(value);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
socket.setTimeout(LOCAL_PROBE_TIMEOUT_MS);
|
|
60
|
+
socket.once("connect", () => finish(true));
|
|
61
|
+
socket.once("timeout", () => finish(false));
|
|
62
|
+
socket.once("error", () => finish(false));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sleep(ms: number): Promise<void> {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
setTimeout(resolve, ms);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function waitForInspectorHealthy(port: number, timeoutMs: number): Promise<boolean> {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (Date.now() - start < timeoutMs) {
|
|
75
|
+
if (await isInspectorHealthy(port)) return true;
|
|
76
|
+
await sleep(250);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openBrowser(targetUrl: string): void {
|
|
82
|
+
let command: string[] | undefined;
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
84
|
+
switch (process.platform) {
|
|
85
|
+
case "darwin":
|
|
86
|
+
command = ["open", targetUrl];
|
|
87
|
+
break;
|
|
88
|
+
case "linux":
|
|
89
|
+
command = ["xdg-open", targetUrl];
|
|
90
|
+
break;
|
|
91
|
+
case "win32":
|
|
92
|
+
command = ["cmd", "/c", "start", "", targetUrl];
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
// Unsupported platform - do nothing
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (command === undefined) return;
|
|
99
|
+
const [bin, ...cmdArgs] = command;
|
|
100
|
+
if (bin === undefined) return;
|
|
101
|
+
const browserProcess = spawn(bin, cmdArgs, {
|
|
102
|
+
stdio: "ignore",
|
|
103
|
+
detached: true,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
});
|
|
106
|
+
browserProcess.unref();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function waitForProcessExit(child: ChildProcess): Promise<number> {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
child.once("exit", (code) => {
|
|
112
|
+
resolve(code ?? 1);
|
|
113
|
+
});
|
|
114
|
+
child.once("error", () => {
|
|
115
|
+
resolve(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runStart(args: string[]): Promise<void> {
|
|
33
121
|
const envPort = process.env["PORT"];
|
|
34
122
|
const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
|
|
35
123
|
|
|
36
124
|
let port = portDefault;
|
|
37
125
|
let open = true;
|
|
126
|
+
let openWasSpecified = false;
|
|
127
|
+
let background = false;
|
|
128
|
+
let forceRestart = false;
|
|
38
129
|
let configDir: string | undefined;
|
|
39
130
|
let providersJson: string | undefined;
|
|
40
131
|
|
|
@@ -48,9 +139,18 @@ function runStart(args: string[]): void {
|
|
|
48
139
|
break;
|
|
49
140
|
case "--no-open":
|
|
50
141
|
open = false;
|
|
142
|
+
openWasSpecified = true;
|
|
51
143
|
break;
|
|
52
144
|
case "--open":
|
|
53
145
|
open = true;
|
|
146
|
+
openWasSpecified = true;
|
|
147
|
+
break;
|
|
148
|
+
case "--force-restart":
|
|
149
|
+
case "--restart":
|
|
150
|
+
forceRestart = true;
|
|
151
|
+
break;
|
|
152
|
+
case "--background":
|
|
153
|
+
background = true;
|
|
54
154
|
break;
|
|
55
155
|
case "--config-dir":
|
|
56
156
|
configDir = args[i + 1];
|
|
@@ -65,6 +165,12 @@ function runStart(args: string[]): void {
|
|
|
65
165
|
}
|
|
66
166
|
}
|
|
67
167
|
|
|
168
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
169
|
+
console.error(`Invalid port: ${String(port)}. Use --port <1-65535>.`);
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
68
174
|
/**
|
|
69
175
|
* Check if a port is in use and kill the process using it
|
|
70
176
|
*/
|
|
@@ -79,6 +185,7 @@ function runStart(args: string[]): void {
|
|
|
79
185
|
const output = execSync(`netstat -ano | findstr :${targetPort}`, {
|
|
80
186
|
encoding: "utf8",
|
|
81
187
|
timeout: 5000,
|
|
188
|
+
windowsHide: true,
|
|
82
189
|
});
|
|
83
190
|
const lines = output.trim().split("\n");
|
|
84
191
|
for (const line of lines) {
|
|
@@ -99,8 +206,12 @@ function runStart(args: string[]): void {
|
|
|
99
206
|
|
|
100
207
|
for (const pid of pids) {
|
|
101
208
|
try {
|
|
102
|
-
console.log(`Killing process ${pid} on port ${
|
|
103
|
-
execSync(`taskkill /PID ${pid} /F`, {
|
|
209
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
210
|
+
execSync(`taskkill /PID ${pid} /F`, {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
timeout: 5000,
|
|
213
|
+
windowsHide: true,
|
|
214
|
+
});
|
|
104
215
|
} catch {
|
|
105
216
|
// Process may have already exited
|
|
106
217
|
}
|
|
@@ -120,7 +231,7 @@ function runStart(args: string[]): void {
|
|
|
120
231
|
|
|
121
232
|
for (const pid of pids) {
|
|
122
233
|
try {
|
|
123
|
-
console.log(`Killing process ${pid} on port ${
|
|
234
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
124
235
|
execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5000 });
|
|
125
236
|
} catch {
|
|
126
237
|
// Process may have already exited
|
|
@@ -134,11 +245,28 @@ function runStart(args: string[]): void {
|
|
|
134
245
|
|
|
135
246
|
process.env["PORT"] = String(port);
|
|
136
247
|
|
|
137
|
-
// Kill any existing process on the port
|
|
138
|
-
killProcessOnPort(port);
|
|
139
|
-
|
|
140
248
|
const url = `http://localhost:${port}`;
|
|
141
249
|
|
|
250
|
+
if (!forceRestart && (await isInspectorHealthy(port))) {
|
|
251
|
+
console.log(`llm-inspector is already running at ${url}`);
|
|
252
|
+
console.log(`Use --force-restart to restart the existing instance.`);
|
|
253
|
+
if (open && openWasSpecified) {
|
|
254
|
+
openBrowser(url);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!forceRestart && (await isPortAcceptingConnections(port))) {
|
|
260
|
+
console.error(`Port ${port} is already in use, but it is not a healthy llm-inspector.`);
|
|
261
|
+
console.error(`Stop that process, choose --port <n>, or re-run with --force-restart.`);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (forceRestart) {
|
|
267
|
+
killProcessOnPort(port);
|
|
268
|
+
}
|
|
269
|
+
|
|
142
270
|
console.log(`Server running at ${url}`);
|
|
143
271
|
console.log(` Proxy: ${url}/proxy`);
|
|
144
272
|
console.log(``);
|
|
@@ -157,29 +285,6 @@ function runStart(args: string[]): void {
|
|
|
157
285
|
` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`,
|
|
158
286
|
);
|
|
159
287
|
|
|
160
|
-
const openBrowser = (targetUrl: string): void => {
|
|
161
|
-
let command: string[] | undefined;
|
|
162
|
-
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
163
|
-
switch (process.platform) {
|
|
164
|
-
case "darwin":
|
|
165
|
-
command = ["open", targetUrl];
|
|
166
|
-
break;
|
|
167
|
-
case "linux":
|
|
168
|
-
command = ["xdg-open", targetUrl];
|
|
169
|
-
break;
|
|
170
|
-
case "win32":
|
|
171
|
-
command = ["cmd", "/c", "start", targetUrl];
|
|
172
|
-
break;
|
|
173
|
-
default:
|
|
174
|
-
// Unsupported platform - do nothing
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
if (command === undefined) return;
|
|
178
|
-
const [bin, ...cmdArgs] = command;
|
|
179
|
-
if (bin === undefined) return;
|
|
180
|
-
spawn(bin, cmdArgs, { stdio: "ignore", detached: true });
|
|
181
|
-
};
|
|
182
|
-
|
|
183
288
|
if (open) {
|
|
184
289
|
openBrowser(url);
|
|
185
290
|
}
|
|
@@ -211,10 +316,22 @@ function runStart(args: string[]): void {
|
|
|
211
316
|
serverEnv["LLM_INSPECTOR_PROVIDERS_JSON"] = providersJson;
|
|
212
317
|
}
|
|
213
318
|
const serverProcess = spawn(process.execPath, [serverPath], {
|
|
214
|
-
stdio: ["ignore", "
|
|
215
|
-
detached:
|
|
319
|
+
stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
320
|
+
detached: background,
|
|
216
321
|
env: serverEnv,
|
|
322
|
+
windowsHide: background,
|
|
217
323
|
});
|
|
218
324
|
|
|
219
|
-
|
|
325
|
+
if (background) {
|
|
326
|
+
serverProcess.unref();
|
|
327
|
+
if (await waitForInspectorHealthy(port, 5000)) {
|
|
328
|
+
console.log(`llm-inspector background server is ready at ${url}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
console.error(`llm-inspector background server did not become ready at ${url}.`);
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
process.exitCode = await waitForProcessExit(serverProcess);
|
|
220
337
|
}
|
|
@@ -61,6 +61,8 @@ function filterLogs(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const DEBOUNCE_MS = 50;
|
|
64
|
+
const HASH_SCROLL_ATTEMPTS = 12;
|
|
65
|
+
const HASH_HIGHLIGHT_MS = 1800;
|
|
64
66
|
|
|
65
67
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
66
68
|
if (sessionId === undefined) return "/api/logs/stream";
|
|
@@ -93,6 +95,7 @@ export function ProxyViewerContainer({
|
|
|
93
95
|
const [error, setError] = useState<string | null>(null);
|
|
94
96
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
95
97
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
+
const handledHashRef = useRef<string | null>(null);
|
|
96
99
|
|
|
97
100
|
// O(1) log lookup by id
|
|
98
101
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
@@ -214,6 +217,55 @@ export function ProxyViewerContainer({
|
|
|
214
217
|
};
|
|
215
218
|
}, [connectSSE]);
|
|
216
219
|
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const hash = window.location.hash;
|
|
222
|
+
if (!hash.startsWith("#log-")) return;
|
|
223
|
+
if (handledHashRef.current === hash) return;
|
|
224
|
+
const targetId = hash.slice(1);
|
|
225
|
+
let cancelled = false;
|
|
226
|
+
let attempts = 0;
|
|
227
|
+
let highlightedTarget: HTMLElement | null = null;
|
|
228
|
+
let highlightTimer: number | null = null;
|
|
229
|
+
|
|
230
|
+
const tryScrollToLog = (): void => {
|
|
231
|
+
if (cancelled) return;
|
|
232
|
+
const target = document.getElementById(targetId);
|
|
233
|
+
if (target !== null) {
|
|
234
|
+
handledHashRef.current = hash;
|
|
235
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
236
|
+
if (target instanceof HTMLElement) {
|
|
237
|
+
highlightedTarget = target;
|
|
238
|
+
target.setAttribute("data-deep-link-highlight", "true");
|
|
239
|
+
highlightTimer = window.setTimeout(() => {
|
|
240
|
+
target.removeAttribute("data-deep-link-highlight");
|
|
241
|
+
}, HASH_HIGHLIGHT_MS);
|
|
242
|
+
target.focus({ preventScroll: true });
|
|
243
|
+
if (target.getAttribute("data-nav-action") === "expand") {
|
|
244
|
+
target.click();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
attempts += 1;
|
|
251
|
+
if (attempts < HASH_SCROLL_ATTEMPTS) {
|
|
252
|
+
window.setTimeout(tryScrollToLog, 100);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
tryScrollToLog();
|
|
257
|
+
|
|
258
|
+
return () => {
|
|
259
|
+
cancelled = true;
|
|
260
|
+
if (highlightTimer !== null) {
|
|
261
|
+
window.clearTimeout(highlightTimer);
|
|
262
|
+
}
|
|
263
|
+
if (highlightedTarget !== null) {
|
|
264
|
+
highlightedTarget.removeAttribute("data-deep-link-highlight");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}, [logs.length]);
|
|
268
|
+
|
|
217
269
|
const handleClearAll = useCallback(() => {
|
|
218
270
|
if (initialSessionId !== undefined && allLogs.length === 0) return;
|
|
219
271
|
void (async () => {
|