@tonyclaw/llm-inspector 1.11.7 → 1.12.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.
@@ -75,6 +75,16 @@ const createLucideIcon = (iconName, iconNode) => {
75
75
  Component.displayName = toPascalCase(iconName);
76
76
  return Component;
77
77
  };
78
+ const __iconNode$L = [
79
+ ["path", { d: "M12 5v14", key: "s699le" }],
80
+ ["path", { d: "m19 12-7 7-7-7", key: "1idqje" }]
81
+ ];
82
+ const ArrowDown = createLucideIcon("arrow-down", __iconNode$L);
83
+ const __iconNode$K = [
84
+ ["path", { d: "m5 12 7-7 7 7", key: "hav0vg" }],
85
+ ["path", { d: "M12 19V5", key: "x0mq9r" }]
86
+ ];
87
+ const ArrowUp = createLucideIcon("arrow-up", __iconNode$K);
78
88
  const __iconNode$J = [
79
89
  ["path", { d: "M12 18V5", key: "adv99a" }],
80
90
  ["path", { d: "M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4", key: "1e3is1" }],
@@ -388,18 +398,20 @@ const __iconNode = [
388
398
  ];
389
399
  const Zap = createLucideIcon("zap", __iconNode);
390
400
  export {
391
- WifiOff as A,
392
- ChevronsUp as B,
401
+ ArrowUp as A,
402
+ Wifi as B,
393
403
  ChevronDown as C,
394
404
  Download as D,
395
405
  ExternalLink as E,
396
406
  FileTerminal as F,
397
407
  Globe as G,
398
- ChevronsDown as H,
399
- Brain as I,
400
- Terminal as J,
408
+ WifiOff as H,
409
+ ChevronsUp as I,
410
+ ChevronsDown as J,
411
+ Brain as K,
401
412
  LayoutGrid as L,
402
413
  MessageSquare as M,
414
+ Terminal as N,
403
415
  Plus as P,
404
416
  RotateCcw as R,
405
417
  Settings as S,
@@ -425,13 +437,13 @@ export {
425
437
  Eye as o,
426
438
  RotateCw as p,
427
439
  Pencil as q,
428
- TriangleAlert as r,
429
- Minus as s,
430
- CircleCheckBig as t,
431
- CircleStop as u,
432
- CircleQuestionMark as v,
433
- Server as w,
434
- Gauge as x,
435
- Lock as y,
436
- Wifi as z
440
+ ArrowDown as r,
441
+ TriangleAlert as s,
442
+ Minus as t,
443
+ CircleCheckBig as u,
444
+ CircleStop as v,
445
+ CircleQuestionMark as w,
446
+ Server as x,
447
+ Gauge as y,
448
+ Lock as z
437
449
  };
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, a as React } from "../_libs/react.mjs";
2
- import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, P as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema, S as StreamingChunkSchema$1 } from "./router-CYFPDggE.mjs";
2
+ import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, P as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema, S as StreamingChunkSchema$1 } from "./router-PZjNwOcw.mjs";
3
3
  import { u as useSWR, a as useSWRConfig } from "../_libs/swr.mjs";
4
4
  import { u as useVirtualizer } from "../_libs/tanstack__react-virtual.mjs";
5
5
  import { J as JSZip } from "../_libs/jszip.mjs";
@@ -9,7 +9,7 @@ import { c as cva } from "../_libs/class-variance-authority.mjs";
9
9
  import { d as diffLines, a as diffJson } from "../_libs/diff.mjs";
10
10
  import { R as Root, T as Trigger$1, C as Content, a as Close, b as Title, P as Portal$1, O as Overlay } from "../_libs/radix-ui__react-dialog.mjs";
11
11
  import { R as Root2, T as Trigger, I as Icon, V as Value, P as Portal, C as Content2, a as Viewport, b as Item, c as ItemIndicator, d as ItemText, S as ScrollUpButton, e as ScrollDownButton } from "../_libs/radix-ui__react-select.mjs";
12
- import { D as Download, L as LayoutGrid, a as List, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, X, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, G as Globe, i as User, F as FileTerminal, j as Radio, k as GitCompareArrows, l as Rows3, m as Columns2, E as ExternalLink, n as EyeOff, o as Eye, p as RotateCw, q as Pencil, T as Trash2, r as TriangleAlert, s as Minus, t as CircleCheckBig, u as CircleStop, v as CircleQuestionMark, w as Server, x as Gauge, y as Lock, z as Wifi, A as WifiOff, B as ChevronsUp, H as ChevronsDown, I as Brain, J as Terminal } from "../_libs/lucide-react.mjs";
12
+ import { D as Download, L as LayoutGrid, a as List, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, X, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, G as Globe, i as User, F as FileTerminal, j as Radio, k as GitCompareArrows, l as Rows3, m as Columns2, E as ExternalLink, n as EyeOff, o as Eye, p as RotateCw, q as Pencil, T as Trash2, A as ArrowUp, r as ArrowDown, s as TriangleAlert, t as Minus, u as CircleCheckBig, v as CircleStop, w as CircleQuestionMark, x as Server, y as Gauge, z as Lock, B as Wifi, H as WifiOff, I as ChevronsUp, J as ChevronsDown, K as Brain, N as Terminal } from "../_libs/lucide-react.mjs";
13
13
  import { M as Markdown } from "../_libs/react-markdown.mjs";
14
14
  import { a as array, s as string, u as union, o as object, l as literal, n as number, b as boolean, r as record, _ as _enum } from "../_libs/zod.mjs";
15
15
  import { R as Root2$1, L as List$1, T as Trigger$2, C as Content$1 } from "../_libs/radix-ui__react-tabs.mjs";
@@ -275,7 +275,7 @@ async function exportLogsAsZip(logs) {
275
275
  document.body.removeChild(anchor);
276
276
  URL.revokeObjectURL(url);
277
277
  }
278
- const version = "1.11.7";
278
+ const version = "1.12.0";
279
279
  const packageJson = {
280
280
  version
281
281
  };
@@ -1395,12 +1395,26 @@ const STATUS_BADGE_CLASSES = {
1395
1395
  server_error: "",
1396
1396
  pending: "bg-muted text-muted-foreground border-border"
1397
1397
  };
1398
+ function CacheTrendIndicator({ trend }) {
1399
+ if (trend === null) return null;
1400
+ const isUp = trend.direction === "up";
1401
+ const Icon2 = isUp ? ArrowUp : ArrowDown;
1402
+ const sign = isUp ? "+" : "-";
1403
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-0.5 text-muted-foreground tabular-nums", children: [
1404
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: isUp ? "size-3 text-emerald-400" : "size-3 text-rose-400" }),
1405
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono", children: [
1406
+ sign,
1407
+ formatTokens(trend.delta)
1408
+ ] })
1409
+ ] });
1410
+ }
1398
1411
  const LogEntryHeader = reactExports.memo(function({
1399
1412
  log,
1400
1413
  parsedRequest,
1401
1414
  expanded,
1402
1415
  onToggle,
1403
- suppressApiFormatBadge = false
1416
+ suppressApiFormatBadge = false,
1417
+ cacheTrend = null
1404
1418
  }) {
1405
1419
  const statusCategory = getStatusCategory(log.responseStatus);
1406
1420
  const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
@@ -1490,14 +1504,20 @@ const LogEntryHeader = reactExports.memo(function({
1490
1504
  )
1491
1505
  ] })
1492
1506
  ] }),
1493
- log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex items-center gap-1 text-xs shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-emerald-400", children: [
1494
- "Cache +",
1495
- formatTokens(log.cacheCreationInputTokens)
1496
- ] }) }),
1497
- log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex items-center gap-1 text-xs shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-purple-400", children: [
1498
- "Cache ~",
1499
- formatTokens(log.cacheReadInputTokens)
1500
- ] }) }),
1507
+ log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-xs shrink-0", children: [
1508
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CacheTrendIndicator, { trend: cacheTrend?.creation ?? null }),
1509
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-emerald-400", children: [
1510
+ "Cache +",
1511
+ formatTokens(log.cacheCreationInputTokens)
1512
+ ] })
1513
+ ] }),
1514
+ log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-xs shrink-0", children: [
1515
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CacheTrendIndicator, { trend: cacheTrend?.read ?? null }),
1516
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-purple-400", children: [
1517
+ "Cache ~",
1518
+ formatTokens(log.cacheReadInputTokens)
1519
+ ] })
1520
+ ] }),
1501
1521
  messageCount !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-muted-foreground text-xs shrink-0", children: [
1502
1522
  /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { className: "size-3" }),
1503
1523
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono tabular-nums", children: messageCount })
@@ -2364,7 +2384,8 @@ const LogEntry = reactExports.memo(function({
2364
2384
  log,
2365
2385
  viewMode = "simple",
2366
2386
  suppressApiFormatBadge = false,
2367
- strip
2387
+ strip,
2388
+ cacheTrend = null
2368
2389
  }) {
2369
2390
  const [expanded, setExpanded] = reactExports.useState(false);
2370
2391
  const [requestCopied, setRequestCopied] = reactExports.useState(false);
@@ -2422,7 +2443,8 @@ const LogEntry = reactExports.memo(function({
2422
2443
  parsedRequest,
2423
2444
  expanded,
2424
2445
  onToggle: () => setExpanded(!expanded),
2425
- suppressApiFormatBadge
2446
+ suppressApiFormatBadge,
2447
+ cacheTrend
2426
2448
  }
2427
2449
  ),
2428
2450
  expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { defaultValue: "request", children: [
@@ -2586,7 +2608,8 @@ function computeStats(logs) {
2586
2608
  const ConversationGroup = reactExports.memo(function({
2587
2609
  group,
2588
2610
  viewMode = "simple",
2589
- strip
2611
+ strip,
2612
+ cacheTrends
2590
2613
  }) {
2591
2614
  const [expanded, setExpanded] = reactExports.useState(false);
2592
2615
  const stats = computeStats(group.logs);
@@ -2616,7 +2639,8 @@ const ConversationGroup = reactExports.memo(function({
2616
2639
  log,
2617
2640
  viewMode,
2618
2641
  suppressApiFormatBadge: !mixed,
2619
- strip
2642
+ strip,
2643
+ cacheTrend: cacheTrends?.get(log.id) ?? null
2620
2644
  },
2621
2645
  log.id
2622
2646
  )) })
@@ -3873,6 +3897,29 @@ function ProxySettingsTab() {
3873
3897
  ] })
3874
3898
  ] });
3875
3899
  }
3900
+ function computeCacheTrends(groups) {
3901
+ const result = /* @__PURE__ */ new Map();
3902
+ for (const group of groups) {
3903
+ const logs = group.logs;
3904
+ for (let i = 1; i < logs.length; i++) {
3905
+ const prev = logs[i - 1];
3906
+ const curr = logs[i];
3907
+ if (prev === void 0 || curr === void 0) continue;
3908
+ result.set(curr.id, {
3909
+ creation: compareField(prev.cacheCreationInputTokens, curr.cacheCreationInputTokens),
3910
+ read: compareField(prev.cacheReadInputTokens, curr.cacheReadInputTokens)
3911
+ });
3912
+ }
3913
+ }
3914
+ return result;
3915
+ }
3916
+ function compareField(previous, current) {
3917
+ if (current === null) return null;
3918
+ if (previous === null) return null;
3919
+ if (current > previous) return { direction: "up", delta: current - previous };
3920
+ if (current < previous) return { direction: "down", delta: previous - current };
3921
+ return null;
3922
+ }
3876
3923
  function truncateSessionId(id) {
3877
3924
  if (id.length <= 30) return id;
3878
3925
  return id.slice(0, 12) + "…" + id.slice(-12);
@@ -3963,6 +4010,7 @@ function ProxyViewer({
3963
4010
  }, [logs]);
3964
4011
  const parentRef = reactExports.useRef(null);
3965
4012
  const groups = reactExports.useMemo(() => groupLogsByConversation(logs), [logs]);
4013
+ const cacheTrends = reactExports.useMemo(() => computeCacheTrends(groups), [groups]);
3966
4014
  const renderGroups = logs.length > 0 && groupedView && !(groups.length === 1 && groups[0]?.logs.length === logs.length);
3967
4015
  const rowVirtualizer = useVirtualizer({
3968
4016
  count: renderGroups ? groups.length : logs.length,
@@ -4103,7 +4151,15 @@ function ProxyViewer({
4103
4151
  width: "100%",
4104
4152
  transform: `translateY(${virtualRow.start}px)`
4105
4153
  },
4106
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationGroup, { group, viewMode, strip })
4154
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
4155
+ ConversationGroup,
4156
+ {
4157
+ group,
4158
+ viewMode,
4159
+ strip,
4160
+ cacheTrends
4161
+ }
4162
+ )
4107
4163
  },
4108
4164
  group.id
4109
4165
  );
@@ -4122,7 +4178,15 @@ function ProxyViewer({
4122
4178
  width: "100%",
4123
4179
  transform: `translateY(${virtualRow.start}px)`
4124
4180
  },
4125
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(LogEntry, { log, viewMode, strip })
4181
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
4182
+ LogEntry,
4183
+ {
4184
+ log,
4185
+ viewMode,
4186
+ strip,
4187
+ cacheTrend: cacheTrends.get(log.id) ?? null
4188
+ }
4189
+ )
4126
4190
  },
4127
4191
  log.id
4128
4192
  );
@@ -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-jl5h5rzb.mjs");
201
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-l1kWkG0h.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-CYFPDggE.mjs").then((n) => n.r);
770
+ const routerEntry = await import("./router-PZjNwOcw.mjs").then((n) => n.r);
771
771
  const startEntry = await import("./start-HYkvq4Ni.mjs");
772
772
  return { startEntry, routerEntry };
773
773
  }
@@ -44,7 +44,7 @@ import "../_libs/debounce-fn.mjs";
44
44
  import "../_libs/mimic-function.mjs";
45
45
  import "../_libs/semver.mjs";
46
46
  import "../_libs/uint8array-extras.mjs";
47
- const appCss = "/assets/index-DiYqfnSp.css";
47
+ const appCss = "/assets/index-DZx2yk8v.css";
48
48
  const Route$h = createRootRoute({
49
49
  head: () => ({
50
50
  meta: [
@@ -68,7 +68,7 @@ function RootDocument({ children }) {
68
68
  ] })
69
69
  ] });
70
70
  }
71
- const $$splitComponentImporter = () => import("./index-i-uTB3Y3.mjs");
71
+ const $$splitComponentImporter = () => import("./index-DhChP_jV.mjs");
72
72
  const Route$g = createFileRoute("/")({
73
73
  component: lazyRouteComponent($$splitComponentImporter, "component")
74
74
  });
@@ -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/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-7LuJWU4Q.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-D8CKc4zg.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/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-7LuJWU4Q.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/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BYCM7aJx.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DVgdkDgq.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/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-BYCM7aJx.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
35
35
  }
36
36
  });
37
37
  const assets = {
38
+ "/assets/alibaba-TTwafVwX.svg": {
39
+ "type": "image/svg+xml",
40
+ "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
+ "mtime": "2026-06-09T08:02:30.828Z",
42
+ "size": 5915,
43
+ "path": "../public/assets/alibaba-TTwafVwX.svg"
44
+ },
38
45
  "/assets/minimax-BPMzvuL-.jpeg": {
39
46
  "type": "image/jpeg",
40
47
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
41
- "mtime": "2026-06-08T10:48:17.641Z",
48
+ "mtime": "2026-06-09T08:02:30.828Z",
42
49
  "size": 6918,
43
50
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
44
51
  },
45
- "/assets/alibaba-TTwafVwX.svg": {
46
- "type": "image/svg+xml",
47
- "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
48
- "mtime": "2026-06-08T10:48:17.641Z",
49
- "size": 5915,
50
- "path": "../public/assets/alibaba-TTwafVwX.svg"
52
+ "/assets/index-DZx2yk8v.css": {
53
+ "type": "text/css; charset=utf-8",
54
+ "etag": '"1177b-93l31JbQrAbdmLoUOxZGbqTQwcc"',
55
+ "mtime": "2026-06-09T08:02:30.830Z",
56
+ "size": 71547,
57
+ "path": "../public/assets/index-DZx2yk8v.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-08T10:48:17.641Z",
62
+ "mtime": "2026-06-09T08:02:30.828Z",
56
63
  "size": 11256,
57
64
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
58
65
  },
59
- "/assets/index-DiYqfnSp.css": {
60
- "type": "text/css; charset=utf-8",
61
- "etag": '"11755-g+paI7gxmWkWiKXnB74OdQXg0jE"',
62
- "mtime": "2026-06-08T10:48:17.641Z",
63
- "size": 71509,
64
- "path": "../public/assets/index-DiYqfnSp.css"
65
- },
66
- "/assets/main-7LuJWU4Q.js": {
66
+ "/assets/main-BYCM7aJx.js": {
67
67
  "type": "text/javascript; charset=utf-8",
68
- "etag": '"50599-3DNmqszgLIzQHbEIKYFnnD++vQs"',
69
- "mtime": "2026-06-08T10:48:17.641Z",
68
+ "etag": '"50599-LT3fOJsfcl49n+Hp+rSe62uyiuk"',
69
+ "mtime": "2026-06-09T08:02:30.830Z",
70
70
  "size": 329113,
71
- "path": "../public/assets/main-7LuJWU4Q.js"
71
+ "path": "../public/assets/main-BYCM7aJx.js"
72
72
  },
73
73
  "/assets/qwen-CONDcHqt.png": {
74
74
  "type": "image/png",
75
75
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-08T10:48:17.641Z",
76
+ "mtime": "2026-06-09T08:02:30.830Z",
77
77
  "size": 357059,
78
78
  "path": "../public/assets/qwen-CONDcHqt.png"
79
79
  },
80
- "/assets/index-D8CKc4zg.js": {
80
+ "/assets/index-DVgdkDgq.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"8a3c6-soGG4aENc//lrPUXq49D1Gz8wRA"',
83
- "mtime": "2026-06-08T10:48:17.642Z",
84
- "size": 566214,
85
- "path": "../public/assets/index-D8CKc4zg.js"
82
+ "etag": '"8a87f-jt4qwgWrMm8cZtArzPx7qX8p12k"',
83
+ "mtime": "2026-06-09T08:02:30.830Z",
84
+ "size": 567423,
85
+ "path": "../public/assets/index-DVgdkDgq.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/README.md CHANGED
@@ -40,7 +40,7 @@ llm-inspector
40
40
  ### Docker
41
41
 
42
42
  ```bash
43
- docker-compose up -d
43
+ cd docker && docker compose up -d
44
44
  ```
45
45
 
46
46
  ### 源码运行
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.11.7",
3
+ "version": "1.12.0",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@ import {
13
13
  import { CrabLogo } from "./ui/crab-logo";
14
14
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
15
15
  import { SettingsDialog } from "./providers/SettingsDialog";
16
+ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
16
17
 
17
18
  function truncateSessionId(id: string): string {
18
19
  if (id.length <= 30) return id;
@@ -124,6 +125,7 @@ export function ProxyViewer({
124
125
  const parentRef = useRef<HTMLDivElement>(null);
125
126
 
126
127
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
128
+ const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
127
129
 
128
130
  // Determine what items to render (groups or individual logs)
129
131
  const renderGroups =
@@ -303,7 +305,12 @@ export function ProxyViewer({
303
305
  transform: `translateY(${virtualRow.start}px)`,
304
306
  }}
305
307
  >
306
- <ConversationGroup group={group} viewMode={viewMode} strip={strip} />
308
+ <ConversationGroup
309
+ group={group}
310
+ viewMode={viewMode}
311
+ strip={strip}
312
+ cacheTrends={cacheTrends}
313
+ />
307
314
  </div>
308
315
  );
309
316
  } else {
@@ -322,7 +329,12 @@ export function ProxyViewer({
322
329
  transform: `translateY(${virtualRow.start}px)`,
323
330
  }}
324
331
  >
325
- <LogEntry log={log} viewMode={viewMode} strip={strip} />
332
+ <LogEntry
333
+ log={log}
334
+ viewMode={viewMode}
335
+ strip={strip}
336
+ cacheTrend={cacheTrends.get(log.id) ?? null}
337
+ />
326
338
  </div>
327
339
  );
328
340
  }
@@ -9,12 +9,18 @@ import {
9
9
  type ConversationGroupData,
10
10
  } from "./ConversationHeader";
11
11
  import { LogEntry } from "./LogEntry";
12
+ import type { CacheTrendEntry } from "./cacheTrend";
12
13
 
13
14
  export type ConversationGroupProps = {
14
15
  group: ConversationGroupData;
15
16
  viewMode?: "simple" | "full";
16
17
  /** Live strip-Claude-Code-billing-header flag from the viewer container. */
17
18
  strip: boolean;
19
+ /**
20
+ * Pre-computed per-log cache token trend map (keyed by `log.id`) shared
21
+ * across the whole viewer. Each `LogEntry` looks up its own entry.
22
+ */
23
+ cacheTrends?: Map<number, CacheTrendEntry>;
18
24
  };
19
25
 
20
26
  function computeStats(logs: CapturedLog[]): {
@@ -34,6 +40,7 @@ export const ConversationGroup = memo(function ({
34
40
  group,
35
41
  viewMode = "simple",
36
42
  strip,
43
+ cacheTrends,
37
44
  }: ConversationGroupProps): JSX.Element {
38
45
  const [expanded, setExpanded] = useState(false);
39
46
 
@@ -73,6 +80,7 @@ export const ConversationGroup = memo(function ({
73
80
  viewMode={viewMode}
74
81
  suppressApiFormatBadge={!mixed}
75
82
  strip={strip}
83
+ cacheTrend={cacheTrends?.get(log.id) ?? null}
76
84
  />
77
85
  ))}
78
86
  </div>
@@ -12,6 +12,7 @@ import { LogEntryHeader } from "./LogEntryHeader";
12
12
  import { ReplayDialog } from "./ReplayDialog";
13
13
  import { ResponseView } from "./ResponseView";
14
14
  import { StreamingChunkSequence } from "./StreamingChunkSequence";
15
+ import type { CacheTrendEntry } from "./cacheTrend";
15
16
 
16
17
  export type LogEntryProps = {
17
18
  log: CapturedLog;
@@ -25,6 +26,11 @@ export type LogEntryProps = {
25
26
  * cost).
26
27
  */
27
28
  strip: boolean;
29
+ /**
30
+ * Per-log cache token trend, looked up in the viewer-level trend map.
31
+ * `null` (or absent) means the header should render with no arrows.
32
+ */
33
+ cacheTrend?: CacheTrendEntry | null;
28
34
  };
29
35
 
30
36
  /**
@@ -131,6 +137,7 @@ export const LogEntry = memo(function ({
131
137
  viewMode = "simple",
132
138
  suppressApiFormatBadge = false,
133
139
  strip,
140
+ cacheTrend = null,
134
141
  }: LogEntryProps): JSX.Element {
135
142
  const [expanded, setExpanded] = useState<boolean>(false);
136
143
  const [requestCopied, setRequestCopied] = useState<boolean>(false);
@@ -193,6 +200,7 @@ export const LogEntry = memo(function ({
193
200
  expanded={expanded}
194
201
  onToggle={() => setExpanded(!expanded)}
195
202
  suppressApiFormatBadge={suppressApiFormatBadge}
203
+ cacheTrend={cacheTrend}
196
204
  />
197
205
 
198
206
  {expanded && (
@@ -1,4 +1,6 @@
1
1
  import {
2
+ ArrowDown,
3
+ ArrowUp,
2
4
  ChevronDown,
3
5
  ChevronRight,
4
6
  Clock,
@@ -17,6 +19,7 @@ import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../
17
19
  import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
18
20
  import { Badge } from "../ui/badge";
19
21
  import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
22
+ import type { CacheTrend } from "./cacheTrend";
20
23
 
21
24
  function formatElapsed(ms: number): string {
22
25
  if (ms < 1000) return `${ms}ms`;
@@ -30,6 +33,26 @@ const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
30
33
  pending: "bg-muted text-muted-foreground border-border",
31
34
  };
32
35
 
36
+ /**
37
+ * Inline trend indicator: small arrow (green up / red down) plus the absolute
38
+ * delta in compact form. Returns `null` when there is no trend to display.
39
+ */
40
+ function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Element | null {
41
+ if (trend === null) return null;
42
+ const isUp = trend.direction === "up";
43
+ const Icon = isUp ? ArrowUp : ArrowDown;
44
+ const sign = isUp ? "+" : "-";
45
+ return (
46
+ <span className="flex items-center gap-0.5 text-muted-foreground tabular-nums">
47
+ <Icon className={isUp ? "size-3 text-emerald-400" : "size-3 text-rose-400"} />
48
+ <span className="font-mono">
49
+ {sign}
50
+ {formatTokens(trend.delta)}
51
+ </span>
52
+ </span>
53
+ );
54
+ }
55
+
33
56
  export type LogEntryHeaderProps = {
34
57
  log: CapturedLog;
35
58
  parsedRequest: InspectorRequest | null;
@@ -37,6 +60,12 @@ export type LogEntryHeaderProps = {
37
60
  onToggle: () => void;
38
61
  /** Suppress the API format badge when log is displayed within a group */
39
62
  suppressApiFormatBadge?: boolean;
63
+ /**
64
+ * Per-log cache token trend (creation + read) relative to the previous log
65
+ * in the same conversation group. When `undefined` or a field is `null`,
66
+ * the corresponding cache span renders as it did before — no arrow.
67
+ */
68
+ cacheTrend?: { creation: CacheTrend | null; read: CacheTrend | null } | null;
40
69
  };
41
70
 
42
71
  export const LogEntryHeader = memo(function ({
@@ -45,6 +74,7 @@ export const LogEntryHeader = memo(function ({
45
74
  expanded,
46
75
  onToggle,
47
76
  suppressApiFormatBadge = false,
77
+ cacheTrend = null,
48
78
  }: LogEntryHeaderProps): JSX.Element {
49
79
  const statusCategory = getStatusCategory(log.responseStatus);
50
80
 
@@ -163,6 +193,7 @@ export const LogEntryHeader = memo(function ({
163
193
  {/* Cache tokens */}
164
194
  {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
165
195
  <span className="flex items-center gap-1 text-xs shrink-0">
196
+ <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
166
197
  <span className="font-mono tabular-nums text-emerald-400">
167
198
  Cache +{formatTokens(log.cacheCreationInputTokens)}
168
199
  </span>
@@ -170,6 +201,7 @@ export const LogEntryHeader = memo(function ({
170
201
  )}
171
202
  {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
172
203
  <span className="flex items-center gap-1 text-xs shrink-0">
204
+ <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
173
205
  <span className="font-mono tabular-nums text-purple-400">
174
206
  Cache ~{formatTokens(log.cacheReadInputTokens)}
175
207
  </span>