@tonyclaw/llm-inspector 1.10.0 → 1.11.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.
Files changed (74) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-BIZeMR5N.js +105 -0
  3. package/.output/public/assets/index-SmtLx1lM.css +1 -0
  4. package/.output/public/assets/main-QfbQ5oBP.js +17 -0
  5. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +1 -1
  6. package/.output/server/_libs/ajv-formats.mjs +18 -18
  7. package/.output/server/_libs/ajv.mjs +6306 -942
  8. package/.output/server/_libs/cookie-es.mjs +21 -7
  9. package/.output/server/_libs/diff.mjs +320 -0
  10. package/.output/server/_libs/floating-ui__core.mjs +17 -18
  11. package/.output/server/_libs/floating-ui__dom.mjs +20 -24
  12. package/.output/server/_libs/floating-ui__react-dom.mjs +28 -55
  13. package/.output/server/_libs/floating-ui__utils.mjs +36 -36
  14. package/.output/server/_libs/h3-v2.mjs +9 -20
  15. package/.output/server/_libs/isbot.mjs +2 -3
  16. package/.output/server/_libs/json-schema-traverse.mjs +90 -1
  17. package/.output/server/_libs/jszip.mjs +28 -28
  18. package/.output/server/_libs/lucide-react.mjs +103 -80
  19. package/.output/server/_libs/mdast-util-from-markdown.mjs +1 -1
  20. package/.output/server/_libs/pako.mjs +13 -13
  21. package/.output/server/_libs/property-information.mjs +1 -13
  22. package/.output/server/_libs/radix-ui__react-collection.mjs +1 -1
  23. package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
  24. package/.output/server/_libs/react-dom.mjs +10 -10
  25. package/.output/server/_libs/react.mjs +44 -44
  26. package/.output/server/_libs/readable-stream.mjs +15 -15
  27. package/.output/server/_libs/rou3.mjs +7 -1
  28. package/.output/server/_libs/safe-buffer.mjs +3 -3
  29. package/.output/server/_libs/semver.mjs +10 -10
  30. package/.output/server/_libs/seroval-plugins.mjs +5 -5
  31. package/.output/server/_libs/seroval.mjs +596 -606
  32. package/.output/server/_libs/srvx.mjs +245 -10
  33. package/.output/server/_libs/swr.mjs +1 -1
  34. package/.output/server/_libs/tailwind-merge.mjs +41 -286
  35. package/.output/server/_libs/tanstack__history.mjs +44 -31
  36. package/.output/server/_libs/tanstack__react-router.mjs +1089 -780
  37. package/.output/server/_libs/tanstack__react-store.mjs +1 -1
  38. package/.output/server/_libs/tanstack__react-virtual.mjs +7 -82
  39. package/.output/server/_libs/tanstack__router-core.mjs +2331 -2226
  40. package/.output/server/_libs/tanstack__virtual-core.mjs +17 -58
  41. package/.output/server/_libs/tiny-invariant.mjs +12 -0
  42. package/.output/server/_libs/tiny-warning.mjs +5 -0
  43. package/.output/server/_libs/ungap__structured-clone.mjs +2 -14
  44. package/.output/server/_libs/use-sync-external-store.mjs +1 -1
  45. package/.output/server/_libs/zod.mjs +207 -505
  46. package/.output/server/_ssr/{index-XyU4WVWt.mjs → index-I_Nx8QEB.mjs} +532 -79
  47. package/.output/server/_ssr/index.mjs +776 -1098
  48. package/.output/server/_ssr/{router-BR4XO8XJ.mjs → router-BVxfVFsb.mjs} +98 -53
  49. package/.output/server/_tanstack-start-manifest_v-Cm2Owszu.mjs +4 -0
  50. package/.output/server/index.mjs +28 -28
  51. package/README.md +1 -1
  52. package/package.json +2 -1
  53. package/src/components/ProxyViewer.tsx +10 -3
  54. package/src/components/ProxyViewerContainer.tsx +6 -0
  55. package/src/components/proxy-viewer/ConversationGroup.tsx +10 -1
  56. package/src/components/proxy-viewer/LogEntry.tsx +114 -5
  57. package/src/components/proxy-viewer/diff/DiffView.tsx +319 -0
  58. package/src/components/proxy-viewer/diff/computeDiff.ts +178 -0
  59. package/src/components/proxy-viewer/diff/index.ts +3 -0
  60. package/src/components/ui/crab-logo.tsx +45 -0
  61. package/src/proxy/config.ts +3 -17
  62. package/src/proxy/dataDir.ts +56 -0
  63. package/src/proxy/formats/anthropic/stream.ts +5 -2
  64. package/src/proxy/formats/jsonSchema.ts +12 -0
  65. package/src/proxy/formats/openai/stream.ts +21 -16
  66. package/src/proxy/handler.ts +15 -5
  67. package/src/proxy/providers.ts +61 -1
  68. package/src/proxy/schemas.ts +42 -0
  69. package/src/proxy/store.ts +6 -23
  70. package/.output/public/assets/index-BFuUQkWu.js +0 -11
  71. package/.output/public/assets/index-F2ojFPJp.css +0 -1
  72. package/.output/public/assets/index-nKnhyW4Y.js +0 -122
  73. package/.output/server/_ssr/empty-plugin-adapters-BFgPZ6_d.mjs +0 -6
  74. package/.output/server/_tanstack-start-manifest_v-DFoSjBkb.mjs +0 -4
@@ -1,17 +1,19 @@
1
1
  import { c as createRouter, a as createRootRoute, b as createFileRoute, l as lazyRouteComponent, O as Outlet, H as HeadContent, S as Scripts } from "../_libs/tanstack__react-router.mjs";
2
2
  import { j as jsxRuntimeExports } from "../_libs/react.mjs";
3
3
  import { S as SWRConfig } from "../_libs/swr.mjs";
4
- import { mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync, existsSync, readFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync, readFileSync } from "node:fs";
5
5
  import path, { join, isAbsolute, dirname } from "node:path";
6
- import { mkdir, appendFile, readdir, stat, unlink, readFile, writeFile } from "node:fs/promises";
6
+ import { mkdir, appendFile, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
7
7
  import { C as Conf } from "../_libs/conf.mjs";
8
8
  import { randomUUID } from "crypto";
9
9
  import { exec } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
- import { o as object, s as string, _ as _enum, u as union, a as array, b as boolean, n as number, d as discriminatedUnion, l as literal, r as record, c as lazy, e as _null, f as unknown } from "../_libs/zod.mjs";
11
+ import { o as object, s as string, _ as _enum, u as union, a as array, b as boolean, n as number, d as discriminatedUnion, l as literal, r as record, c as _null, e as lazy, f as unknown } from "../_libs/zod.mjs";
12
+ import "../_libs/tiny-warning.mjs";
12
13
  import "../_libs/tanstack__router-core.mjs";
13
- import "../_libs/tanstack__history.mjs";
14
14
  import "../_libs/cookie-es.mjs";
15
+ import "../_libs/tanstack__history.mjs";
16
+ import "../_libs/tiny-invariant.mjs";
15
17
  import "../_libs/seroval.mjs";
16
18
  import "../_libs/seroval-plugins.mjs";
17
19
  import "node:stream/web";
@@ -42,7 +44,7 @@ import "../_libs/debounce-fn.mjs";
42
44
  import "../_libs/mimic-function.mjs";
43
45
  import "../_libs/semver.mjs";
44
46
  import "../_libs/uint8array-extras.mjs";
45
- const appCss = "/assets/index-F2ojFPJp.css";
47
+ const appCss = "/assets/index-SmtLx1lM.css";
46
48
  const Route$h = createRootRoute({
47
49
  head: () => ({
48
50
  meta: [
@@ -66,7 +68,7 @@ function RootDocument({ children }) {
66
68
  ] })
67
69
  ] });
68
70
  }
69
- const $$splitComponentImporter = () => import("./index-XyU4WVWt.mjs");
71
+ const $$splitComponentImporter = () => import("./index-I_Nx8QEB.mjs");
70
72
  const Route$g = createFileRoute("/")({
71
73
  component: lazyRouteComponent($$splitComponentImporter, "component")
72
74
  });
@@ -328,6 +330,9 @@ const JsonValueSchema = lazy(
328
330
  record(string(), JsonValueSchema)
329
331
  ])
330
332
  );
333
+ function trustAsJsonValue(value) {
334
+ return value;
335
+ }
331
336
  const CacheControl = object({
332
337
  type: string(),
333
338
  ttl: string().optional(),
@@ -698,6 +703,25 @@ function extractModelFromBody(body) {
698
703
  return null;
699
704
  }
700
705
  }
706
+ const LooseRequestSchema = object({
707
+ model: string().optional(),
708
+ metadata: object({ user_id: string().optional() }).passthrough().optional()
709
+ });
710
+ function extractRequestMetadata(body, headers) {
711
+ if (body === null) return { model: null, sessionId: null };
712
+ try {
713
+ const json = JSON.parse(body);
714
+ const loose = LooseRequestSchema.safeParse(json);
715
+ if (loose.success) {
716
+ return {
717
+ model: loose.data.model ?? null,
718
+ sessionId: loose.data.metadata?.user_id ?? headers.get("x-session-affinity") ?? null
719
+ };
720
+ }
721
+ } catch {
722
+ }
723
+ return { model: null, sessionId: null };
724
+ }
701
725
  function parseRequest(rawBody) {
702
726
  if (rawBody === null) return null;
703
727
  try {
@@ -777,10 +801,6 @@ function readChunks(path2) {
777
801
  return null;
778
802
  }
779
803
  }
780
- const LooseRequestSchema = object({
781
- model: string().optional(),
782
- metadata: object({ user_id: string().optional() }).passthrough().optional()
783
- });
784
804
  const MAX_MEMORY_CACHE = 100;
785
805
  const memoryCache = /* @__PURE__ */ new Map();
786
806
  let headId = null;
@@ -840,23 +860,10 @@ async function addTestLogEntry(entry) {
840
860
  emitLogUpdate(log);
841
861
  return log;
842
862
  }
843
- async function createLog(method, path2, requestBody, headers, clientInfo, rawHeaders, upstreamHeaders, apiFormat = "unknown") {
844
- let model = null;
845
- let sessionId = null;
846
- if (requestBody !== null) {
847
- try {
848
- const json = JSON.parse(requestBody);
849
- const loose = LooseRequestSchema.safeParse(json);
850
- if (loose.success) {
851
- model = loose.data.model ?? null;
852
- sessionId = loose.data.metadata?.user_id ?? headers.get("x-session-affinity") ?? null;
853
- }
854
- } catch {
855
- }
856
- }
863
+ async function createLog(method, path2, requestBody, headers, clientInfo, rawHeaders, upstreamHeaders, apiFormat = "unknown", model = null, sessionId = null, preAcquiredId) {
857
864
  const userAgent = headers.get("user-agent");
858
865
  const origin = headers.get("origin");
859
- const id = await getNextLogId();
866
+ const id = preAcquiredId ?? await getNextLogId();
860
867
  const log = {
861
868
  id,
862
869
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1098,7 +1105,10 @@ function extractAnthropicStream(raw, log, fallbackModel, collectChunks) {
1098
1105
  index: chunkIndex,
1099
1106
  timestamp: Date.now() - streamStartMs,
1100
1107
  type: data.type,
1101
- data: JsonValueSchema.parse(data)
1108
+ // `data` has already been validated by SseEventSchema above; the
1109
+ // recursive JsonValue walk is a redundant full-tree zod parse on
1110
+ // every SSE event. See trustAsJsonValue for the rationale.
1111
+ data: trustAsJsonValue(data)
1102
1112
  });
1103
1113
  }
1104
1114
  chunkIndex++;
@@ -1325,7 +1335,7 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1325
1335
  let completionTokens = 0;
1326
1336
  let usageCaptured = false;
1327
1337
  let started = false;
1328
- const toolCalls = [];
1338
+ const toolCalls = /* @__PURE__ */ new Map();
1329
1339
  const MAX_CHUNKS = 1e3;
1330
1340
  let chunkIndex = 0;
1331
1341
  let streamStartMs = 0;
@@ -1352,15 +1362,12 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1352
1362
  const chunk = chunkResult.data;
1353
1363
  if (chunkIndex === 0) streamStartMs = Date.now();
1354
1364
  if (collectChunks === true && chunks.length < MAX_CHUNKS) {
1355
- const jsonResult = JsonValueSchema.safeParse(chunk);
1356
- if (jsonResult.success) {
1357
- chunks.push({
1358
- index: chunkIndex,
1359
- timestamp: Date.now() - streamStartMs,
1360
- type: "chat.completion.chunk",
1361
- data: jsonResult.data
1362
- });
1363
- }
1365
+ chunks.push({
1366
+ index: chunkIndex,
1367
+ timestamp: Date.now() - streamStartMs,
1368
+ type: "chat.completion.chunk",
1369
+ data: trustAsJsonValue(chunk)
1370
+ });
1364
1371
  }
1365
1372
  chunkIndex++;
1366
1373
  if (!started) {
@@ -1394,14 +1401,14 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1394
1401
  }
1395
1402
  if (delta.tool_calls !== void 0 && delta.tool_calls !== null) {
1396
1403
  for (const tc of delta.tool_calls) {
1397
- let existing = toolCalls.find((t) => t.index === tc.index);
1398
- if (!existing) {
1404
+ let existing = toolCalls.get(tc.index);
1405
+ if (existing === void 0) {
1399
1406
  existing = {
1400
1407
  index: tc.index,
1401
1408
  type: "function",
1402
1409
  function: { name: "", arguments: "" }
1403
1410
  };
1404
- toolCalls.push(existing);
1411
+ toolCalls.set(tc.index, existing);
1405
1412
  }
1406
1413
  if (tc.id !== void 0) existing.id = tc.id;
1407
1414
  if (tc.function?.name !== void 0) existing.function.name += tc.function.name;
@@ -1428,7 +1435,7 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1428
1435
  content: completionContent
1429
1436
  };
1430
1437
  if (reasoningContent) message.reasoning_content = reasoningContent;
1431
- if (toolCalls.length > 0) message.tool_calls = toolCalls;
1438
+ if (toolCalls.size > 0) message.tool_calls = [...toolCalls.values()];
1432
1439
  return JSON.stringify({
1433
1440
  id,
1434
1441
  object: "chat.completion",
@@ -1549,6 +1556,22 @@ const alibabaProvider = {
1549
1556
  }
1550
1557
  };
1551
1558
  registry.register(alibabaProvider);
1559
+ function getDataDir() {
1560
+ const isWindows = process.platform === "win32";
1561
+ const base = isWindows ? process.env["APPDATA"] ?? join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector") : process.env["HOME"] ?? "/tmp";
1562
+ const dirEnv = process.env["LLM_INSPECTOR_DATA_DIR"];
1563
+ if (dirEnv !== void 0 && dirEnv !== "") {
1564
+ return isAbsolute(dirEnv) ? dirEnv : join(base, dirEnv);
1565
+ }
1566
+ const legacyDirEnv = process.env["LLM_INSPECTOR_CONFIG_DIR"];
1567
+ if (legacyDirEnv !== void 0 && legacyDirEnv !== "") {
1568
+ return isAbsolute(legacyDirEnv) ? legacyDirEnv : join(base, legacyDirEnv);
1569
+ }
1570
+ return join(base, ".llm-inspector");
1571
+ }
1572
+ function hasExplicitDataDir() {
1573
+ return process.env["LLM_INSPECTOR_DATA_DIR"] !== void 0 && process.env["LLM_INSPECTOR_DATA_DIR"] !== "" || process.env["LLM_INSPECTOR_CONFIG_DIR"] !== void 0 && process.env["LLM_INSPECTOR_CONFIG_DIR"] !== "";
1574
+ }
1552
1575
  const ProviderConfigSchema = object({
1553
1576
  id: string(),
1554
1577
  name: string(),
@@ -1572,14 +1595,42 @@ const ProviderConfigSchema = object({
1572
1595
  object({
1573
1596
  providers: array(ProviderConfigSchema)
1574
1597
  });
1598
+ function migrateFromLegacyConfLocation(targetStore) {
1599
+ if (process.env["LLM_INSPECTOR_CONFIG_PATH"] !== void 0) return;
1600
+ if (hasExplicitDataDir()) return;
1601
+ if (targetStore.get("providers", []).length > 0) return;
1602
+ let legacyStore;
1603
+ try {
1604
+ legacyStore = new Conf({
1605
+ projectName: "llm-inspector",
1606
+ defaults: { providers: [] }
1607
+ });
1608
+ const legacyProviders = legacyStore.get("providers", []);
1609
+ if (legacyProviders.length === 0) return;
1610
+ targetStore.set("providers", legacyProviders);
1611
+ logger.info(
1612
+ `[providers] Migrated ${legacyProviders.length} provider(s) from ${legacyStore.path} to ${targetStore.path}`
1613
+ );
1614
+ } catch (err) {
1615
+ logger.warn(`[providers] Legacy migration failed: ${String(err)}`);
1616
+ } finally {
1617
+ legacyStore?._closeWatcher();
1618
+ }
1619
+ }
1575
1620
  const configPath = process.env["LLM_INSPECTOR_CONFIG_PATH"];
1621
+ const dataDir = getDataDir();
1622
+ if (configPath === void 0) {
1623
+ mkdirSync(dataDir, { recursive: true });
1624
+ }
1576
1625
  const store = new Conf({
1577
- projectName: "llm-inspector",
1626
+ cwd: dataDir,
1627
+ configName: "providers",
1578
1628
  defaults: {
1579
1629
  providers: []
1580
1630
  },
1581
1631
  ...configPath !== void 0 ? { path: configPath } : {}
1582
1632
  });
1633
+ migrateFromLegacyConfLocation(store);
1583
1634
  function migrateProvider(p) {
1584
1635
  const currentAnthropicUrl = p.anthropicBaseUrl ?? "";
1585
1636
  const currentOpenaiUrl = p.openaiBaseUrl ?? "";
@@ -1930,15 +1981,6 @@ async function getClientInfo(request) {
1930
1981
  const RuntimeConfigSchema = object({
1931
1982
  stripClaudeCodeBillingHeader: boolean()
1932
1983
  });
1933
- function getDataDir() {
1934
- const isWindows = process.platform === "win32";
1935
- const base = isWindows ? process.env["APPDATA"] ?? join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector") : process.env["HOME"] ?? "/tmp";
1936
- const configDirEnv = process.env["LLM_INSPECTOR_CONFIG_DIR"];
1937
- if (configDirEnv !== void 0) {
1938
- return isAbsolute(configDirEnv) ? configDirEnv : join(base, configDirEnv);
1939
- }
1940
- return join(base, ".llm-inspector");
1941
- }
1942
1984
  function getConfigFilePath() {
1943
1985
  return join(getDataDir(), "config.json");
1944
1986
  }
@@ -2265,7 +2307,7 @@ async function handleProxy(req) {
2265
2307
  bodyToForward = stripped.body;
2266
2308
  }
2267
2309
  }
2268
- const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
2310
+ const { model, sessionId } = extractRequestMetadata(requestBody, req.headers);
2269
2311
  const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
2270
2312
  const upstreamBase = selectUpstreamBase$1(parsed.isChatCompletions, matchedProviderConfig);
2271
2313
  const upstreamUrl = buildUpstreamUrl$1(upstreamBase, parsed.normalizedPath);
@@ -2290,7 +2332,7 @@ async function handleProxy(req) {
2290
2332
  if (formatHandler === null) {
2291
2333
  return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
2292
2334
  }
2293
- const clientInfo = await getClientInfo(req);
2335
+ const [clientInfo, preAcquiredId] = await Promise.all([getClientInfo(req), getNextLogId()]);
2294
2336
  const upstreamHeadersObj = {};
2295
2337
  upstreamHeaders.forEach((value, key) => {
2296
2338
  upstreamHeadersObj[key.toLowerCase()] = value;
@@ -2303,7 +2345,10 @@ async function handleProxy(req) {
2303
2345
  clientInfo,
2304
2346
  rawHeaders,
2305
2347
  upstreamHeadersObj,
2306
- formatHandler.format
2348
+ formatHandler.format,
2349
+ model,
2350
+ sessionId,
2351
+ preAcquiredId
2307
2352
  );
2308
2353
  let upstreamRes;
2309
2354
  try {
@@ -0,0 +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-QfbQ5oBP.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-BIZeMR5N.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-QfbQ5oBP.js" });
2
+ export {
3
+ tsrStartManifest
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-06T06:10:03.367Z",
41
+ "mtime": "2026-06-06T08:18:21.509Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/index-F2ojFPJp.css": {
46
- "type": "text/css; charset=utf-8",
47
- "etag": '"10ec0-6l9kmLR4Pe6YaaY4kOiwBp4tyLs"',
48
- "mtime": "2026-06-06T06:10:03.368Z",
49
- "size": 69312,
50
- "path": "../public/assets/index-F2ojFPJp.css"
51
- },
52
- "/assets/index-BFuUQkWu.js": {
53
- "type": "text/javascript; charset=utf-8",
54
- "etag": '"5103c-l+8AHdGxgCZ2mb5rpeb9f++id64"',
55
- "mtime": "2026-06-06T06:10:03.369Z",
56
- "size": 331836,
57
- "path": "../public/assets/index-BFuUQkWu.js"
58
- },
59
45
  "/assets/minimax-BPMzvuL-.jpeg": {
60
46
  "type": "image/jpeg",
61
47
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
62
- "mtime": "2026-06-06T06:10:03.368Z",
48
+ "mtime": "2026-06-06T08:18:21.509Z",
63
49
  "size": 6918,
64
50
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
65
51
  },
66
- "/assets/qwen-CONDcHqt.png": {
67
- "type": "image/png",
68
- "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
69
- "mtime": "2026-06-06T06:10:03.368Z",
70
- "size": 357059,
71
- "path": "../public/assets/qwen-CONDcHqt.png"
52
+ "/assets/index-SmtLx1lM.css": {
53
+ "type": "text/css; charset=utf-8",
54
+ "etag": '"11492-YrH3CdmyZtpurvkMiG+Z8YMKpbg"',
55
+ "mtime": "2026-06-06T08:18:21.509Z",
56
+ "size": 70802,
57
+ "path": "../public/assets/index-SmtLx1lM.css"
72
58
  },
73
59
  "/assets/zhipuai-BPNAnxo-.svg": {
74
60
  "type": "image/svg+xml",
75
61
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
76
- "mtime": "2026-06-06T06:10:03.367Z",
62
+ "mtime": "2026-06-06T08:18:21.506Z",
77
63
  "size": 11256,
78
64
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
79
65
  },
80
- "/assets/index-nKnhyW4Y.js": {
66
+ "/assets/main-QfbQ5oBP.js": {
67
+ "type": "text/javascript; charset=utf-8",
68
+ "etag": '"50599-yvOA/5xDdT1OIDnPKqlDcpJMeK8"',
69
+ "mtime": "2026-06-06T08:18:21.511Z",
70
+ "size": 329113,
71
+ "path": "../public/assets/main-QfbQ5oBP.js"
72
+ },
73
+ "/assets/qwen-CONDcHqt.png": {
74
+ "type": "image/png",
75
+ "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
+ "mtime": "2026-06-06T08:18:21.509Z",
77
+ "size": 357059,
78
+ "path": "../public/assets/qwen-CONDcHqt.png"
79
+ },
80
+ "/assets/index-BIZeMR5N.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"88176-QQw6RfsFjx/+JC7JvSklqQcNnXg"',
83
- "mtime": "2026-06-06T06:10:03.369Z",
84
- "size": 557430,
85
- "path": "../public/assets/index-nKnhyW4Y.js"
82
+ "etag": '"899b2-Z4qeBA7/hHMhVcDcD+o7Iw8hgNw"',
83
+ "mtime": "2026-06-06T08:18:21.511Z",
84
+ "size": 563634,
85
+ "path": "../public/assets/index-BIZeMR5N.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # llm-inspector
2
2
 
3
- > LLM API 透明代理调试器 / Transparent LLM API proxy inspector
3
+ > LLM Inspector
4
4
 
5
5
  ![Proxy Overview](docs/Proxy.png)
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.10.0",
3
+ "version": "1.11.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",
@@ -55,6 +55,7 @@
55
55
  "class-variance-authority": "^0.7.1",
56
56
  "clsx": "^2.1.1",
57
57
  "conf": "^15.1.0",
58
+ "diff": "^9.0.0",
58
59
  "jszip": "^3.10.1",
59
60
  "lucide-react": "^0.563.0",
60
61
  "radix-ui": "^1.4.3",
@@ -10,6 +10,7 @@ import {
10
10
  LogEntry,
11
11
  type ConversationGroupData,
12
12
  } from "./proxy-viewer";
13
+ import { CrabLogo } from "./ui/crab-logo";
13
14
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
14
15
  import { SettingsDialog } from "./providers/SettingsDialog";
15
16
 
@@ -91,6 +92,8 @@ export type ProxyViewerProps = {
91
92
  onClearAll: () => void;
92
93
  viewMode: "simple" | "full";
93
94
  onViewModeChange: (mode: "simple" | "full") => void;
95
+ /** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
96
+ strip: boolean;
94
97
  };
95
98
 
96
99
  export function ProxyViewer({
@@ -104,6 +107,7 @@ export function ProxyViewer({
104
107
  onClearAll,
105
108
  viewMode,
106
109
  onViewModeChange,
110
+ strip,
107
111
  }: ProxyViewerProps): JSX.Element {
108
112
  const { totalIn, totalOut } = computeTokenSummary(logs);
109
113
  const [groupedView, setGroupedView] = useState(true);
@@ -142,7 +146,10 @@ export function ProxyViewer({
142
146
  <div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
143
147
  {/* Header */}
144
148
  <div className="flex items-center gap-4 mb-4 px-6 pt-6">
145
- <h1 className="text-lg font-bold flex-1">LLM Proxy Inspector</h1>
149
+ <h1 className="text-lg font-bold flex-1 flex items-center gap-2">
150
+ <CrabLogo className="size-10 text-amber-500" />
151
+ LLM Inspector
152
+ </h1>
146
153
  <span className="text-muted-foreground text-xs font-mono">v{packageJson.version}</span>
147
154
  <div className="flex items-center border border-border rounded-md overflow-hidden">
148
155
  <button
@@ -294,7 +301,7 @@ export function ProxyViewer({
294
301
  transform: `translateY(${virtualRow.start}px)`,
295
302
  }}
296
303
  >
297
- <ConversationGroup group={group} viewMode={viewMode} />
304
+ <ConversationGroup group={group} viewMode={viewMode} strip={strip} />
298
305
  </div>
299
306
  );
300
307
  } else {
@@ -313,7 +320,7 @@ export function ProxyViewer({
313
320
  transform: `translateY(${virtualRow.start}px)`,
314
321
  }}
315
322
  >
316
- <LogEntry log={log} viewMode={viewMode} />
323
+ <LogEntry log={log} viewMode={viewMode} strip={strip} />
317
324
  </div>
318
325
  );
319
326
  }
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback, useRef, type JSX } from "react";
2
2
  import { z } from "zod";
3
3
  import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
4
+ import { useStripConfig } from "../lib/useStripConfig";
4
5
  import { ProxyViewer } from "./ProxyViewer";
5
6
 
6
7
  type logsResponse = {
@@ -196,6 +197,10 @@ export function ProxyViewerContainer(): JSX.Element {
196
197
  })();
197
198
  }, []);
198
199
 
200
+ // Read the strip config once at the container so the virtualized list does
201
+ // not need N independent SWR subscriptions per row.
202
+ const { strip } = useStripConfig();
203
+
199
204
  return (
200
205
  <>
201
206
  {error !== null && (
@@ -214,6 +219,7 @@ export function ProxyViewerContainer(): JSX.Element {
214
219
  onClearAll={handleClearAll}
215
220
  viewMode={viewMode}
216
221
  onViewModeChange={setViewMode}
222
+ strip={strip}
217
223
  />
218
224
  </>
219
225
  );
@@ -13,6 +13,8 @@ import { LogEntry } from "./LogEntry";
13
13
  export type ConversationGroupProps = {
14
14
  group: ConversationGroupData;
15
15
  viewMode?: "simple" | "full";
16
+ /** Live strip-Claude-Code-billing-header flag from the viewer container. */
17
+ strip: boolean;
16
18
  };
17
19
 
18
20
  function computeStats(logs: CapturedLog[]): {
@@ -31,6 +33,7 @@ function computeStats(logs: CapturedLog[]): {
31
33
  export const ConversationGroup = memo(function ({
32
34
  group,
33
35
  viewMode = "simple",
36
+ strip,
34
37
  }: ConversationGroupProps): JSX.Element {
35
38
  const [expanded, setExpanded] = useState(false);
36
39
 
@@ -64,7 +67,13 @@ export const ConversationGroup = memo(function ({
64
67
  {expanded && (
65
68
  <div className="pl-4 border-l-2 border-muted ml-3">
66
69
  {group.logs.map((log) => (
67
- <LogEntry key={log.id} log={log} viewMode={viewMode} suppressApiFormatBadge={!mixed} />
70
+ <LogEntry
71
+ key={log.id}
72
+ log={log}
73
+ viewMode={viewMode}
74
+ suppressApiFormatBadge={!mixed}
75
+ strip={strip}
76
+ />
68
77
  ))}
69
78
  </div>
70
79
  )}