@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,386 @@
1
+ import { z } from "zod";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { createReadStream } from "node:fs";
4
+ import { createInterface } from "node:readline";
5
+ import { join } from "node:path";
6
+ import { appendLogEntry, resolveLogDir } from "./logger";
7
+ import {
8
+ addToIndex,
9
+ findInIndex,
10
+ getNextLogId,
11
+ getCurrentLogFile,
12
+ saveIndex,
13
+ loadIndex,
14
+ } from "./logIndex";
15
+ import { writeChunks } from "./chunkStorage";
16
+ import type { CapturedLog } from "./schemas";
17
+ import { CapturedLogSchema } from "./schemas";
18
+
19
+ export type { CapturedLog };
20
+
21
+ const LooseRequestSchema = z.object({
22
+ model: z.string().optional(),
23
+ metadata: z.object({ user_id: z.string().optional() }).passthrough().optional(),
24
+ });
25
+
26
+ const MAX_MEMORY_CACHE = 100;
27
+
28
+ // Memory cache: id -> CapturedLog (recent logs only)
29
+ const memoryCache: Map<number, CapturedLog> = new Map();
30
+
31
+ // O(1) LRU ordering using Map's insertion order
32
+ // head -> ... -> tail (head = oldest, tail = newest)
33
+ let headId: number | null = null;
34
+ let tailId: number | null = null;
35
+ const prevMap: Map<number, number | null> = new Map(); // id -> previous id (null if head)
36
+ const nextMap: Map<number, number | null> = new Map(); // id -> next id (null if tail)
37
+
38
+ function evictOldestIfNeeded(): void {
39
+ while (memoryCache.size >= MAX_MEMORY_CACHE && headId !== null) {
40
+ const oldest = headId;
41
+ const next = nextMap.get(oldest) ?? null;
42
+
43
+ memoryCache.delete(oldest);
44
+ prevMap.delete(oldest);
45
+ nextMap.delete(oldest);
46
+
47
+ if (next !== null) {
48
+ prevMap.set(next, null);
49
+ headId = next;
50
+ } else {
51
+ headId = null;
52
+ tailId = null;
53
+ }
54
+ }
55
+ }
56
+
57
+ function addToCache(log: CapturedLog): void {
58
+ evictOldestIfNeeded();
59
+
60
+ // Add at tail (newest position)
61
+ if (tailId !== null) {
62
+ nextMap.set(tailId, log.id);
63
+ prevMap.set(log.id, tailId);
64
+ } else {
65
+ // First element
66
+ headId = log.id;
67
+ prevMap.set(log.id, null);
68
+ }
69
+ nextMap.set(log.id, null);
70
+ tailId = log.id;
71
+
72
+ memoryCache.set(log.id, log);
73
+ }
74
+
75
+ function removeFromCache(id: number): void {
76
+ const prev = prevMap.get(id) ?? null;
77
+ const next = nextMap.get(id) ?? null;
78
+
79
+ if (prev !== null) {
80
+ nextMap.set(prev, next);
81
+ } else {
82
+ headId = next;
83
+ }
84
+
85
+ if (next !== null) {
86
+ prevMap.set(next, prev);
87
+ } else {
88
+ tailId = prev;
89
+ }
90
+
91
+ prevMap.delete(id);
92
+ nextMap.delete(id);
93
+ memoryCache.delete(id);
94
+ }
95
+
96
+ /**
97
+ * Add a test log entry directly to the in-memory store (for dashboard display).
98
+ * This is used by the provider test endpoint alongside appendLogEntry (file logging).
99
+ */
100
+ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
101
+ const id = await getNextLogId();
102
+ // Update the index with the new maxId so subsequent calls get unique IDs
103
+ const index = await loadIndex();
104
+ if (id > index.maxId) {
105
+ index.maxId = id;
106
+ await saveIndex(index);
107
+ }
108
+
109
+ // Persist streaming chunks to disk if present
110
+ let streamingChunksPath: string | null = null;
111
+ if (entry.streamingChunks !== undefined && entry.streamingChunks.chunks.length > 0) {
112
+ streamingChunksPath = writeChunks(
113
+ id,
114
+ entry.streamingChunks.chunks,
115
+ entry.streamingChunks.truncated,
116
+ );
117
+ }
118
+
119
+ const log: CapturedLog = {
120
+ id,
121
+ ...entry,
122
+ streamingChunksPath,
123
+ };
124
+ addToCache(log);
125
+ emitLogUpdate(log);
126
+ return log;
127
+ }
128
+
129
+ type ClientInfo = {
130
+ port: number | null;
131
+ pid: number | null;
132
+ cwd: string | null;
133
+ projectFolder: string | null;
134
+ };
135
+
136
+ export async function createLog(
137
+ method: string,
138
+ path: string,
139
+ requestBody: string | null,
140
+ headers: Headers,
141
+ clientInfo?: ClientInfo,
142
+ rawHeaders?: Record<string, string>,
143
+ upstreamHeaders?: Record<string, string>,
144
+ apiFormat: "anthropic" | "openai" | "unknown" = "unknown",
145
+ ): Promise<CapturedLog> {
146
+ let model: string | null = null;
147
+ let sessionId: string | null = null;
148
+
149
+ if (requestBody !== null) {
150
+ try {
151
+ const json: unknown = JSON.parse(requestBody);
152
+ const loose = LooseRequestSchema.safeParse(json);
153
+ if (loose.success) {
154
+ model = loose.data.model ?? null;
155
+ sessionId = loose.data.metadata?.user_id ?? headers.get("x-session-affinity") ?? null;
156
+ }
157
+ } catch {
158
+ // request body not valid JSON, skip parsing
159
+ }
160
+ }
161
+
162
+ const userAgent = headers.get("user-agent");
163
+ const origin = headers.get("origin");
164
+
165
+ const id = await getNextLogId();
166
+ const log: CapturedLog = {
167
+ id,
168
+ timestamp: new Date().toISOString(),
169
+ method,
170
+ path,
171
+ model,
172
+ sessionId,
173
+ rawRequestBody: requestBody,
174
+ responseStatus: null,
175
+ responseText: null,
176
+ inputTokens: null,
177
+ outputTokens: null,
178
+ cacheCreationInputTokens: null,
179
+ cacheReadInputTokens: null,
180
+ elapsedMs: null,
181
+ streaming: false,
182
+ userAgent,
183
+ origin,
184
+ rawHeaders: rawHeaders,
185
+ headers: upstreamHeaders,
186
+ apiFormat,
187
+ isTest: false,
188
+ providerName: null,
189
+ clientPort: clientInfo?.port ?? null,
190
+ clientPid: clientInfo?.pid ?? null,
191
+ clientCwd: clientInfo?.cwd ?? null,
192
+ clientProjectFolder: clientInfo?.projectFolder ?? null,
193
+ streamingChunksPath: null,
194
+ };
195
+
196
+ // Write to disk and update index
197
+ const logFile = getCurrentLogFile();
198
+ appendLogEntry(log);
199
+ await addToIndex(id, logFile, -1, -1); // line numbers not tracked precisely
200
+
201
+ // Add to memory cache
202
+ addToCache(log);
203
+ emitLogUpdate(log);
204
+
205
+ return log;
206
+ }
207
+
208
+ /**
209
+ * Get a log by ID, checking memory cache first then disk.
210
+ * Each request has two entries in the log file: initial (incomplete) and complete (with response).
211
+ * We scan for the complete entry (responseStatus !== null) first, falling back to the last match.
212
+ */
213
+ export async function getLogById(id: number): Promise<CapturedLog | null> {
214
+ // Check memory cache first
215
+ const cached = memoryCache.get(id);
216
+ if (cached !== undefined) {
217
+ return cached;
218
+ }
219
+
220
+ // Look up in index and load from disk
221
+ const entry = await findInIndex(id);
222
+ if (entry === null) {
223
+ return null;
224
+ }
225
+
226
+ // Load from disk using index entry
227
+ try {
228
+ const filePath = join(resolveLogDir(), entry.file);
229
+ if (!existsSync(filePath)) {
230
+ return null;
231
+ }
232
+
233
+ const content = readFileSync(filePath, "utf-8");
234
+ const lines = content.split("\n");
235
+
236
+ let lastMatch: CapturedLog | null = null;
237
+ for (const line of lines) {
238
+ if (line.trim() === "") continue;
239
+ try {
240
+ const parsed: unknown = JSON.parse(line);
241
+ if (typeof parsed === "object" && parsed !== null) {
242
+ const desc = Object.getOwnPropertyDescriptor(parsed, "id");
243
+ if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
244
+ const result = CapturedLogSchema.safeParse(parsed);
245
+ if (result.success) {
246
+ lastMatch = result.data;
247
+ // Return immediately if we found a completed entry (has responseStatus)
248
+ if (result.data.responseStatus !== null) {
249
+ return result.data;
250
+ }
251
+ }
252
+ }
253
+ }
254
+ } catch {
255
+ // Skip malformed lines
256
+ }
257
+ }
258
+ // Fallback: return the last matching entry (incomplete is better than nothing)
259
+ if (lastMatch !== null) return lastMatch;
260
+ } catch (err) {
261
+ // eslint-disable-next-line no-console
262
+ console.error("[store] Failed to read log from disk:", err);
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog[] {
269
+ // Get all logs from memory cache sorted by ID
270
+ const cachedLogs = Array.from(memoryCache.values()).sort((a, b) => a.id - b.id);
271
+
272
+ return cachedLogs.filter((l) => {
273
+ if (sessionId !== undefined && l.sessionId !== sessionId) return false;
274
+ if (model !== undefined && l.model !== model) return false;
275
+ return true;
276
+ });
277
+ }
278
+
279
+ export function getSessions(): string[] {
280
+ const set = new Set<string>();
281
+ for (const l of memoryCache.values()) {
282
+ if (l.sessionId !== null && l.sessionId !== "") set.add(l.sessionId);
283
+ }
284
+ return [...set];
285
+ }
286
+
287
+ export function getModels(): string[] {
288
+ const set = new Set<string>();
289
+ for (const l of memoryCache.values()) {
290
+ if (l.model !== null && l.model !== "") set.add(l.model);
291
+ }
292
+ return [...set];
293
+ }
294
+
295
+ /**
296
+ * Load recent completed log entries from today's log file into the memory cache.
297
+ * Called on server startup so the frontend shows existing logs.
298
+ */
299
+ export async function loadLogsIntoMemory(): Promise<void> {
300
+ const logDir = resolveLogDir();
301
+ if (!existsSync(logDir)) return;
302
+
303
+ const today = getCurrentLogFile();
304
+ const filePath = join(logDir, today);
305
+ if (!existsSync(filePath)) return;
306
+
307
+ // Keep track of IDs seen so we only load the completed entry for each
308
+ const seenComplete = new Set<number>();
309
+
310
+ try {
311
+ const fileStream = createInterface({
312
+ input: createReadStream(filePath),
313
+ crlfDelay: Infinity,
314
+ });
315
+
316
+ const lines: string[] = [];
317
+ for await (const line of fileStream) {
318
+ lines.push(line);
319
+ }
320
+
321
+ // Process in reverse so we find completed entries first
322
+ for (let i = lines.length - 1; i >= 0; i--) {
323
+ const line = lines[i] ?? "";
324
+ if (line.trim() === "") continue;
325
+ try {
326
+ const parsed: unknown = JSON.parse(line);
327
+ if (typeof parsed === "object" && parsed !== null) {
328
+ const desc = Object.getOwnPropertyDescriptor(parsed, "id");
329
+ if (desc !== undefined && typeof desc.value === "number") {
330
+ const entryId = desc.value;
331
+ // Only load completed entries (those with responseStatus)
332
+ const statusDesc = Object.getOwnPropertyDescriptor(parsed, "responseStatus");
333
+ if (
334
+ statusDesc !== undefined &&
335
+ statusDesc.value !== null &&
336
+ !seenComplete.has(entryId)
337
+ ) {
338
+ const result = CapturedLogSchema.safeParse(parsed);
339
+ if (result.success) {
340
+ addToCache(result.data);
341
+ seenComplete.add(entryId);
342
+ }
343
+ }
344
+ }
345
+ }
346
+ } catch {
347
+ // Skip malformed lines
348
+ }
349
+ }
350
+ } catch (err) {
351
+ // eslint-disable-next-line no-console
352
+ console.error("[store] Failed to load logs into memory:", err);
353
+ }
354
+ }
355
+
356
+ export function clearAllLogs(): { cleared: number } {
357
+ const count = memoryCache.size;
358
+ memoryCache.clear();
359
+ headId = null;
360
+ tailId = null;
361
+ prevMap.clear();
362
+ nextMap.clear();
363
+ return { cleared: count };
364
+ }
365
+
366
+ // SSE event system for real-time log updates
367
+ type LogUpdateHandler = (log: CapturedLog) => void;
368
+ const sseHandlers: Set<LogUpdateHandler> = new Set();
369
+
370
+ export function onLogUpdate(handler: LogUpdateHandler): () => void {
371
+ sseHandlers.add(handler);
372
+ return () => {
373
+ sseHandlers.delete(handler);
374
+ };
375
+ }
376
+
377
+ export function emitLogUpdate(log: CapturedLog): void {
378
+ for (const handler of sseHandlers) {
379
+ try {
380
+ handler(log);
381
+ } catch {
382
+ // Remove handler that threw
383
+ sseHandlers.delete(handler);
384
+ }
385
+ }
386
+ }
package/src/router.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import { createRouter } from "@tanstack/react-router";
2
+ import { routeTree } from "./routeTree.gen";
3
+
4
+ export function getRouter() {
5
+ const router = createRouter({
6
+ routeTree,
7
+ scrollRestoration: false,
8
+ });
9
+ return router;
10
+ }
11
+
12
+ declare module "@tanstack/react-router" {
13
+ interface Register {
14
+ router: ReturnType<typeof getRouter>;
15
+ }
16
+ }
@@ -0,0 +1,38 @@
1
+ /// <reference types="vite/client" />
2
+ import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
3
+ import type { ReactNode } from "react";
4
+ import appCss from "../index.css?url";
5
+
6
+ export const Route = createRootRoute({
7
+ head: () => ({
8
+ meta: [
9
+ { charSet: "utf-8" },
10
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
11
+ { title: "llm-inspector" },
12
+ ],
13
+ links: [{ rel: "stylesheet", href: appCss }],
14
+ }),
15
+ component: RootComponent,
16
+ });
17
+
18
+ function RootComponent() {
19
+ return (
20
+ <RootDocument>
21
+ <Outlet />
22
+ </RootDocument>
23
+ );
24
+ }
25
+
26
+ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
27
+ return (
28
+ <html lang="en" className="dark">
29
+ <head>
30
+ <HeadContent />
31
+ </head>
32
+ <body>
33
+ {children}
34
+ <Scripts />
35
+ </body>
36
+ </html>
37
+ );
38
+ }
@@ -0,0 +1,14 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { store } from "../../proxy/providers";
3
+
4
+ export const Route = createFileRoute("/api/config/paths")({
5
+ server: {
6
+ handlers: {
7
+ GET: () => {
8
+ return Response.json({
9
+ providerConfig: store.path,
10
+ });
11
+ },
12
+ },
13
+ },
14
+ });
@@ -0,0 +1,11 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ export const Route = createFileRoute("/api/health")({
4
+ server: {
5
+ handlers: {
6
+ GET: () => {
7
+ return Response.json({ status: "ok" });
8
+ },
9
+ },
10
+ },
11
+ });
@@ -0,0 +1,36 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { getLogById } from "../../proxy/store";
3
+ import { readChunks } from "../../proxy/chunkStorage";
4
+
5
+ export const Route = createFileRoute("/api/logs/$id/chunks")({
6
+ server: {
7
+ handlers: {
8
+ GET: async ({ params }: { params: { id: string } }) => {
9
+ const id = Number(params.id);
10
+ if (isNaN(id)) {
11
+ return Response.json({ error: "Invalid log ID" }, { status: 400 });
12
+ }
13
+
14
+ const log = await getLogById(id);
15
+ if (log === null) {
16
+ return Response.json({ error: "Log not found" }, { status: 404 });
17
+ }
18
+
19
+ // Try disk file first (persistent chunks from real proxy streaming)
20
+ if (log.streamingChunksPath !== null && log.streamingChunksPath !== undefined) {
21
+ const chunksData = readChunks(log.streamingChunksPath);
22
+ if (chunksData !== null) {
23
+ return Response.json(chunksData);
24
+ }
25
+ }
26
+
27
+ // Fall back to in-memory streamingChunks (e.g., from provider test)
28
+ if (log.streamingChunks !== undefined && log.streamingChunks.chunks.length > 0) {
29
+ return Response.json(log.streamingChunks);
30
+ }
31
+
32
+ return Response.json({ error: "Chunks not found" }, { status: 404 });
33
+ },
34
+ },
35
+ },
36
+ });