@tonyclaw/llm-inspector 1.14.8 → 1.15.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-CMuJQyt1.js +105 -0
- package/.output/public/assets/index-DciyfYBk.css +1 -0
- package/.output/public/assets/{main-CJ4MreBr.js → main-BLYgekFx.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +85 -111
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_ssr/{index-9uTJ4xYR.mjs → index-P66uoVEU.mjs} +677 -304
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BKnjB_zi.mjs → router-DpLCKk51.mjs} +45 -14
- package/.output/server/{_tanstack-start-manifest_v-IsglLVKy.mjs → _tanstack-start-manifest_v-C9Wq6YdJ.mjs} +1 -1
- package/.output/server/index.mjs +22 -22
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +99 -180
- package/src/components/proxy-viewer/ConversationGroup.tsx +70 -66
- package/src/components/proxy-viewer/ConversationHeader.tsx +15 -39
- package/src/components/proxy-viewer/LogEntry.tsx +68 -9
- package/src/components/proxy-viewer/LogEntryHeader.tsx +62 -75
- package/src/components/proxy-viewer/ThreadConnector.tsx +78 -65
- package/src/components/proxy-viewer/TurnGroup.tsx +83 -0
- package/src/components/ui/crab-variants.tsx +456 -0
- package/src/lib/stopReason.ts +7 -6
- package/src/proxy/formats/anthropic/handler.ts +2 -5
- package/src/proxy/formats/openai/handler.ts +33 -7
- package/src/proxy/formats/openai/schemas.ts +1 -0
- package/src/proxy/formats/openai/stream.ts +24 -0
- package/src/proxy/handler.ts +8 -2
- package/src/proxy/schemas.ts +6 -3
- package/styles/globals.css +38 -0
- package/.output/public/assets/index-CdnotuLh.js +0 -105
- package/.output/public/assets/index-vP91146S.css +0 -1
|
@@ -198,7 +198,7 @@ function getResponse() {
|
|
|
198
198
|
return event.res;
|
|
199
199
|
}
|
|
200
200
|
async function getStartManifest(matchedRoutes) {
|
|
201
|
-
const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-
|
|
201
|
+
const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-C9Wq6YdJ.mjs");
|
|
202
202
|
const startManifest = tsrStartManifest();
|
|
203
203
|
const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
|
|
204
204
|
rootRoute.assets = rootRoute.assets || [];
|
|
@@ -767,7 +767,7 @@ let entriesPromise;
|
|
|
767
767
|
let baseManifestPromise;
|
|
768
768
|
let cachedFinalManifestPromise;
|
|
769
769
|
async function loadEntries() {
|
|
770
|
-
const routerEntry = await import("./router-
|
|
770
|
+
const routerEntry = await import("./router-DpLCKk51.mjs").then((n) => n.r);
|
|
771
771
|
const startEntry = await import("./start-HYkvq4Ni.mjs");
|
|
772
772
|
return { startEntry, routerEntry };
|
|
773
773
|
}
|
|
@@ -45,7 +45,7 @@ import "../_libs/debounce-fn.mjs";
|
|
|
45
45
|
import "../_libs/mimic-function.mjs";
|
|
46
46
|
import "../_libs/semver.mjs";
|
|
47
47
|
import "../_libs/uint8array-extras.mjs";
|
|
48
|
-
const appCss = "/assets/index-
|
|
48
|
+
const appCss = "/assets/index-DciyfYBk.css";
|
|
49
49
|
const Route$k = createRootRoute({
|
|
50
50
|
head: () => ({
|
|
51
51
|
meta: [
|
|
@@ -69,7 +69,7 @@ function RootDocument({ children }) {
|
|
|
69
69
|
] })
|
|
70
70
|
] });
|
|
71
71
|
}
|
|
72
|
-
const $$splitComponentImporter = () => import("./index-
|
|
72
|
+
const $$splitComponentImporter = () => import("./index-P66uoVEU.mjs");
|
|
73
73
|
const Route$j = createFileRoute("/")({
|
|
74
74
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
75
75
|
});
|
|
@@ -577,6 +577,7 @@ const OpenAIRequestSchema = object({
|
|
|
577
577
|
stream: boolean().optional(),
|
|
578
578
|
tools: array(OpenAIToolDefinition).optional(),
|
|
579
579
|
tool_choice: union([
|
|
580
|
+
_enum(["auto", "none", "required"]),
|
|
580
581
|
object({ type: literal("auto") }),
|
|
581
582
|
object({ type: literal("none") }),
|
|
582
583
|
object({ type: literal("function"), function: object({ name: string() }) })
|
|
@@ -1276,11 +1277,7 @@ const AnthropicFormatHandler = {
|
|
|
1276
1277
|
const json = JSON.parse(rawBody);
|
|
1277
1278
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
1278
1279
|
const keys = Object.keys(json);
|
|
1279
|
-
|
|
1280
|
-
if (keys.includes("system") || keys.includes("tools")) {
|
|
1281
|
-
return true;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1280
|
+
return keys.includes("model") && keys.includes("messages") && keys.includes("system");
|
|
1284
1281
|
}
|
|
1285
1282
|
return false;
|
|
1286
1283
|
} catch {
|
|
@@ -1410,6 +1407,21 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
|
|
|
1410
1407
|
promptTokens = chunk.usage.prompt_tokens ?? 0;
|
|
1411
1408
|
completionTokens = chunk.usage.completion_tokens ?? 0;
|
|
1412
1409
|
log.inputTokens = promptTokens;
|
|
1410
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1411
|
+
const usageDesc = Object.getOwnPropertyDescriptor(parsed, "usage");
|
|
1412
|
+
if (usageDesc !== void 0 && typeof usageDesc.value === "object" && usageDesc.value !== null) {
|
|
1413
|
+
const detailsDesc = Object.getOwnPropertyDescriptor(
|
|
1414
|
+
usageDesc.value,
|
|
1415
|
+
"prompt_tokens_details"
|
|
1416
|
+
);
|
|
1417
|
+
if (detailsDesc !== void 0 && typeof detailsDesc.value === "object" && detailsDesc.value !== null) {
|
|
1418
|
+
const cacheDesc = Object.getOwnPropertyDescriptor(detailsDesc.value, "cached_tokens");
|
|
1419
|
+
if (cacheDesc !== void 0 && typeof cacheDesc.value === "number") {
|
|
1420
|
+
log.cacheReadInputTokens = cacheDesc.value;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1413
1425
|
usageCaptured = true;
|
|
1414
1426
|
}
|
|
1415
1427
|
for (const choice of chunk.choices) {
|
|
@@ -1506,11 +1518,31 @@ const OpenAIFormatHandler = {
|
|
|
1506
1518
|
extractTokens(responseBody) {
|
|
1507
1519
|
const parsed = parseOpenAIResponse(responseBody);
|
|
1508
1520
|
if (parsed) {
|
|
1521
|
+
let cacheReadInputTokens = null;
|
|
1522
|
+
try {
|
|
1523
|
+
const raw = JSON.parse(responseBody);
|
|
1524
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1525
|
+
const usageDesc = Object.getOwnPropertyDescriptor(raw, "usage");
|
|
1526
|
+
if (usageDesc !== void 0 && typeof usageDesc.value === "object" && usageDesc.value !== null) {
|
|
1527
|
+
const detailsDesc = Object.getOwnPropertyDescriptor(
|
|
1528
|
+
usageDesc.value,
|
|
1529
|
+
"prompt_tokens_details"
|
|
1530
|
+
);
|
|
1531
|
+
if (detailsDesc !== void 0 && typeof detailsDesc.value === "object" && detailsDesc.value !== null) {
|
|
1532
|
+
const cacheDesc = Object.getOwnPropertyDescriptor(detailsDesc.value, "cached_tokens");
|
|
1533
|
+
if (cacheDesc !== void 0 && typeof cacheDesc.value === "number") {
|
|
1534
|
+
cacheReadInputTokens = cacheDesc.value;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1509
1541
|
return {
|
|
1510
1542
|
inputTokens: parsed.usage.prompt_tokens ?? null,
|
|
1511
1543
|
outputTokens: parsed.usage.completion_tokens ?? null,
|
|
1512
1544
|
cacheCreationInputTokens: null,
|
|
1513
|
-
cacheReadInputTokens
|
|
1545
|
+
cacheReadInputTokens
|
|
1514
1546
|
};
|
|
1515
1547
|
}
|
|
1516
1548
|
return {
|
|
@@ -1529,11 +1561,7 @@ const OpenAIFormatHandler = {
|
|
|
1529
1561
|
const json = JSON.parse(rawBody);
|
|
1530
1562
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
1531
1563
|
const keys = Object.keys(json);
|
|
1532
|
-
|
|
1533
|
-
if (!keys.includes("system") && !keys.includes("tools")) {
|
|
1534
|
-
return true;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1564
|
+
return keys.includes("model") && keys.includes("messages") && !keys.includes("system");
|
|
1537
1565
|
}
|
|
1538
1566
|
return false;
|
|
1539
1567
|
} catch {
|
|
@@ -2504,6 +2532,8 @@ async function handleProxy(req) {
|
|
|
2504
2532
|
upstreamHeaders.forEach((value, key) => {
|
|
2505
2533
|
upstreamHeadersObj[key.toLowerCase()] = value;
|
|
2506
2534
|
});
|
|
2535
|
+
const bodyFormat = formatRegistry.detectFormat(requestBody);
|
|
2536
|
+
const displayApiFormat = bodyFormat !== "unknown" ? bodyFormat : formatHandler.format;
|
|
2507
2537
|
const log = await createLog(
|
|
2508
2538
|
req.method,
|
|
2509
2539
|
parsed.apiPath,
|
|
@@ -2512,7 +2542,7 @@ async function handleProxy(req) {
|
|
|
2512
2542
|
clientInfo,
|
|
2513
2543
|
rawHeaders,
|
|
2514
2544
|
upstreamHeadersObj,
|
|
2515
|
-
|
|
2545
|
+
displayApiFormat,
|
|
2516
2546
|
model,
|
|
2517
2547
|
sessionId,
|
|
2518
2548
|
preAcquiredId
|
|
@@ -4713,6 +4743,7 @@ const router = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
|
|
|
4713
4743
|
export {
|
|
4714
4744
|
CapturedLogSchema as C,
|
|
4715
4745
|
InspectorResponseSchema as I,
|
|
4746
|
+
OpenAIRequestSchema as O,
|
|
4716
4747
|
ProviderTestResultsSchema as P,
|
|
4717
4748
|
RuntimeConfigSchema as R,
|
|
4718
4749
|
parseRequest as a,
|
|
@@ -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-
|
|
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-BLYgekFx.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-CMuJQyt1.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/providers/scan"] }, "/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/providers/scan": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.scan.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-BLYgekFx.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
+
"/assets/index-DciyfYBk.css": {
|
|
39
|
+
"type": "text/css; charset=utf-8",
|
|
40
|
+
"etag": '"15867-TOX1Gddns0p3jKt+7co1NuZS+d8"',
|
|
41
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
42
|
+
"size": 88167,
|
|
43
|
+
"path": "../public/assets/index-DciyfYBk.css"
|
|
44
|
+
},
|
|
38
45
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
46
|
"type": "image/svg+xml",
|
|
40
47
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
48
|
+
"mtime": "2026-06-12T07:17:56.428Z",
|
|
42
49
|
"size": 5915,
|
|
43
50
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
51
|
},
|
|
45
|
-
"/assets/
|
|
46
|
-
"type": "text/
|
|
47
|
-
"etag": '"
|
|
48
|
-
"mtime": "2026-06-
|
|
49
|
-
"size":
|
|
50
|
-
"path": "../public/assets/
|
|
52
|
+
"/assets/main-BLYgekFx.js": {
|
|
53
|
+
"type": "text/javascript; charset=utf-8",
|
|
54
|
+
"etag": '"50599-tks6aTeb/Edw4laR3cdpiWA48mc"',
|
|
55
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
56
|
+
"size": 329113,
|
|
57
|
+
"path": "../public/assets/main-BLYgekFx.js"
|
|
51
58
|
},
|
|
52
59
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
53
60
|
"type": "image/svg+xml",
|
|
54
61
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
55
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
56
63
|
"size": 11256,
|
|
57
64
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
58
65
|
},
|
|
59
66
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
60
67
|
"type": "image/jpeg",
|
|
61
68
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
62
|
-
"mtime": "2026-06-
|
|
69
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
63
70
|
"size": 6918,
|
|
64
71
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
65
72
|
},
|
|
66
|
-
"/assets/main-CJ4MreBr.js": {
|
|
67
|
-
"type": "text/javascript; charset=utf-8",
|
|
68
|
-
"etag": '"50599-xS7/i8WCDvqJMx9qZW3sZ2pNsvE"',
|
|
69
|
-
"mtime": "2026-06-11T12:14:10.997Z",
|
|
70
|
-
"size": 329113,
|
|
71
|
-
"path": "../public/assets/main-CJ4MreBr.js"
|
|
72
|
-
},
|
|
73
73
|
"/assets/qwen-CONDcHqt.png": {
|
|
74
74
|
"type": "image/png",
|
|
75
75
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
77
77
|
"size": 357059,
|
|
78
78
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
79
|
},
|
|
80
|
-
"/assets/index-
|
|
80
|
+
"/assets/index-CMuJQyt1.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"97f56-eCDTvraNzTc2Un6pdqimYNiZVhw"',
|
|
83
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
84
|
+
"size": 622422,
|
|
85
|
+
"path": "../public/assets/index-CMuJQyt1.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
-
import { Download
|
|
3
|
+
import { Download } from "lucide-react";
|
|
4
|
+
|
|
4
5
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
5
6
|
import type { CapturedLog } from "../proxy/schemas";
|
|
6
7
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
7
8
|
import packageJson from "../../package.json";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
groupLogsByConversation,
|
|
11
|
-
LogEntry,
|
|
12
|
-
ThreadConnector,
|
|
13
|
-
type ConversationGroupData,
|
|
14
|
-
type ViewMode,
|
|
15
|
-
} from "./proxy-viewer";
|
|
16
|
-
import { extractStopReason } from "../lib/stopReason";
|
|
9
|
+
import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
|
|
10
|
+
|
|
17
11
|
import { CrabLogo } from "./ui/crab-logo";
|
|
12
|
+
import { crabVariants } from "./ui/crab-variants";
|
|
18
13
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
19
14
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
20
15
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
@@ -116,8 +111,6 @@ export function ProxyViewer({
|
|
|
116
111
|
strip,
|
|
117
112
|
}: ProxyViewerProps): JSX.Element {
|
|
118
113
|
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
119
|
-
const [groupedView, setGroupedView] = useState(true);
|
|
120
|
-
const [groupViewMode, setGroupViewMode] = useState<ViewMode>("thread");
|
|
121
114
|
const [exporting, setExporting] = useState(false);
|
|
122
115
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
123
116
|
|
|
@@ -154,13 +147,9 @@ export function ProxyViewer({
|
|
|
154
147
|
|
|
155
148
|
const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
|
|
156
149
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
157
|
-
const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
|
|
158
|
-
|
|
159
|
-
// Determine what items to render (groups or individual logs)
|
|
160
|
-
const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
|
|
161
150
|
|
|
162
151
|
const rowVirtualizer = useVirtualizer({
|
|
163
|
-
count:
|
|
152
|
+
count: groups.length,
|
|
164
153
|
getScrollElement: () => parentRef.current,
|
|
165
154
|
estimateSize: () => 150,
|
|
166
155
|
measureElement:
|
|
@@ -172,15 +161,75 @@ export function ProxyViewer({
|
|
|
172
161
|
|
|
173
162
|
return (
|
|
174
163
|
<div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
|
|
175
|
-
{/*
|
|
176
|
-
<div className="flex items-
|
|
177
|
-
<h1 className="text-lg font-bold flex
|
|
178
|
-
|
|
164
|
+
{/* Brand row */}
|
|
165
|
+
<div className="flex items-end px-6 pt-6 pb-3 relative">
|
|
166
|
+
<h1 className="text-lg font-bold flex items-end gap-2 absolute left-1/2 -translate-x-1/2 whitespace-nowrap">
|
|
167
|
+
{/* Crab family — hover to animate together */}
|
|
168
|
+
<span className="flex items-end gap-1 group cursor-default" aria-hidden="true">
|
|
169
|
+
<CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
|
|
170
|
+
<span className="flex items-end gap-0.5">
|
|
171
|
+
{crabVariants.map((Crab, i) => (
|
|
172
|
+
<Crab
|
|
173
|
+
key={i}
|
|
174
|
+
className={`size-5 ${
|
|
175
|
+
[
|
|
176
|
+
"text-amber-500",
|
|
177
|
+
"text-rose-500",
|
|
178
|
+
"text-sky-500",
|
|
179
|
+
"text-emerald-500",
|
|
180
|
+
"text-violet-500",
|
|
181
|
+
"text-orange-500",
|
|
182
|
+
"text-cyan-500",
|
|
183
|
+
"text-pink-500",
|
|
184
|
+
"text-lime-500",
|
|
185
|
+
"text-blue-500",
|
|
186
|
+
"text-yellow-500",
|
|
187
|
+
"text-fuchsia-500",
|
|
188
|
+
][i]
|
|
189
|
+
} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1`}
|
|
190
|
+
style={{ transitionDelay: `${i * 50}ms` }}
|
|
191
|
+
/>
|
|
192
|
+
))}
|
|
193
|
+
</span>
|
|
194
|
+
</span>
|
|
179
195
|
<span className="flex items-baseline gap-2">
|
|
180
196
|
LLM Inspector
|
|
181
197
|
<span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
|
|
182
198
|
</span>
|
|
183
199
|
</h1>
|
|
200
|
+
<div className="ml-auto">
|
|
201
|
+
<SettingsDialog />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Controls + Filters */}
|
|
206
|
+
<div className="flex items-center gap-3 px-6 mb-4">
|
|
207
|
+
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
208
|
+
<SelectTrigger className="flex-1 max-w-[350px] text-xs">
|
|
209
|
+
<SelectValue placeholder="All sessions" />
|
|
210
|
+
</SelectTrigger>
|
|
211
|
+
<SelectContent>
|
|
212
|
+
<SelectItem value="__all__">All sessions</SelectItem>
|
|
213
|
+
{sessions.map((s) => (
|
|
214
|
+
<SelectItem key={s} value={s}>
|
|
215
|
+
{truncateSessionId(s)}
|
|
216
|
+
</SelectItem>
|
|
217
|
+
))}
|
|
218
|
+
</SelectContent>
|
|
219
|
+
</Select>
|
|
220
|
+
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
221
|
+
<SelectTrigger className="flex-1 max-w-[250px] text-xs">
|
|
222
|
+
<SelectValue placeholder="All models" />
|
|
223
|
+
</SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
<SelectItem value="__all__">All models</SelectItem>
|
|
226
|
+
{models.map((m) => (
|
|
227
|
+
<SelectItem key={m} value={m}>
|
|
228
|
+
{m}
|
|
229
|
+
</SelectItem>
|
|
230
|
+
))}
|
|
231
|
+
</SelectContent>
|
|
232
|
+
</Select>
|
|
184
233
|
<TooltipProvider>
|
|
185
234
|
<Tooltip>
|
|
186
235
|
<TooltipTrigger asChild>
|
|
@@ -214,7 +263,7 @@ export function ProxyViewer({
|
|
|
214
263
|
</TooltipContent>
|
|
215
264
|
</Tooltip>
|
|
216
265
|
</TooltipProvider>
|
|
217
|
-
<
|
|
266
|
+
<div className="flex-1" />
|
|
218
267
|
<span className="text-muted-foreground text-xs font-mono">
|
|
219
268
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
220
269
|
{totalIn > 0 || totalOut > 0
|
|
@@ -236,7 +285,7 @@ export function ProxyViewer({
|
|
|
236
285
|
) : (
|
|
237
286
|
<>
|
|
238
287
|
<Download className="size-3" />
|
|
239
|
-
<span>Export
|
|
288
|
+
<span>Export</span>
|
|
240
289
|
</>
|
|
241
290
|
)}
|
|
242
291
|
</button>
|
|
@@ -247,97 +296,15 @@ export function ProxyViewer({
|
|
|
247
296
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
248
297
|
title="Clear all logs"
|
|
249
298
|
>
|
|
250
|
-
Clear
|
|
299
|
+
Clear
|
|
251
300
|
</button>
|
|
252
301
|
</div>
|
|
253
302
|
|
|
254
|
-
{/* Filters */}
|
|
255
|
-
<div className="flex gap-3 px-6 mb-4">
|
|
256
|
-
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
257
|
-
<SelectTrigger className="flex-1 max-w-[400px] text-xs">
|
|
258
|
-
<SelectValue placeholder="All sessions" />
|
|
259
|
-
</SelectTrigger>
|
|
260
|
-
<SelectContent>
|
|
261
|
-
<SelectItem value="__all__">All sessions</SelectItem>
|
|
262
|
-
{sessions.map((s) => (
|
|
263
|
-
<SelectItem key={s} value={s}>
|
|
264
|
-
{truncateSessionId(s)}
|
|
265
|
-
</SelectItem>
|
|
266
|
-
))}
|
|
267
|
-
</SelectContent>
|
|
268
|
-
</Select>
|
|
269
|
-
|
|
270
|
-
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
271
|
-
<SelectTrigger className="text-xs">
|
|
272
|
-
<SelectValue placeholder="All models" />
|
|
273
|
-
</SelectTrigger>
|
|
274
|
-
<SelectContent>
|
|
275
|
-
<SelectItem value="__all__">All models</SelectItem>
|
|
276
|
-
{models.map((m) => (
|
|
277
|
-
<SelectItem key={m} value={m}>
|
|
278
|
-
{m}
|
|
279
|
-
</SelectItem>
|
|
280
|
-
))}
|
|
281
|
-
</SelectContent>
|
|
282
|
-
</Select>
|
|
283
|
-
|
|
284
|
-
{/* View toggle */}
|
|
285
|
-
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
286
|
-
<button
|
|
287
|
-
type="button"
|
|
288
|
-
onClick={() => setGroupedView(true)}
|
|
289
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
290
|
-
groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
|
|
291
|
-
}`}
|
|
292
|
-
title="Grouped view"
|
|
293
|
-
>
|
|
294
|
-
<LayoutGrid className="size-4" />
|
|
295
|
-
</button>
|
|
296
|
-
<button
|
|
297
|
-
type="button"
|
|
298
|
-
onClick={() => setGroupedView(false)}
|
|
299
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
300
|
-
!groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
|
|
301
|
-
}`}
|
|
302
|
-
title="Flat view"
|
|
303
|
-
>
|
|
304
|
-
<List className="size-4" />
|
|
305
|
-
</button>
|
|
306
|
-
</div>
|
|
307
|
-
|
|
308
|
-
{/* Thread/flat mode toggle */}
|
|
309
|
-
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
onClick={() => setGroupViewMode("thread")}
|
|
313
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
314
|
-
groupViewMode === "thread"
|
|
315
|
-
? "bg-amber-500/15 text-amber-400 border-r border-amber-500/30"
|
|
316
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
317
|
-
}`}
|
|
318
|
-
title="Thread view (connected timeline)"
|
|
319
|
-
>
|
|
320
|
-
<GitBranch className="size-4" />
|
|
321
|
-
</button>
|
|
322
|
-
<button
|
|
323
|
-
type="button"
|
|
324
|
-
onClick={() => setGroupViewMode("flat")}
|
|
325
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
326
|
-
groupViewMode === "flat"
|
|
327
|
-
? "bg-muted text-foreground"
|
|
328
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
329
|
-
}`}
|
|
330
|
-
title="Flat view (card list)"
|
|
331
|
-
>
|
|
332
|
-
<List className="size-4" />
|
|
333
|
-
</button>
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
303
|
{/* Log list */}
|
|
338
304
|
<div className="flex-1 min-h-0 px-6 pb-6">
|
|
339
305
|
{logs.length === 0 ? (
|
|
340
306
|
<div className="text-center text-muted-foreground py-16 space-y-4">
|
|
307
|
+
<CrabLogo className="size-12 text-muted-foreground/20 mx-auto" />
|
|
341
308
|
<p className="text-sm">No requests captured yet.</p>
|
|
342
309
|
<p className="text-xs">Route AI coding tools through the proxy:</p>
|
|
343
310
|
<CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
@@ -352,79 +319,31 @@ export function ProxyViewer({
|
|
|
352
319
|
}}
|
|
353
320
|
>
|
|
354
321
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
);
|
|
381
|
-
} else {
|
|
382
|
-
const log = logs[virtualRow.index];
|
|
383
|
-
if (log === undefined) return null;
|
|
384
|
-
const idx = virtualRow.index;
|
|
385
|
-
return (
|
|
386
|
-
<div
|
|
387
|
-
key={log.id}
|
|
388
|
-
data-index={virtualRow.index}
|
|
389
|
-
ref={rowVirtualizer.measureElement}
|
|
390
|
-
style={{
|
|
391
|
-
position: "absolute",
|
|
392
|
-
top: 0,
|
|
393
|
-
left: 0,
|
|
394
|
-
width: "100%",
|
|
395
|
-
transform: `translateY(${virtualRow.start}px)`,
|
|
396
|
-
}}
|
|
397
|
-
>
|
|
398
|
-
{groupViewMode === "thread" ? (
|
|
399
|
-
<div className="flex items-stretch ml-3">
|
|
400
|
-
<ThreadConnector
|
|
401
|
-
stopReason={stopReasons[idx] ?? null}
|
|
402
|
-
isPending={log.responseStatus === null}
|
|
403
|
-
isFirst={idx === 0}
|
|
404
|
-
isLast={idx === logs.length - 1}
|
|
405
|
-
/>
|
|
406
|
-
<div className="flex-1 min-w-0 mb-2">
|
|
407
|
-
<LogEntry
|
|
408
|
-
log={log}
|
|
409
|
-
viewMode={viewMode}
|
|
410
|
-
strip={strip}
|
|
411
|
-
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
412
|
-
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
413
|
-
/>
|
|
414
|
-
</div>
|
|
415
|
-
</div>
|
|
416
|
-
) : (
|
|
417
|
-
<LogEntry
|
|
418
|
-
log={log}
|
|
419
|
-
viewMode={viewMode}
|
|
420
|
-
strip={strip}
|
|
421
|
-
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
422
|
-
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
423
|
-
/>
|
|
424
|
-
)}
|
|
425
|
-
</div>
|
|
426
|
-
);
|
|
427
|
-
}
|
|
322
|
+
const group = groups[virtualRow.index];
|
|
323
|
+
if (group === undefined) return null;
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
key={group.id}
|
|
327
|
+
data-index={virtualRow.index}
|
|
328
|
+
ref={rowVirtualizer.measureElement}
|
|
329
|
+
style={{
|
|
330
|
+
position: "absolute",
|
|
331
|
+
top: 0,
|
|
332
|
+
left: 0,
|
|
333
|
+
width: "100%",
|
|
334
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
<ConversationGroup
|
|
338
|
+
group={group}
|
|
339
|
+
viewMode={viewMode}
|
|
340
|
+
strip={strip}
|
|
341
|
+
cacheTrends={cacheTrends}
|
|
342
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
343
|
+
standalone={groups.length === 1}
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
428
347
|
})}
|
|
429
348
|
</div>
|
|
430
349
|
</div>
|