@tonyclaw/llm-inspector 1.14.7 → 1.14.9
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-Dv-dj1xH.js +105 -0
- package/.output/public/assets/index-bqeypwJB.css +1 -0
- package/.output/public/assets/{main-BV7uNIIz.js → main-C8OUJKbz.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +87 -79
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_ssr/{index-BvHLASu8.mjs → index-_9xcAkkw.mjs} +861 -608
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-lUOA8pi6.mjs → router-CmanwZJc.mjs} +45 -14
- package/.output/server/{_tanstack-start-manifest_v-XNH7fVPN.mjs → _tanstack-start-manifest_v-BVIiyDeJ.mjs} +1 -1
- package/.output/server/index.mjs +23 -23
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +137 -146
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +79 -47
- package/src/components/providers/SettingsDialog.tsx +25 -15
- package/src/components/proxy-viewer/ConversationGroup.tsx +74 -11
- package/src/components/proxy-viewer/ConversationHeader.tsx +63 -2
- package/src/components/proxy-viewer/LogEntry.tsx +184 -54
- package/src/components/proxy-viewer/LogEntryHeader.tsx +148 -143
- package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
- package/src/components/proxy-viewer/ThreadConnector.tsx +93 -0
- package/src/components/proxy-viewer/index.ts +2 -1
- package/src/lib/stopReason.ts +57 -0
- 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/.output/public/assets/index-Cmi8TfeU.js +0 -105
- package/.output/public/assets/index-DXUNTCVh.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-BVIiyDeJ.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-CmanwZJc.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-bqeypwJB.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-_9xcAkkw.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-C8OUJKbz.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-Dv-dj1xH.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-C8OUJKbz.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -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-
|
|
41
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
|
+
"/assets/index-bqeypwJB.css": {
|
|
46
|
+
"type": "text/css; charset=utf-8",
|
|
47
|
+
"etag": '"14d2d-fciDL/LvvG4KiFnlq0WNx0FqBmw"',
|
|
48
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
49
|
+
"size": 85293,
|
|
50
|
+
"path": "../public/assets/index-bqeypwJB.css"
|
|
51
|
+
},
|
|
45
52
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
46
53
|
"type": "image/jpeg",
|
|
47
54
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
48
|
-
"mtime": "2026-06-
|
|
55
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
49
56
|
"size": 6918,
|
|
50
57
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
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-11T13:44:56.122Z",
|
|
56
63
|
"size": 11256,
|
|
57
64
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
58
65
|
},
|
|
66
|
+
"/assets/main-C8OUJKbz.js": {
|
|
67
|
+
"type": "text/javascript; charset=utf-8",
|
|
68
|
+
"etag": '"50599-x4LjDXM7jigK2tdjHCfXl0STwVQ"',
|
|
69
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
70
|
+
"size": 329113,
|
|
71
|
+
"path": "../public/assets/main-C8OUJKbz.js"
|
|
72
|
+
},
|
|
59
73
|
"/assets/qwen-CONDcHqt.png": {
|
|
60
74
|
"type": "image/png",
|
|
61
75
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
62
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-11T13:44:56.125Z",
|
|
63
77
|
"size": 357059,
|
|
64
78
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
65
79
|
},
|
|
66
|
-
"/assets/
|
|
67
|
-
"type": "text/javascript; charset=utf-8",
|
|
68
|
-
"etag": '"50599-Hw6XWHNqYOzGgMQUjQUlo6V5ZGQ"',
|
|
69
|
-
"mtime": "2026-06-11T07:46:13.439Z",
|
|
70
|
-
"size": 329113,
|
|
71
|
-
"path": "../public/assets/main-BV7uNIIz.js"
|
|
72
|
-
},
|
|
73
|
-
"/assets/index-DXUNTCVh.css": {
|
|
74
|
-
"type": "text/css; charset=utf-8",
|
|
75
|
-
"etag": '"145d7-BpJnON+Y0T31X0nkA2icHh97eLY"',
|
|
76
|
-
"mtime": "2026-06-11T07:46:13.439Z",
|
|
77
|
-
"size": 83415,
|
|
78
|
-
"path": "../public/assets/index-DXUNTCVh.css"
|
|
79
|
-
},
|
|
80
|
-
"/assets/index-Cmi8TfeU.js": {
|
|
80
|
+
"/assets/index-Dv-dj1xH.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"9600f-3dnGI92f5ekK1BS9qp5iLg0Map4"',
|
|
83
|
+
"mtime": "2026-06-11T13:44:56.127Z",
|
|
84
|
+
"size": 614415,
|
|
85
|
+
"path": "../public/assets/index-Dv-dj1xH.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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, GitBranch, LayoutGrid, List } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
4
6
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
7
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
6
8
|
import packageJson from "../../package.json";
|
|
@@ -8,14 +10,16 @@ import {
|
|
|
8
10
|
ConversationGroup,
|
|
9
11
|
groupLogsByConversation,
|
|
10
12
|
LogEntry,
|
|
13
|
+
ThreadConnector,
|
|
11
14
|
type ConversationGroupData,
|
|
15
|
+
type ViewMode,
|
|
12
16
|
} from "./proxy-viewer";
|
|
17
|
+
import { extractStopReason } from "../lib/stopReason";
|
|
13
18
|
import { CrabLogo } from "./ui/crab-logo";
|
|
14
19
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
15
20
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
16
21
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
17
22
|
import { CompareDrawer } from "./proxy-viewer/CompareDrawer";
|
|
18
|
-
import { getConversationId } from "./proxy-viewer/ConversationHeader";
|
|
19
23
|
|
|
20
24
|
function truncateSessionId(id: string): string {
|
|
21
25
|
if (id.length <= 30) return id;
|
|
@@ -114,9 +118,8 @@ export function ProxyViewer({
|
|
|
114
118
|
}: ProxyViewerProps): JSX.Element {
|
|
115
119
|
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
116
120
|
const [groupedView, setGroupedView] = useState(true);
|
|
121
|
+
const [groupViewMode, setGroupViewMode] = useState<ViewMode>("thread");
|
|
117
122
|
const [exporting, setExporting] = useState(false);
|
|
118
|
-
const [selectedLogIds, setSelectedLogIds] = useState<number[]>([]);
|
|
119
|
-
const [compareOpen, setCompareOpen] = useState(false);
|
|
120
123
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
121
124
|
|
|
122
125
|
const handleExport = useCallback(async () => {
|
|
@@ -129,96 +132,45 @@ export function ProxyViewer({
|
|
|
129
132
|
}, [logs]);
|
|
130
133
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (prev.includes(logId)) {
|
|
135
|
-
return prev.filter((id) => id !== logId);
|
|
136
|
-
}
|
|
137
|
-
if (prev.length < 2) {
|
|
138
|
-
return [...prev, logId];
|
|
139
|
-
}
|
|
140
|
-
// FIFO eviction: drop the oldest, append the new id.
|
|
141
|
-
const newer = prev[1];
|
|
142
|
-
if (newer === undefined) return prev;
|
|
143
|
-
return [newer, logId];
|
|
144
|
-
});
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
// Reset the selection (and close the compare drawer) whenever the user
|
|
148
|
-
// changes the session or model filter, since the selected logs may no
|
|
149
|
-
// longer be in the visible list.
|
|
135
|
+
// Close the compare drawer when the user changes the session or model
|
|
136
|
+
// filter, since the predecessor relationship may no longer be meaningful.
|
|
150
137
|
useEffect(() => {
|
|
151
|
-
|
|
152
|
-
setCompareOpen(false);
|
|
138
|
+
setComparePair(null);
|
|
153
139
|
}, [selectedSession, selectedModel]);
|
|
154
140
|
|
|
155
|
-
const selectedSet = useMemo(() => new Set(selectedLogIds), [selectedLogIds]);
|
|
156
|
-
|
|
157
|
-
const openCompare = useCallback(() => {
|
|
158
|
-
if (selectedLogIds.length !== 2) return;
|
|
159
|
-
const [idA, idB] = selectedLogIds;
|
|
160
|
-
if (idA === undefined || idB === undefined) return;
|
|
161
|
-
const logA = logs.find((l) => l.id === idA);
|
|
162
|
-
const logB = logs.find((l) => l.id === idB);
|
|
163
|
-
if (logA === undefined || logB === undefined) return;
|
|
164
|
-
setComparePair([logA, logB]);
|
|
165
|
-
setCompareOpen(true);
|
|
166
|
-
}, [selectedLogIds, logs]);
|
|
167
|
-
|
|
168
141
|
const closeCompare = useCallback(() => {
|
|
169
|
-
|
|
170
|
-
// Keep `comparePair` so the selection survives across the drawer being
|
|
171
|
-
// closed and re-opened; it is replaced the next time the user opens
|
|
172
|
-
// the drawer with a different pair.
|
|
173
|
-
}, []);
|
|
174
|
-
|
|
175
|
-
const clearSelection = useCallback(() => {
|
|
176
|
-
setSelectedLogIds([]);
|
|
142
|
+
setComparePair(null);
|
|
177
143
|
}, []);
|
|
178
144
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const a = Date.parse(logA.timestamp);
|
|
190
|
-
const b = Date.parse(logB.timestamp);
|
|
191
|
-
if (!Number.isNaN(a) && !Number.isNaN(b)) {
|
|
192
|
-
const ms = Math.abs(b - a);
|
|
193
|
-
elapsed = formatElapsed(ms);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
logA,
|
|
198
|
-
logB,
|
|
199
|
-
sameSession,
|
|
200
|
-
elapsed,
|
|
201
|
-
};
|
|
202
|
-
}, [selectedLogIds, logs]);
|
|
203
|
-
|
|
204
|
-
function formatElapsed(ms: number): string {
|
|
205
|
-
if (ms < 1000) return `${ms}ms`;
|
|
206
|
-
const sec = Math.floor(ms / 1000);
|
|
207
|
-
if (sec < 60) return `${sec}s`;
|
|
208
|
-
const min = Math.floor(sec / 60);
|
|
209
|
-
if (min < 60) return `${min}m`;
|
|
210
|
-
const hr = Math.floor(min / 60);
|
|
211
|
-
return `${hr}h${min % 60}m`;
|
|
212
|
-
}
|
|
145
|
+
const handleCompareWithPrevious = useCallback(
|
|
146
|
+
(log: CapturedLog) => {
|
|
147
|
+
const idx = logs.indexOf(log);
|
|
148
|
+
if (idx <= 0) return;
|
|
149
|
+
const predecessor = logs[idx - 1];
|
|
150
|
+
if (predecessor === undefined) return;
|
|
151
|
+
setComparePair([predecessor, log]);
|
|
152
|
+
},
|
|
153
|
+
[logs],
|
|
154
|
+
);
|
|
213
155
|
|
|
214
156
|
const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
|
|
215
157
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
158
|
+
const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
|
|
159
|
+
|
|
160
|
+
const turnIndices = useMemo(() => {
|
|
161
|
+
const indices: number[] = [];
|
|
162
|
+
let turn = 0;
|
|
163
|
+
for (let i = 0; i < stopReasons.length; i++) {
|
|
164
|
+
if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
|
|
165
|
+
turn++;
|
|
166
|
+
}
|
|
167
|
+
indices.push(turn);
|
|
168
|
+
}
|
|
169
|
+
return indices;
|
|
170
|
+
}, [stopReasons]);
|
|
216
171
|
|
|
217
172
|
// Determine what items to render (groups or individual logs)
|
|
218
|
-
const renderGroups =
|
|
219
|
-
logs.length > 0 &&
|
|
220
|
-
groupedView &&
|
|
221
|
-
!(groups.length === 1 && groups[0]?.logs.length === logs.length);
|
|
173
|
+
const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
|
|
222
174
|
|
|
223
175
|
const rowVirtualizer = useVirtualizer({
|
|
224
176
|
count: renderGroups ? groups.length : logs.length,
|
|
@@ -242,30 +194,39 @@ export function ProxyViewer({
|
|
|
242
194
|
<span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
|
|
243
195
|
</span>
|
|
244
196
|
</h1>
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
197
|
+
<TooltipProvider>
|
|
198
|
+
<Tooltip>
|
|
199
|
+
<TooltipTrigger asChild>
|
|
200
|
+
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => onViewModeChange("simple")}
|
|
204
|
+
className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
|
|
205
|
+
viewMode === "simple"
|
|
206
|
+
? "bg-muted text-foreground"
|
|
207
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
Simple
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => onViewModeChange("full")}
|
|
215
|
+
className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
|
|
216
|
+
viewMode === "full"
|
|
217
|
+
? "bg-muted text-foreground"
|
|
218
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
219
|
+
}`}
|
|
220
|
+
>
|
|
221
|
+
Full
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</TooltipTrigger>
|
|
225
|
+
<TooltipContent>
|
|
226
|
+
Simple shows parsed output; Full adds raw headers and tokens
|
|
227
|
+
</TooltipContent>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</TooltipProvider>
|
|
269
230
|
<SettingsDialog />
|
|
270
231
|
<span className="text-muted-foreground text-xs font-mono">
|
|
271
232
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
@@ -356,6 +317,34 @@ export function ProxyViewer({
|
|
|
356
317
|
<List className="size-4" />
|
|
357
318
|
</button>
|
|
358
319
|
</div>
|
|
320
|
+
|
|
321
|
+
{/* Thread/flat mode toggle */}
|
|
322
|
+
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
onClick={() => setGroupViewMode("thread")}
|
|
326
|
+
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
327
|
+
groupViewMode === "thread"
|
|
328
|
+
? "bg-amber-500/15 text-amber-400 border-r border-amber-500/30"
|
|
329
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
330
|
+
}`}
|
|
331
|
+
title="Thread view (connected timeline)"
|
|
332
|
+
>
|
|
333
|
+
<GitBranch className="size-4" />
|
|
334
|
+
</button>
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
onClick={() => setGroupViewMode("flat")}
|
|
338
|
+
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
339
|
+
groupViewMode === "flat"
|
|
340
|
+
? "bg-muted text-foreground"
|
|
341
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
342
|
+
}`}
|
|
343
|
+
title="Flat view (card list)"
|
|
344
|
+
>
|
|
345
|
+
<List className="size-4" />
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
359
348
|
</div>
|
|
360
349
|
|
|
361
350
|
{/* Log list */}
|
|
@@ -397,14 +386,15 @@ export function ProxyViewer({
|
|
|
397
386
|
viewMode={viewMode}
|
|
398
387
|
strip={strip}
|
|
399
388
|
cacheTrends={cacheTrends}
|
|
400
|
-
|
|
401
|
-
|
|
389
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
390
|
+
defaultGroupViewMode={groupViewMode}
|
|
402
391
|
/>
|
|
403
392
|
</div>
|
|
404
393
|
);
|
|
405
394
|
} else {
|
|
406
395
|
const log = logs[virtualRow.index];
|
|
407
396
|
if (log === undefined) return null;
|
|
397
|
+
const idx = virtualRow.index;
|
|
408
398
|
return (
|
|
409
399
|
<div
|
|
410
400
|
key={log.id}
|
|
@@ -418,14 +408,43 @@ export function ProxyViewer({
|
|
|
418
408
|
transform: `translateY(${virtualRow.start}px)`,
|
|
419
409
|
}}
|
|
420
410
|
>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
411
|
+
{groupViewMode === "thread" ? (
|
|
412
|
+
<div className="flex items-stretch ml-3">
|
|
413
|
+
<ThreadConnector
|
|
414
|
+
stopReason={stopReasons[idx] ?? null}
|
|
415
|
+
isPending={log.responseStatus === null}
|
|
416
|
+
isFirst={idx === 0}
|
|
417
|
+
isLast={idx === logs.length - 1}
|
|
418
|
+
isTurnStart={
|
|
419
|
+
idx === 0 ||
|
|
420
|
+
stopReasons[idx - 1] === "end_turn" ||
|
|
421
|
+
stopReasons[idx - 1] === "stop"
|
|
422
|
+
}
|
|
423
|
+
/>
|
|
424
|
+
<div
|
|
425
|
+
className={cn(
|
|
426
|
+
"flex-1 min-w-0 mb-2 rounded-lg",
|
|
427
|
+
(turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
<LogEntry
|
|
431
|
+
log={log}
|
|
432
|
+
viewMode={viewMode}
|
|
433
|
+
strip={strip}
|
|
434
|
+
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
435
|
+
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
436
|
+
/>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
) : (
|
|
440
|
+
<LogEntry
|
|
441
|
+
log={log}
|
|
442
|
+
viewMode={viewMode}
|
|
443
|
+
strip={strip}
|
|
444
|
+
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
445
|
+
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
446
|
+
/>
|
|
447
|
+
)}
|
|
429
448
|
</div>
|
|
430
449
|
);
|
|
431
450
|
}
|
|
@@ -435,36 +454,8 @@ export function ProxyViewer({
|
|
|
435
454
|
)}
|
|
436
455
|
</div>
|
|
437
456
|
|
|
438
|
-
{/* Floating action bar — shown only when 2 logs are selected. */}
|
|
439
|
-
{selectedSummary !== null && (
|
|
440
|
-
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-3 bg-background border border-border rounded-lg shadow-lg px-4 py-2 text-xs">
|
|
441
|
-
<GitCompareArrows className="size-4 text-amber-400 shrink-0" />
|
|
442
|
-
<span className="text-muted-foreground font-mono">
|
|
443
|
-
#{selectedSummary.logA.id} ↔ #{selectedSummary.logB.id}
|
|
444
|
-
{" · "}
|
|
445
|
-
{selectedSummary.sameSession ? "same session" : "different sessions"}
|
|
446
|
-
{selectedSummary.elapsed !== "" && ` · ${selectedSummary.elapsed} apart`}
|
|
447
|
-
</span>
|
|
448
|
-
<button
|
|
449
|
-
type="button"
|
|
450
|
-
onClick={clearSelection}
|
|
451
|
-
className="text-muted-foreground hover:text-foreground transition-colors cursor-pointer inline-flex items-center gap-1"
|
|
452
|
-
>
|
|
453
|
-
<X className="size-3" />
|
|
454
|
-
Clear
|
|
455
|
-
</button>
|
|
456
|
-
<button
|
|
457
|
-
type="button"
|
|
458
|
-
onClick={openCompare}
|
|
459
|
-
className="bg-amber-400 text-amber-950 hover:bg-amber-300 transition-colors px-3 py-1 rounded font-medium cursor-pointer"
|
|
460
|
-
>
|
|
461
|
-
Compare 2 logs
|
|
462
|
-
</button>
|
|
463
|
-
</div>
|
|
464
|
-
)}
|
|
465
|
-
|
|
466
457
|
{/* Compare drawer — sibling of the log list, not a route change. */}
|
|
467
|
-
{
|
|
458
|
+
{comparePair !== null && (
|
|
468
459
|
<CompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
|
|
469
460
|
)}
|
|
470
461
|
</div>
|