@tonyclaw/llm-inspector 1.14.0 → 1.14.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.
@@ -1,4 +1,4 @@
1
- const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-C1k6vRnH.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-C6tbslcs.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/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/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/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-C1k6vRnH.js" });
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BElVT2p3.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DDrUlr6L.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/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/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/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", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-BElVT2p3.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -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-10T06:24:38.532Z",
41
+ "mtime": "2026-06-10T13:27:43.020Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/index-B5q3Llgm.css": {
45
+ "/assets/minimax-BPMzvuL-.jpeg": {
46
+ "type": "image/jpeg",
47
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
48
+ "mtime": "2026-06-10T13:27:43.023Z",
49
+ "size": 6918,
50
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
51
+ },
52
+ "/assets/index-DOG5AdQ9.css": {
46
53
  "type": "text/css; charset=utf-8",
47
- "etag": '"13e7d-80ipn+yqCPCa5t/vCr1MDg4cP4g"',
48
- "mtime": "2026-06-10T06:24:38.532Z",
49
- "size": 81533,
50
- "path": "../public/assets/index-B5q3Llgm.css"
54
+ "etag": '"142da-qXGGlxiu6GXrwGw4lzJEDsR446o"',
55
+ "mtime": "2026-06-10T13:27:43.023Z",
56
+ "size": 82650,
57
+ "path": "../public/assets/index-DOG5AdQ9.css"
51
58
  },
52
59
  "/assets/zhipuai-BPNAnxo-.svg": {
53
60
  "type": "image/svg+xml",
54
61
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
55
- "mtime": "2026-06-10T06:24:38.529Z",
62
+ "mtime": "2026-06-10T13:27:43.023Z",
56
63
  "size": 11256,
57
64
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
58
65
  },
59
- "/assets/main-C1k6vRnH.js": {
60
- "type": "text/javascript; charset=utf-8",
61
- "etag": '"50599-AYrUeIFAuGF/4DPZ+5lAMslWd7Q"',
62
- "mtime": "2026-06-10T06:24:38.532Z",
63
- "size": 329113,
64
- "path": "../public/assets/main-C1k6vRnH.js"
65
- },
66
- "/assets/minimax-BPMzvuL-.jpeg": {
67
- "type": "image/jpeg",
68
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
69
- "mtime": "2026-06-10T06:24:38.532Z",
70
- "size": 6918,
71
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
72
- },
73
66
  "/assets/qwen-CONDcHqt.png": {
74
67
  "type": "image/png",
75
68
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-10T06:24:38.532Z",
69
+ "mtime": "2026-06-10T13:27:43.023Z",
77
70
  "size": 357059,
78
71
  "path": "../public/assets/qwen-CONDcHqt.png"
79
72
  },
80
- "/assets/index-C6tbslcs.js": {
73
+ "/assets/main-BElVT2p3.js": {
74
+ "type": "text/javascript; charset=utf-8",
75
+ "etag": '"50599-c0Re5vg5+KI0J9dHMI2SM+wdp9A"',
76
+ "mtime": "2026-06-10T13:27:43.023Z",
77
+ "size": 329113,
78
+ "path": "../public/assets/main-BElVT2p3.js"
79
+ },
80
+ "/assets/index-DDrUlr6L.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"90252-JCPOfSZg2vTS69tqlOJ4qp75AeM"',
83
- "mtime": "2026-06-10T06:24:38.532Z",
84
- "size": 590418,
85
- "path": "../public/assets/index-C6tbslcs.js"
82
+ "etag": '"91fb4-sGd/w9UW0TGYuvQ5WfgvmwcNukI"',
83
+ "mtime": "2026-06-10T13:27:43.024Z",
84
+ "size": 597940,
85
+ "path": "../public/assets/index-DDrUlr6L.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.14.0",
3
+ "version": "1.14.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",
@@ -1,4 +1,4 @@
1
- import { type JSX, type ReactNode, useState } from "react";
1
+ import { type JSX, type ReactNode, useCallback, useRef, useState } from "react";
2
2
  import { Button } from "../ui/button";
3
3
  import {
4
4
  Eye,
@@ -17,8 +17,11 @@ import {
17
17
  Server,
18
18
  HelpCircle,
19
19
  Clock,
20
+ Copy,
21
+ Check,
20
22
  } from "lucide-react";
21
23
  import type { ProviderConfig } from "../../proxy/providers";
24
+ import { maskApiKey } from "../../lib/mask";
22
25
  import type {
23
26
  ProviderTestErrorType as ErrorType,
24
27
  ProviderTestResult as TestResult,
@@ -42,11 +45,6 @@ type ProviderCardProps = {
42
45
  onTest?: (providerId: string) => void;
43
46
  };
44
47
 
45
- function maskApiKey(apiKey: string): string {
46
- if (apiKey.length <= 8) return "••••••••";
47
- return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
48
- }
49
-
50
48
  function hasSuccessField(result: ProviderTestState): result is TestResult {
51
49
  return Object.prototype.hasOwnProperty.call(result, "success");
52
50
  }
@@ -171,6 +169,20 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
171
169
  );
172
170
  }
173
171
 
172
+ function formatTimeAgo(isoString: string): string {
173
+ const now = Date.now();
174
+ const then = new Date(isoString).getTime();
175
+ const diffMs = now - then;
176
+ const diffSec = Math.floor(diffMs / 1000);
177
+ if (diffSec < 60) return "just now";
178
+ const diffMin = Math.floor(diffSec / 60);
179
+ if (diffMin < 60) return `${diffMin}m ago`;
180
+ const diffHr = Math.floor(diffMin / 60);
181
+ if (diffHr < 24) return `${diffHr}h ago`;
182
+ const diffDay = Math.floor(diffHr / 24);
183
+ return `${diffDay}d ago`;
184
+ }
185
+
174
186
  export function ProviderCard({
175
187
  provider,
176
188
  testResults,
@@ -182,6 +194,39 @@ export function ProviderCard({
182
194
  onTest,
183
195
  }: ProviderCardProps): JSX.Element {
184
196
  const [showApiKey, setShowApiKey] = useState(false);
197
+ const [copied, setCopied] = useState(false);
198
+ const [showModelResults, setShowModelResults] = useState(false);
199
+ const hasLoggedRef = useRef<string | null>(null);
200
+ const lastTestedAtRef = useRef<string | undefined>(undefined);
201
+
202
+ // Reset log state when new test results arrive
203
+ if (testResults?.testedAt !== undefined && testResults.testedAt !== lastTestedAtRef.current) {
204
+ lastTestedAtRef.current = testResults.testedAt;
205
+ hasLoggedRef.current = null;
206
+ }
207
+
208
+ // Call log API when user expands model test results for the first time
209
+ const handleToggleModelResults = useCallback(() => {
210
+ setShowModelResults((v) => {
211
+ const next = !v;
212
+ if (next && hasLoggedRef.current === null && testResults?.models !== undefined) {
213
+ hasLoggedRef.current = testResults.testedAt ?? "";
214
+ // Fire-and-forget: commit test results to dashboard log
215
+ void fetch(`/api/providers/${provider.id}/test/log`, {
216
+ method: "POST",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify(testResults),
219
+ });
220
+ }
221
+ return next;
222
+ });
223
+ }, [provider.id, testResults]);
224
+
225
+ function handleCopy() {
226
+ navigator.clipboard.writeText(provider.apiKey).catch(() => {});
227
+ setCopied(true);
228
+ setTimeout(() => setCopied(false), 2000);
229
+ }
185
230
 
186
231
  // Get docs URL: use provider.apiDocsUrl if configured, otherwise check KNOWN_PROVIDER_DOCS
187
232
  const docsUrl =
@@ -196,11 +241,17 @@ export function ProviderCard({
196
241
  >
197
242
  <div className="flex items-start justify-between gap-2">
198
243
  <div className="flex items-center gap-2 min-w-0">
199
- <span className="font-medium truncate">
200
- {provider.model !== undefined && provider.model !== ""
201
- ? `${provider.model} (${provider.name})`
202
- : provider.name}
203
- </span>
244
+ <span className="font-medium truncate">{provider.name}</span>
245
+ {provider.source === "company" && (
246
+ <span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0">
247
+ 公司
248
+ </span>
249
+ )}
250
+ {provider.source === "personal" && (
251
+ <span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 shrink-0">
252
+ 个人
253
+ </span>
254
+ )}
204
255
  </div>
205
256
  {docsUrl !== undefined && (
206
257
  <a
@@ -216,6 +267,16 @@ export function ProviderCard({
216
267
  )}
217
268
  </div>
218
269
 
270
+ {provider.models !== undefined && provider.models.length > 0 && (
271
+ <div className="flex flex-wrap gap-1">
272
+ {provider.models.map((m) => (
273
+ <span key={m} className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
274
+ {m}
275
+ </span>
276
+ ))}
277
+ </div>
278
+ )}
279
+
219
280
  <div className="flex items-center gap-2">
220
281
  <code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate">
221
282
  {showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
@@ -228,6 +289,14 @@ export function ProviderCard({
228
289
  >
229
290
  {showApiKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
230
291
  </button>
292
+ <button
293
+ type="button"
294
+ onClick={handleCopy}
295
+ className="text-muted-foreground hover:text-foreground transition-colors p-1"
296
+ aria-label="Copy API key"
297
+ >
298
+ {copied ? <Check className="size-4 text-green-500" /> : <Copy className="size-4" />}
299
+ </button>
231
300
  </div>
232
301
 
233
302
  {provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
@@ -250,6 +319,65 @@ export function ProviderCard({
250
319
  </div>
251
320
  )}
252
321
 
322
+ {testResults?.testedAt !== undefined && (
323
+ <div className="text-xs text-muted-foreground flex items-center gap-1">
324
+ <Clock className="size-3" />
325
+ <span>Tested {formatTimeAgo(testResults.testedAt)}</span>
326
+ </div>
327
+ )}
328
+ {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
329
+ <div className="border-t pt-2">
330
+ <button
331
+ type="button"
332
+ onClick={handleToggleModelResults}
333
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
334
+ >
335
+ <span className="font-mono">{showModelResults ? "▾" : "▸"}</span>
336
+ Model Test Results ({Object.keys(testResults.models).length})
337
+ </button>
338
+ {showModelResults && (
339
+ <div className="mt-2 overflow-x-auto">
340
+ <table className="w-full text-xs border-collapse">
341
+ <thead>
342
+ <tr className="border-b border-border">
343
+ <th className="text-left py-1 px-2 font-medium text-muted-foreground">Model</th>
344
+ {provider.anthropicBaseUrl !== undefined &&
345
+ provider.anthropicBaseUrl !== "" && (
346
+ <th className="text-left py-1 px-2 font-medium text-muted-foreground">
347
+ Anthropic
348
+ </th>
349
+ )}
350
+ {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
351
+ <th className="text-left py-1 px-2 font-medium text-muted-foreground">
352
+ OpenAI
353
+ </th>
354
+ )}
355
+ </tr>
356
+ </thead>
357
+ <tbody>
358
+ {Object.entries(testResults.models).map(([modelName, modelResult]) => (
359
+ <tr key={modelName} className="border-b border-border/50 last:border-0">
360
+ <td className="py-1 px-2 font-medium">{modelName}</td>
361
+ {provider.anthropicBaseUrl !== undefined &&
362
+ provider.anthropicBaseUrl !== "" && (
363
+ <td className="py-1 px-2">
364
+ <TestStatus result={modelResult.anthropic.nonStreaming} />
365
+ </td>
366
+ )}
367
+ {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
368
+ <td className="py-1 px-2">
369
+ <TestStatus result={modelResult.openai.nonStreaming} />
370
+ </td>
371
+ )}
372
+ </tr>
373
+ ))}
374
+ </tbody>
375
+ </table>
376
+ </div>
377
+ )}
378
+ </div>
379
+ )}
380
+
253
381
  <div className="flex gap-2 pt-1 border-t">
254
382
  {onTest !== undefined && (
255
383
  <Button