@tonyclaw/llm-inspector 1.6.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 (286) hide show
  1. package/.output/nitro.json +17 -0
  2. package/.output/public/assets/alibaba-TTwafVwX.svg +1 -0
  3. package/.output/public/assets/index-B3RwBPLW.css +1 -0
  4. package/.output/public/assets/index-s4lwsWvq.js +97 -0
  5. package/.output/public/assets/main-Cp8AM0Pa.js +17 -0
  6. package/.output/public/assets/minimax-BPMzvuL-.jpeg +0 -0
  7. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  8. package/.output/public/assets/zhipuai-BPNAnxo-.svg +219 -0
  9. package/.output/server/_chunks/ssr-renderer.mjs +17 -0
  10. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +1 -0
  11. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +210 -0
  12. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +1 -0
  13. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +1 -0
  14. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +1 -0
  15. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +11 -0
  16. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +69 -0
  17. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +1 -0
  18. package/.output/server/_libs/@radix-ui/react-use-escape-keydown+[...].mjs +17 -0
  19. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +1 -0
  20. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +6 -0
  21. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +34 -0
  22. package/.output/server/_libs/ajv-formats.mjs +330 -0
  23. package/.output/server/_libs/ajv.mjs +11444 -0
  24. package/.output/server/_libs/aria-hidden.mjs +122 -0
  25. package/.output/server/_libs/atomically.mjs +152 -0
  26. package/.output/server/_libs/bail.mjs +8 -0
  27. package/.output/server/_libs/character-entities.mjs +2130 -0
  28. package/.output/server/_libs/class-variance-authority.mjs +44 -0
  29. package/.output/server/_libs/clsx.mjs +16 -0
  30. package/.output/server/_libs/comma-separated-tokens.mjs +10 -0
  31. package/.output/server/_libs/conf.mjs +635 -0
  32. package/.output/server/_libs/cookie-es.mjs +58 -0
  33. package/.output/server/_libs/core-util-is.mjs +75 -0
  34. package/.output/server/_libs/croner.mjs +1 -0
  35. package/.output/server/_libs/crossws.mjs +1 -0
  36. package/.output/server/_libs/debounce-fn.mjs +69 -0
  37. package/.output/server/_libs/decode-named-character-reference+[...].mjs +8 -0
  38. package/.output/server/_libs/detect-node-es.mjs +1 -0
  39. package/.output/server/_libs/devlop.mjs +8 -0
  40. package/.output/server/_libs/dot-prop.mjs +265 -0
  41. package/.output/server/_libs/env-paths.mjs +57 -0
  42. package/.output/server/_libs/estree-util-is-identifier-name.mjs +11 -0
  43. package/.output/server/_libs/extend.mjs +97 -0
  44. package/.output/server/_libs/fast-deep-equal.mjs +38 -0
  45. package/.output/server/_libs/fast-uri.mjs +812 -0
  46. package/.output/server/_libs/floating-ui__core.mjs +725 -0
  47. package/.output/server/_libs/floating-ui__dom.mjs +622 -0
  48. package/.output/server/_libs/floating-ui__react-dom.mjs +292 -0
  49. package/.output/server/_libs/floating-ui__utils.mjs +320 -0
  50. package/.output/server/_libs/get-nonce.mjs +9 -0
  51. package/.output/server/_libs/h3-v2.mjs +276 -0
  52. package/.output/server/_libs/h3.mjs +400 -0
  53. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +388 -0
  54. package/.output/server/_libs/hast-util-whitespace.mjs +10 -0
  55. package/.output/server/_libs/hookable.mjs +1 -0
  56. package/.output/server/_libs/html-url-attributes.mjs +26 -0
  57. package/.output/server/_libs/immediate.mjs +74 -0
  58. package/.output/server/_libs/inherits.mjs +50 -0
  59. package/.output/server/_libs/inline-style-parser.mjs +142 -0
  60. package/.output/server/_libs/is-plain-obj.mjs +10 -0
  61. package/.output/server/_libs/isarray.mjs +14 -0
  62. package/.output/server/_libs/isbot.mjs +20 -0
  63. package/.output/server/_libs/json-schema-traverse.mjs +180 -0
  64. package/.output/server/_libs/jszip.mjs +3049 -0
  65. package/.output/server/_libs/lie.mjs +273 -0
  66. package/.output/server/_libs/lucide-react.mjs +368 -0
  67. package/.output/server/_libs/mdast-util-from-markdown.mjs +717 -0
  68. package/.output/server/_libs/mdast-util-to-hast.mjs +710 -0
  69. package/.output/server/_libs/mdast-util-to-string.mjs +38 -0
  70. package/.output/server/_libs/micromark-core-commonmark.mjs +2259 -0
  71. package/.output/server/_libs/micromark-factory-destination.mjs +94 -0
  72. package/.output/server/_libs/micromark-factory-label.mjs +63 -0
  73. package/.output/server/_libs/micromark-factory-space.mjs +24 -0
  74. package/.output/server/_libs/micromark-factory-title.mjs +65 -0
  75. package/.output/server/_libs/micromark-factory-whitespace.mjs +22 -0
  76. package/.output/server/_libs/micromark-util-character.mjs +44 -0
  77. package/.output/server/_libs/micromark-util-chunked.mjs +36 -0
  78. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +12 -0
  79. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +41 -0
  80. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +19 -0
  81. package/.output/server/_libs/micromark-util-decode-string.mjs +21 -0
  82. package/.output/server/_libs/micromark-util-encode.mjs +1 -0
  83. package/.output/server/_libs/micromark-util-html-tag-name.mjs +69 -0
  84. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +6 -0
  85. package/.output/server/_libs/micromark-util-resolve-all.mjs +15 -0
  86. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +41 -0
  87. package/.output/server/_libs/micromark-util-subtokenize.mjs +346 -0
  88. package/.output/server/_libs/micromark.mjs +906 -0
  89. package/.output/server/_libs/mimic-function.mjs +47 -0
  90. package/.output/server/_libs/ohash.mjs +1 -0
  91. package/.output/server/_libs/pako.mjs +4223 -0
  92. package/.output/server/_libs/process-nextick-args.mjs +48 -0
  93. package/.output/server/_libs/property-information.mjs +1209 -0
  94. package/.output/server/_libs/radix-ui.mjs +1 -0
  95. package/.output/server/_libs/radix-ui__number.mjs +6 -0
  96. package/.output/server/_libs/radix-ui__primitive.mjs +11 -0
  97. package/.output/server/_libs/radix-ui__react-accordion.mjs +1 -0
  98. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +1 -0
  99. package/.output/server/_libs/radix-ui__react-arrow.mjs +23 -0
  100. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +1 -0
  101. package/.output/server/_libs/radix-ui__react-avatar.mjs +1 -0
  102. package/.output/server/_libs/radix-ui__react-checkbox.mjs +1 -0
  103. package/.output/server/_libs/radix-ui__react-collapsible.mjs +144 -0
  104. package/.output/server/_libs/radix-ui__react-collection.mjs +69 -0
  105. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +39 -0
  106. package/.output/server/_libs/radix-ui__react-context-menu.mjs +1 -0
  107. package/.output/server/_libs/radix-ui__react-context.mjs +78 -0
  108. package/.output/server/_libs/radix-ui__react-dialog.mjs +325 -0
  109. package/.output/server/_libs/radix-ui__react-direction.mjs +9 -0
  110. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +1 -0
  111. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +29 -0
  112. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +206 -0
  113. package/.output/server/_libs/radix-ui__react-form.mjs +1 -0
  114. package/.output/server/_libs/radix-ui__react-hover-card.mjs +1 -0
  115. package/.output/server/_libs/radix-ui__react-id.mjs +14 -0
  116. package/.output/server/_libs/radix-ui__react-label.mjs +1 -0
  117. package/.output/server/_libs/radix-ui__react-menu.mjs +1 -0
  118. package/.output/server/_libs/radix-ui__react-menubar.mjs +1 -0
  119. package/.output/server/_libs/radix-ui__react-popover.mjs +1 -0
  120. package/.output/server/_libs/radix-ui__react-popper.mjs +286 -0
  121. package/.output/server/_libs/radix-ui__react-portal.mjs +16 -0
  122. package/.output/server/_libs/radix-ui__react-presence.mjs +128 -0
  123. package/.output/server/_libs/radix-ui__react-primitive.mjs +42 -0
  124. package/.output/server/_libs/radix-ui__react-progress.mjs +1 -0
  125. package/.output/server/_libs/radix-ui__react-radio-group.mjs +1 -0
  126. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +224 -0
  127. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +721 -0
  128. package/.output/server/_libs/radix-ui__react-select.mjs +1163 -0
  129. package/.output/server/_libs/radix-ui__react-separator.mjs +28 -0
  130. package/.output/server/_libs/radix-ui__react-slider.mjs +1 -0
  131. package/.output/server/_libs/radix-ui__react-slot.mjs +99 -0
  132. package/.output/server/_libs/radix-ui__react-switch.mjs +1 -0
  133. package/.output/server/_libs/radix-ui__react-tabs.mjs +189 -0
  134. package/.output/server/_libs/radix-ui__react-toast.mjs +1 -0
  135. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +1 -0
  136. package/.output/server/_libs/radix-ui__react-toggle.mjs +1 -0
  137. package/.output/server/_libs/radix-ui__react-toolbar.mjs +1 -0
  138. package/.output/server/_libs/radix-ui__react-tooltip.mjs +495 -0
  139. package/.output/server/_libs/radix-ui__react-use-previous.mjs +14 -0
  140. package/.output/server/_libs/radix-ui__react-use-size.mjs +39 -0
  141. package/.output/server/_libs/react-dom.mjs +9935 -0
  142. package/.output/server/_libs/react-markdown.mjs +147 -0
  143. package/.output/server/_libs/react-remove-scroll-bar.mjs +82 -0
  144. package/.output/server/_libs/react-remove-scroll.mjs +328 -0
  145. package/.output/server/_libs/react-style-singleton.mjs +69 -0
  146. package/.output/server/_libs/react.mjs +515 -0
  147. package/.output/server/_libs/readable-stream.mjs +1518 -0
  148. package/.output/server/_libs/remark-parse.mjs +19 -0
  149. package/.output/server/_libs/remark-rehype.mjs +21 -0
  150. package/.output/server/_libs/rou3.mjs +8 -0
  151. package/.output/server/_libs/safe-buffer.mjs +64 -0
  152. package/.output/server/_libs/semver.mjs +1984 -0
  153. package/.output/server/_libs/seroval-plugins.mjs +58 -0
  154. package/.output/server/_libs/seroval.mjs +1765 -0
  155. package/.output/server/_libs/setimmediate.mjs +1 -0
  156. package/.output/server/_libs/space-separated-tokens.mjs +6 -0
  157. package/.output/server/_libs/srvx.mjs +334 -0
  158. package/.output/server/_libs/stubborn-fs.mjs +91 -0
  159. package/.output/server/_libs/stubborn-utils.mjs +66 -0
  160. package/.output/server/_libs/style-to-js.mjs +72 -0
  161. package/.output/server/_libs/style-to-object.mjs +38 -0
  162. package/.output/server/_libs/tailwind-merge.mjs +3010 -0
  163. package/.output/server/_libs/tanstack__history.mjs +217 -0
  164. package/.output/server/_libs/tanstack__react-router.mjs +1480 -0
  165. package/.output/server/_libs/tanstack__react-store.mjs +1 -0
  166. package/.output/server/_libs/tanstack__react-virtual.mjs +44 -0
  167. package/.output/server/_libs/tanstack__router-core.mjs +4827 -0
  168. package/.output/server/_libs/tanstack__store.mjs +1 -0
  169. package/.output/server/_libs/tanstack__virtual-core.mjs +1225 -0
  170. package/.output/server/_libs/tiny-invariant.mjs +12 -0
  171. package/.output/server/_libs/tiny-warning.mjs +5 -0
  172. package/.output/server/_libs/trim-lines.mjs +41 -0
  173. package/.output/server/_libs/trough.mjs +85 -0
  174. package/.output/server/_libs/tslib.mjs +576 -0
  175. package/.output/server/_libs/ufo.mjs +54 -0
  176. package/.output/server/_libs/uint8array-extras.mjs +69 -0
  177. package/.output/server/_libs/unctx.mjs +1 -0
  178. package/.output/server/_libs/ungap__structured-clone.mjs +212 -0
  179. package/.output/server/_libs/unified.mjs +661 -0
  180. package/.output/server/_libs/unist-util-is.mjs +100 -0
  181. package/.output/server/_libs/unist-util-position.mjs +27 -0
  182. package/.output/server/_libs/unist-util-stringify-position.mjs +27 -0
  183. package/.output/server/_libs/unist-util-visit-parents.mjs +82 -0
  184. package/.output/server/_libs/unist-util-visit.mjs +24 -0
  185. package/.output/server/_libs/unstorage.mjs +1 -0
  186. package/.output/server/_libs/use-callback-ref.mjs +66 -0
  187. package/.output/server/_libs/use-sidecar.mjs +106 -0
  188. package/.output/server/_libs/use-sync-external-store.mjs +1 -0
  189. package/.output/server/_libs/util-deprecate.mjs +12 -0
  190. package/.output/server/_libs/vfile-message.mjs +138 -0
  191. package/.output/server/_libs/vfile.mjs +467 -0
  192. package/.output/server/_libs/when-exit.mjs +53 -0
  193. package/.output/server/_libs/zod.mjs +4460 -0
  194. package/.output/server/_ssr/index-ByCLZu7J.mjs +3061 -0
  195. package/.output/server/_ssr/index.mjs +1176 -0
  196. package/.output/server/_ssr/router-Bq_mxeNz.mjs +2872 -0
  197. package/.output/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  198. package/.output/server/_tanstack-start-manifest_v-C4E0e9my.mjs +4 -0
  199. package/.output/server/index.mjs +393 -0
  200. package/README.md +196 -0
  201. package/package.json +91 -0
  202. package/src/assets/logos/alibaba.svg +1 -0
  203. package/src/assets/logos/anthropic.svg +1 -0
  204. package/src/assets/logos/deepseek.svg +1 -0
  205. package/src/assets/logos/minimax.jpeg +0 -0
  206. package/src/assets/logos/openai.svg +1 -0
  207. package/src/assets/logos/qwen.png +0 -0
  208. package/src/assets/logos/zhipuai.svg +219 -0
  209. package/src/cli.ts +68 -0
  210. package/src/components/ProxyViewer.tsx +325 -0
  211. package/src/components/ProxyViewerContainer.tsx +211 -0
  212. package/src/components/providers/ProviderCard.tsx +186 -0
  213. package/src/components/providers/ProviderForm.tsx +259 -0
  214. package/src/components/providers/ProviderLogo.tsx +111 -0
  215. package/src/components/providers/ProvidersPanel.tsx +259 -0
  216. package/src/components/providers/SettingsDialog.tsx +39 -0
  217. package/src/components/proxy-viewer/ConversationGroup.tsx +68 -0
  218. package/src/components/proxy-viewer/ConversationHeader.tsx +141 -0
  219. package/src/components/proxy-viewer/LogEntry.tsx +225 -0
  220. package/src/components/proxy-viewer/LogEntryHeader.tsx +250 -0
  221. package/src/components/proxy-viewer/ReplayDialog.tsx +208 -0
  222. package/src/components/proxy-viewer/ResponseView.tsx +161 -0
  223. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +171 -0
  224. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +139 -0
  225. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +64 -0
  226. package/src/components/proxy-viewer/formats/index.tsx +24 -0
  227. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +80 -0
  228. package/src/components/proxy-viewer/index.ts +8 -0
  229. package/src/components/ui/badge.tsx +47 -0
  230. package/src/components/ui/button.tsx +47 -0
  231. package/src/components/ui/collapsible.tsx +21 -0
  232. package/src/components/ui/dialog.tsx +129 -0
  233. package/src/components/ui/json-viewer.tsx +464 -0
  234. package/src/components/ui/scroll-area.tsx +54 -0
  235. package/src/components/ui/select.tsx +178 -0
  236. package/src/components/ui/separator.tsx +28 -0
  237. package/src/components/ui/tabs.tsx +88 -0
  238. package/src/components/ui/tooltip.tsx +51 -0
  239. package/src/index.css +11 -0
  240. package/src/lib/export-logs.ts +51 -0
  241. package/src/lib/utils.ts +22 -0
  242. package/src/proxy/chunkStorage.ts +118 -0
  243. package/src/proxy/constants.ts +36 -0
  244. package/src/proxy/formats/anthropic/anthropicProvider.ts +75 -0
  245. package/src/proxy/formats/anthropic/handler.ts +74 -0
  246. package/src/proxy/formats/anthropic/index.ts +14 -0
  247. package/src/proxy/formats/anthropic/register.ts +4 -0
  248. package/src/proxy/formats/anthropic/schemas.ts +217 -0
  249. package/src/proxy/formats/anthropic/stream.ts +167 -0
  250. package/src/proxy/formats/handler.ts +46 -0
  251. package/src/proxy/formats/index.ts +12 -0
  252. package/src/proxy/formats/jsonSchema.ts +24 -0
  253. package/src/proxy/formats/openai/alibabaProvider.ts +38 -0
  254. package/src/proxy/formats/openai/handler.ts +70 -0
  255. package/src/proxy/formats/openai/index.ts +25 -0
  256. package/src/proxy/formats/openai/provider.ts +50 -0
  257. package/src/proxy/formats/openai/register.ts +4 -0
  258. package/src/proxy/formats/openai/schemas.ts +150 -0
  259. package/src/proxy/formats/openai/stream.ts +153 -0
  260. package/src/proxy/formats/protocol.ts +50 -0
  261. package/src/proxy/formats/providerRegistry.ts +51 -0
  262. package/src/proxy/formats/providers/index.ts +3 -0
  263. package/src/proxy/formats/registry.ts +61 -0
  264. package/src/proxy/handler.ts +389 -0
  265. package/src/proxy/logIndex.ts +187 -0
  266. package/src/proxy/logger.ts +99 -0
  267. package/src/proxy/providers.ts +234 -0
  268. package/src/proxy/schemas.ts +160 -0
  269. package/src/proxy/socketTracker.ts +158 -0
  270. package/src/proxy/store.ts +386 -0
  271. package/src/router.tsx +16 -0
  272. package/src/routes/__root.tsx +38 -0
  273. package/src/routes/api/config.paths.ts +14 -0
  274. package/src/routes/api/health.ts +11 -0
  275. package/src/routes/api/logs.$id.chunks.ts +36 -0
  276. package/src/routes/api/logs.$id.replay.ts +262 -0
  277. package/src/routes/api/logs.$id.ts +22 -0
  278. package/src/routes/api/logs.stream.ts +64 -0
  279. package/src/routes/api/logs.ts +30 -0
  280. package/src/routes/api/models.ts +10 -0
  281. package/src/routes/api/providers.$providerId.ts +45 -0
  282. package/src/routes/api/providers.ts +37 -0
  283. package/src/routes/api/sessions.ts +10 -0
  284. package/src/routes/index.tsx +6 -0
  285. package/src/routes/proxy/$.ts +15 -0
  286. package/styles/globals.css +121 -0
@@ -0,0 +1,225 @@
1
+ import { Check, Copy, RotateCcw } from "lucide-react";
2
+ import type { JSX } from "react";
3
+ import { useMemo, useState } from "react";
4
+ import { cn } from "../../lib/utils";
5
+ import { type CapturedLog, parseRequest } from "../../proxy/schemas";
6
+ import { Button } from "../ui/button";
7
+ import { JsonViewerFromString } from "../ui/json-viewer";
8
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
9
+ import { LogEntryHeader } from "./LogEntryHeader";
10
+ import { ReplayDialog } from "./ReplayDialog";
11
+ import { ResponseView } from "./ResponseView";
12
+ import { StreamingChunkSequence } from "./StreamingChunkSequence";
13
+
14
+ export type LogEntryProps = {
15
+ log: CapturedLog;
16
+ viewMode?: "simple" | "full";
17
+ };
18
+
19
+ function CopyButton({
20
+ text,
21
+ label,
22
+ copied,
23
+ onCopy,
24
+ }: {
25
+ text: string | null;
26
+ label: string;
27
+ copied: boolean;
28
+ onCopy: (e: React.MouseEvent) => void;
29
+ }): JSX.Element | null {
30
+ if (text === null) return null;
31
+ return (
32
+ <button
33
+ type="button"
34
+ onClick={onCopy}
35
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded hover:bg-muted"
36
+ >
37
+ {copied ? (
38
+ <>
39
+ <Check className="size-3 text-green-500" />
40
+ <span className="text-green-500">Copied!</span>
41
+ </>
42
+ ) : (
43
+ <>
44
+ <Copy className="size-3" />
45
+ <span>{label}</span>
46
+ </>
47
+ )}
48
+ </button>
49
+ );
50
+ }
51
+
52
+ export function LogEntry({ log, viewMode = "simple" }: LogEntryProps): JSX.Element {
53
+ const [expanded, setExpanded] = useState<boolean>(false);
54
+ const [requestCopied, setRequestCopied] = useState<boolean>(false);
55
+ const [responseCopied, setResponseCopied] = useState<boolean>(false);
56
+ const [replayOpen, setReplayOpen] = useState<boolean>(false);
57
+ const parsedRequest = useMemo(() => parseRequest(log.rawRequestBody), [log.rawRequestBody]);
58
+
59
+ function handleCopyRequest(e: React.MouseEvent): void {
60
+ e.stopPropagation();
61
+ if (log.rawRequestBody === null) return;
62
+ void window.navigator.clipboard.writeText(log.rawRequestBody).then(() => {
63
+ setRequestCopied(true);
64
+ setTimeout(() => setRequestCopied(false), 2000);
65
+ });
66
+ }
67
+
68
+ function handleCopyResponse(e: React.MouseEvent): void {
69
+ e.stopPropagation();
70
+ if (log.responseText === null) return;
71
+ void window.navigator.clipboard.writeText(log.responseText).then(() => {
72
+ setResponseCopied(true);
73
+ setTimeout(() => setResponseCopied(false), 2000);
74
+ });
75
+ }
76
+
77
+ return (
78
+ <>
79
+ <div className={cn("border border-border rounded-lg mb-3 overflow-hidden")}>
80
+ <LogEntryHeader
81
+ log={log}
82
+ parsedRequest={parsedRequest}
83
+ expanded={expanded}
84
+ onToggle={() => setExpanded(!expanded)}
85
+ />
86
+
87
+ {expanded && (
88
+ <div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
89
+ <Tabs defaultValue="request">
90
+ <TabsList className="mx-4 mt-2">
91
+ {viewMode === "full" && <TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>}
92
+ {viewMode === "full" && <TabsTrigger value="headers">Headers</TabsTrigger>}
93
+ <TabsTrigger value="request">Request</TabsTrigger>
94
+ {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
95
+ <TabsTrigger value="parsed">Parsed Response</TabsTrigger>
96
+ </TabsList>
97
+
98
+ <TabsContent value="request">
99
+ <div className="px-4 py-3">
100
+ <div className="flex justify-end gap-2 mb-2">
101
+ <Button
102
+ variant="outline"
103
+ size="sm"
104
+ className="h-7 text-xs"
105
+ onClick={(e) => {
106
+ e.stopPropagation();
107
+ setReplayOpen(true);
108
+ }}
109
+ >
110
+ <RotateCcw className="size-3 mr-1" />
111
+ Replay
112
+ </Button>
113
+ <CopyButton
114
+ text={log.rawRequestBody}
115
+ label="Copy Request"
116
+ copied={requestCopied}
117
+ onCopy={handleCopyRequest}
118
+ />
119
+ </div>
120
+ {log.rawRequestBody !== null ? (
121
+ <JsonViewerFromString text={log.rawRequestBody} defaultExpandDepth={1} />
122
+ ) : (
123
+ <p className="text-xs text-muted-foreground italic">No request body</p>
124
+ )}
125
+ </div>
126
+ </TabsContent>
127
+
128
+ {viewMode === "full" && (
129
+ <TabsContent value="headers">
130
+ <div className="px-4 py-3">
131
+ {log.headers && Object.keys(log.headers).length > 0 ? (
132
+ <div className="space-y-1 font-mono text-xs">
133
+ {Object.entries(log.headers)
134
+ .sort(([a], [b]) => a.localeCompare(b))
135
+ .map(([key, value]) => (
136
+ <div key={key} className="flex gap-2">
137
+ <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
138
+ {key}:
139
+ </span>
140
+ <span className="text-muted-foreground truncate" title={value}>
141
+ {value}
142
+ </span>
143
+ </div>
144
+ ))}
145
+ </div>
146
+ ) : (
147
+ <p className="text-xs text-muted-foreground italic">No headers captured</p>
148
+ )}
149
+ </div>
150
+ </TabsContent>
151
+ )}
152
+
153
+ {viewMode === "full" && (
154
+ <TabsContent value="raw-headers">
155
+ <div className="px-4 py-3">
156
+ {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
157
+ <div className="space-y-1 font-mono text-xs">
158
+ {Object.entries(log.rawHeaders)
159
+ .sort(([a], [b]) => a.localeCompare(b))
160
+ .map(([key, value]) => (
161
+ <div key={key} className="flex gap-2">
162
+ <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
163
+ {key}:
164
+ </span>
165
+ <span className="text-muted-foreground truncate" title={value}>
166
+ {value}
167
+ </span>
168
+ </div>
169
+ ))}
170
+ </div>
171
+ ) : (
172
+ <p className="text-xs text-muted-foreground italic">
173
+ No raw headers captured
174
+ </p>
175
+ )}
176
+ </div>
177
+ </TabsContent>
178
+ )}
179
+
180
+ <TabsContent value="raw">
181
+ <div className="px-4 py-3 space-y-3">
182
+ <div className="flex justify-end">
183
+ <CopyButton
184
+ text={log.responseText}
185
+ label="Copy Response"
186
+ copied={responseCopied}
187
+ onCopy={handleCopyResponse}
188
+ />
189
+ </div>
190
+ {log.responseText !== null ? (
191
+ <JsonViewerFromString text={log.responseText} defaultExpandDepth={1} />
192
+ ) : (
193
+ <p className="text-xs text-muted-foreground italic">No response</p>
194
+ )}
195
+ {log.streaming === true && (
196
+ <StreamingChunkSequence
197
+ logId={log.id}
198
+ truncated={log.streamingChunksPath !== null}
199
+ />
200
+ )}
201
+ </div>
202
+ </TabsContent>
203
+
204
+ <TabsContent value="parsed">
205
+ <div className="px-4 py-3">
206
+ <ResponseView
207
+ responseText={log.responseText}
208
+ responseStatus={log.responseStatus}
209
+ streaming={log.streaming}
210
+ inputTokens={log.inputTokens}
211
+ outputTokens={log.outputTokens}
212
+ cacheCreationInputTokens={log.cacheCreationInputTokens}
213
+ cacheReadInputTokens={log.cacheReadInputTokens}
214
+ apiFormat={log.apiFormat}
215
+ />
216
+ </div>
217
+ </TabsContent>
218
+ </Tabs>
219
+ </div>
220
+ )}
221
+ </div>
222
+ <ReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
223
+ </>
224
+ );
225
+ }
@@ -0,0 +1,250 @@
1
+ import {
2
+ ChevronDown,
3
+ ChevronRight,
4
+ Clock,
5
+ FileTerminal,
6
+ Globe,
7
+ Loader2,
8
+ MessageSquare,
9
+ Radio,
10
+ User,
11
+ Wrench,
12
+ Zap,
13
+ } from "lucide-react";
14
+ import type { JSX } from "react";
15
+ import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
16
+ import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
17
+ import { Badge } from "../ui/badge";
18
+ import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
19
+
20
+ const API_FORMAT_LABELS: Record<"anthropic" | "openai" | "unknown", string> = {
21
+ anthropic: "Anthropic",
22
+ openai: "OpenAI",
23
+ unknown: "Unknown",
24
+ };
25
+
26
+ function formatElapsed(ms: number): string {
27
+ if (ms < 1000) return `${ms}ms`;
28
+ return `${(ms / 1000).toFixed(1)}s`;
29
+ }
30
+
31
+ const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
32
+ success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25",
33
+ client_error: "bg-amber-500/15 text-amber-400 border-amber-500/25",
34
+ server_error: "",
35
+ pending: "bg-muted text-muted-foreground border-border",
36
+ };
37
+
38
+ export type LogEntryHeaderProps = {
39
+ log: CapturedLog;
40
+ parsedRequest: InspectorRequest | null;
41
+ expanded: boolean;
42
+ onToggle: () => void;
43
+ };
44
+
45
+ export function LogEntryHeader({
46
+ log,
47
+ parsedRequest,
48
+ expanded,
49
+ onToggle,
50
+ }: LogEntryHeaderProps): JSX.Element {
51
+ const statusCategory = getStatusCategory(log.responseStatus);
52
+
53
+ const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
54
+
55
+ const messageCount = parsedRequest !== null ? parsedRequest.messages.length : null;
56
+
57
+ const toolCount =
58
+ parsedRequest !== null && parsedRequest.tools !== undefined && parsedRequest.tools.length > 0
59
+ ? parsedRequest.tools.length
60
+ : null;
61
+
62
+ return (
63
+ <div
64
+ role="button"
65
+ tabIndex={0}
66
+ className={cn(
67
+ "flex items-center gap-2.5 px-3 py-2 cursor-pointer transition-colors",
68
+ "hover:bg-muted/50",
69
+ "select-none",
70
+ )}
71
+ onClick={onToggle}
72
+ onKeyDown={(e) => {
73
+ if (e.key === "Enter" || e.key === " ") {
74
+ e.preventDefault();
75
+ onToggle();
76
+ }
77
+ }}
78
+ >
79
+ {/* Request ID */}
80
+ <span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
81
+ #{log.id}
82
+ </span>
83
+
84
+ {/* Model */}
85
+ {log.model !== null && (
86
+ <>
87
+ <ProviderLogo provider={detectProvider(log.model)} className="size-4 shrink-0" />
88
+ <Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
89
+ {log.model}
90
+ </Badge>
91
+ </>
92
+ )}
93
+
94
+ {/* API Format Badge */}
95
+ <Badge
96
+ variant="outline"
97
+ className={cn(
98
+ "text-[10px] px-1.5 py-0 h-5 font-mono",
99
+ log.apiFormat === "openai" && "border-blue-500/40 text-blue-400",
100
+ log.apiFormat === "anthropic" && "border-orange-500/40 text-orange-400",
101
+ log.apiFormat === "unknown" && "border-muted text-muted-foreground",
102
+ )}
103
+ >
104
+ {API_FORMAT_LABELS[log.apiFormat] ?? log.apiFormat}
105
+ </Badge>
106
+
107
+ {/* Response Status */}
108
+ {statusCategory === "server_error" ? (
109
+ <Badge variant="destructive" className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums">
110
+ {log.responseStatus}
111
+ </Badge>
112
+ ) : statusCategory === "pending" ? (
113
+ <Badge
114
+ variant="outline"
115
+ className={cn(
116
+ "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
117
+ STATUS_BADGE_CLASSES[statusCategory],
118
+ )}
119
+ >
120
+ <Loader2 className="size-3 animate-spin" />
121
+ </Badge>
122
+ ) : (
123
+ <Badge
124
+ variant="outline"
125
+ className={cn(
126
+ "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
127
+ STATUS_BADGE_CLASSES[statusCategory],
128
+ )}
129
+ >
130
+ {log.responseStatus}
131
+ </Badge>
132
+ )}
133
+
134
+ {/* Elapsed time */}
135
+ {log.elapsedMs !== null && (
136
+ <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
137
+ <Clock className="size-3" />
138
+ <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
139
+ </span>
140
+ )}
141
+
142
+ {/* Token counts */}
143
+ {hasTokens && (
144
+ <span className="flex items-center gap-1 text-xs shrink-0">
145
+ <Zap className="size-3 text-muted-foreground" />
146
+ <span className="font-mono tabular-nums">
147
+ <span className={log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground"}>
148
+ IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "—"}
149
+ </span>
150
+ {" / "}
151
+ <span
152
+ className={log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground"}
153
+ >
154
+ OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "—"}
155
+ </span>
156
+ </span>
157
+ </span>
158
+ )}
159
+ {/* Cache tokens */}
160
+ {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
161
+ <span className="flex items-center gap-1 text-xs shrink-0">
162
+ <span className="font-mono tabular-nums text-emerald-400">
163
+ Cache +{formatTokens(log.cacheCreationInputTokens)}
164
+ </span>
165
+ </span>
166
+ )}
167
+ {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
168
+ <span className="flex items-center gap-1 text-xs shrink-0">
169
+ <span className="font-mono tabular-nums text-purple-400">
170
+ Cache ~{formatTokens(log.cacheReadInputTokens)}
171
+ </span>
172
+ </span>
173
+ )}
174
+
175
+ {/* Message count */}
176
+ {messageCount !== null && (
177
+ <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
178
+ <MessageSquare className="size-3" />
179
+ <span className="font-mono tabular-nums">{messageCount}</span>
180
+ </span>
181
+ )}
182
+
183
+ {/* Tool count */}
184
+ {toolCount !== null && (
185
+ <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
186
+ <Wrench className="size-3" />
187
+ <span className="font-mono tabular-nums">{toolCount}</span>
188
+ </span>
189
+ )}
190
+
191
+ {/* Origin */}
192
+ {log.origin !== null && (
193
+ <span
194
+ className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
195
+ title={`Origin: ${log.origin}`}
196
+ >
197
+ <Globe className="size-3" />
198
+ <span className="font-mono tabular-nums truncate max-w-[120px]" title={log.origin}>
199
+ {log.origin}
200
+ </span>
201
+ </span>
202
+ )}
203
+
204
+ {/* User-Agent */}
205
+ {log.userAgent !== null && (
206
+ <span
207
+ className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
208
+ title={`User-Agent: ${log.userAgent}`}
209
+ >
210
+ <User className="size-3" />
211
+ <span className="font-mono tabular-nums truncate max-w-[150px]" title={log.userAgent}>
212
+ {log.userAgent}
213
+ </span>
214
+ </span>
215
+ )}
216
+
217
+ {/* Client info (PID + project folder) */}
218
+ {(log.clientPid !== null || log.clientProjectFolder !== null) && (
219
+ <span
220
+ className="flex items-center gap-1 text-purple-400/80 text-xs shrink-0"
221
+ title={
222
+ log.clientCwd !== null
223
+ ? `PID: ${log.clientPid ?? "?"} | CWD: ${log.clientCwd}`
224
+ : `PID: ${log.clientPid ?? "?"}`
225
+ }
226
+ >
227
+ <FileTerminal className="size-3" />
228
+ {log.clientProjectFolder !== null ? (
229
+ <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
230
+ ) : (
231
+ <span className="font-mono tabular-nums">PID {log.clientPid}</span>
232
+ )}
233
+ </span>
234
+ )}
235
+
236
+ {/* Streaming indicator */}
237
+ {log.streaming && <Radio className="size-3 text-muted-foreground/60 shrink-0" />}
238
+
239
+ {/* Spacer */}
240
+ <span className="flex-1 min-w-0" />
241
+
242
+ {/* Expand chevron */}
243
+ {expanded ? (
244
+ <ChevronDown className="size-4 text-muted-foreground shrink-0" />
245
+ ) : (
246
+ <ChevronRight className="size-4 text-muted-foreground shrink-0" />
247
+ )}
248
+ </div>
249
+ );
250
+ }
@@ -0,0 +1,208 @@
1
+ import { RotateCcw } from "lucide-react";
2
+ import type { JSX } from "react";
3
+ import { useState } from "react";
4
+ import { z } from "zod";
5
+ import { Button } from "../ui/button";
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
8
+ import { ResponseView } from "./ResponseView";
9
+ import type { CapturedLog } from "../../proxy/schemas";
10
+
11
+ const ReplayResultSchema = z.object({
12
+ success: z.boolean(),
13
+ error: z.string().optional(),
14
+ responseStatus: z.number().optional(),
15
+ responseText: z.string().optional(),
16
+ inputTokens: z.number().optional(),
17
+ outputTokens: z.number().optional(),
18
+ elapsedMs: z.number().optional(),
19
+ streaming: z.boolean().optional(),
20
+ });
21
+
22
+ type ReplayResult = z.infer<typeof ReplayResultSchema>;
23
+
24
+ type ReplayDialogProps = {
25
+ log: CapturedLog;
26
+ open: boolean;
27
+ onOpenChange: (open: boolean) => void;
28
+ };
29
+
30
+ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JSX.Element {
31
+ const [modifiedBody, setModifiedBody] = useState<string>(() => {
32
+ return log.rawRequestBody ?? "{}";
33
+ });
34
+ const [replayResult, setReplayResult] = useState<ReplayResult | null>(null);
35
+ const [loading, setLoading] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ async function handleReplay() {
39
+ setLoading(true);
40
+ setError(null);
41
+ setReplayResult(null);
42
+
43
+ try {
44
+ const res = await fetch(`/api/logs/${log.id}/replay`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ modifiedBody }),
48
+ });
49
+
50
+ const json: unknown = await res.json();
51
+ const parsed = ReplayResultSchema.safeParse(json);
52
+ if (!parsed.success) {
53
+ setError("Invalid response from server");
54
+ setLoading(false);
55
+ return;
56
+ }
57
+ const data = parsed.data;
58
+ setReplayResult(data);
59
+ if (!data.success) {
60
+ setError(data.error ?? "Replay failed");
61
+ }
62
+ } catch (err) {
63
+ setError(err instanceof Error ? err.message : "Network error");
64
+ } finally {
65
+ setLoading(false);
66
+ }
67
+ }
68
+
69
+ function handleClose() {
70
+ setReplayResult(null);
71
+ setError(null);
72
+ onOpenChange(false);
73
+ }
74
+
75
+ return (
76
+ <Dialog open={open} onOpenChange={handleClose}>
77
+ <DialogContent className="max-w-4xl max-h-[85vh] overflow-auto">
78
+ <DialogHeader>
79
+ <DialogTitle className="flex items-center gap-2">
80
+ <RotateCcw className="size-4" />
81
+ Replay Request #{log.id}
82
+ </DialogTitle>
83
+ </DialogHeader>
84
+
85
+ <Tabs defaultValue="modified">
86
+ <TabsList>
87
+ <TabsTrigger value="modified">Modified Request</TabsTrigger>
88
+ <TabsTrigger value="original">Original Response</TabsTrigger>
89
+ {replayResult && <TabsTrigger value="replay">Replay Response</TabsTrigger>}
90
+ </TabsList>
91
+
92
+ <TabsContent value="modified" className="space-y-4">
93
+ <div>
94
+ <label className="text-sm font-medium mb-2 block">Request Body (JSON)</label>
95
+ <textarea
96
+ className="w-full h-64 p-3 font-mono text-xs bg-muted rounded-md border border-input resize-none focus:outline-none focus:ring-2 focus:ring-ring"
97
+ value={modifiedBody}
98
+ onChange={(e) => setModifiedBody(e.target.value)}
99
+ spellCheck={false}
100
+ />
101
+ </div>
102
+
103
+ {error !== null && error !== "" && (
104
+ <div className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-md">
105
+ {error}
106
+ </div>
107
+ )}
108
+
109
+ <div className="flex justify-end">
110
+ <Button
111
+ onClick={() => {
112
+ void handleReplay();
113
+ }}
114
+ disabled={loading}
115
+ >
116
+ {loading ? "Replaying..." : "Replay"}
117
+ </Button>
118
+ </div>
119
+
120
+ {replayResult && replayResult.success && (
121
+ <Tabs defaultValue="parsed">
122
+ <TabsList>
123
+ <TabsTrigger value="parsed">Parsed Response</TabsTrigger>
124
+ <TabsTrigger value="raw">Raw Response</TabsTrigger>
125
+ </TabsList>
126
+ <TabsContent value="parsed">
127
+ <ResponseView
128
+ responseText={replayResult.responseText ?? null}
129
+ responseStatus={replayResult.responseStatus ?? null}
130
+ streaming={replayResult.streaming ?? false}
131
+ inputTokens={replayResult.inputTokens ?? null}
132
+ outputTokens={replayResult.outputTokens ?? null}
133
+ apiFormat={log.apiFormat}
134
+ />
135
+ </TabsContent>
136
+ <TabsContent value="raw">
137
+ <pre className="font-mono text-xs whitespace-pre-wrap bg-muted p-3 rounded-md max-h-96 overflow-auto">
138
+ {replayResult.responseText ?? "No response"}
139
+ </pre>
140
+ </TabsContent>
141
+ </Tabs>
142
+ )}
143
+ </TabsContent>
144
+
145
+ <TabsContent value="original">
146
+ {log.responseText !== null ? (
147
+ <Tabs defaultValue="parsed">
148
+ <TabsList>
149
+ <TabsTrigger value="parsed">Parsed Response</TabsTrigger>
150
+ <TabsTrigger value="raw">Raw Response</TabsTrigger>
151
+ </TabsList>
152
+ <TabsContent value="parsed">
153
+ <ResponseView
154
+ responseText={log.responseText}
155
+ responseStatus={log.responseStatus}
156
+ streaming={log.streaming}
157
+ inputTokens={log.inputTokens}
158
+ outputTokens={log.outputTokens}
159
+ cacheCreationInputTokens={log.cacheCreationInputTokens}
160
+ cacheReadInputTokens={log.cacheReadInputTokens}
161
+ apiFormat={log.apiFormat}
162
+ />
163
+ </TabsContent>
164
+ <TabsContent value="raw">
165
+ <pre className="font-mono text-xs whitespace-pre-wrap bg-muted p-3 rounded-md max-h-96 overflow-auto">
166
+ {log.responseText}
167
+ </pre>
168
+ </TabsContent>
169
+ </Tabs>
170
+ ) : (
171
+ <p className="text-sm text-muted-foreground italic">No original response</p>
172
+ )}
173
+ </TabsContent>
174
+
175
+ {replayResult && replayResult.success && (
176
+ <TabsContent value="replay">
177
+ {replayResult.responseText !== null ? (
178
+ <Tabs defaultValue="parsed">
179
+ <TabsList>
180
+ <TabsTrigger value="parsed">Parsed Response</TabsTrigger>
181
+ <TabsTrigger value="raw">Raw Response</TabsTrigger>
182
+ </TabsList>
183
+ <TabsContent value="parsed">
184
+ <ResponseView
185
+ responseText={replayResult.responseText ?? null}
186
+ responseStatus={replayResult.responseStatus ?? null}
187
+ streaming={replayResult.streaming ?? false}
188
+ inputTokens={replayResult.inputTokens ?? null}
189
+ outputTokens={replayResult.outputTokens ?? null}
190
+ apiFormat={log.apiFormat}
191
+ />
192
+ </TabsContent>
193
+ <TabsContent value="raw">
194
+ <pre className="font-mono text-xs whitespace-pre-wrap bg-muted p-3 rounded-md max-h-96 overflow-auto">
195
+ {replayResult.responseText}
196
+ </pre>
197
+ </TabsContent>
198
+ </Tabs>
199
+ ) : (
200
+ <p className="text-sm text-muted-foreground italic">No replay response</p>
201
+ )}
202
+ </TabsContent>
203
+ )}
204
+ </Tabs>
205
+ </DialogContent>
206
+ </Dialog>
207
+ );
208
+ }