@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,389 @@
1
+ import { createLog, emitLogUpdate, type CapturedLog } from "./store";
2
+ import { appendLogEntry } from "./logger";
3
+ import { writeChunks } from "./chunkStorage";
4
+ import { extractModelFromBody, type RequestFormat } from "./schemas";
5
+ import { registry } from "./formats";
6
+ import { findProviderByModel } from "./providers";
7
+ import { getClientInfo } from "./socketTracker";
8
+ import { formatForPath, formatRegistry, type FormatHandler } from "./formats";
9
+ import {
10
+ DEFAULT_UPSTREAM,
11
+ DEFAULT_OPENAI_UPSTREAM,
12
+ PROXY_IDENTITY,
13
+ PRESERVE_HEADERS,
14
+ PATH_V1_CHAT_COMPLETIONS,
15
+ PATH_CHAT_COMPLETIONS,
16
+ PATH_V1_MESSAGES,
17
+ HEADER_CONTENT_TYPE,
18
+ HEADER_USER_AGENT,
19
+ HEADER_X_PROXY_IDENTITY,
20
+ HEADER_AUTHORIZATION,
21
+ HEADER_X_API_KEY,
22
+ HEADER_CONTENT_ENCODING,
23
+ HEADER_CONTENT_LENGTH,
24
+ HEADER_HOST,
25
+ AUTH_HEADER_X_API_KEY,
26
+ CONTENT_TYPE_EVENT_STREAM,
27
+ STATUS_FORBIDDEN,
28
+ STATUS_BAD_GATEWAY,
29
+ } from "./constants";
30
+
31
+ /**
32
+ * Strips all custom/non-standard headers from the request and replaces with
33
+ * unified proxy identity. Only preserves standard HTTP headers needed for API calls.
34
+ */
35
+ function buildProxyHeaders(originalHeaders: Headers): {
36
+ headers: Headers;
37
+ rawHeaders: Record<string, string>;
38
+ } {
39
+ const rawHeaders: Record<string, string> = {};
40
+ const headers = new Headers();
41
+
42
+ originalHeaders.forEach((value, key) => {
43
+ rawHeaders[key.toLowerCase()] = value;
44
+ });
45
+
46
+ headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
47
+ headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
48
+
49
+ for (const name of PRESERVE_HEADERS) {
50
+ const value = originalHeaders.get(name);
51
+ if (value !== null) {
52
+ headers.set(name, value);
53
+ }
54
+ }
55
+
56
+ return { headers, rawHeaders };
57
+ }
58
+
59
+ function getHostFromUrl(urlStr: string): string {
60
+ try {
61
+ const url = new URL(urlStr);
62
+ return url.host;
63
+ } catch {
64
+ return "api.anthropic.com";
65
+ }
66
+ }
67
+
68
+ function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
69
+ return {
70
+ timestamp: log.timestamp,
71
+ id: log.id,
72
+ method: log.method,
73
+ path: log.path,
74
+ model: log.model,
75
+ sessionId: log.sessionId,
76
+ rawRequestBody: log.rawRequestBody,
77
+ responseStatus: log.responseStatus,
78
+ responseText: log.responseText,
79
+ inputTokens: log.inputTokens,
80
+ outputTokens: log.outputTokens,
81
+ elapsedMs: log.elapsedMs,
82
+ streaming: log.streaming,
83
+ userAgent: log.userAgent,
84
+ origin: log.origin,
85
+ upstreamUrl,
86
+ clientPort: log.clientPort,
87
+ clientPid: log.clientPid,
88
+ clientCwd: log.clientCwd,
89
+ clientProjectFolder: log.clientProjectFolder,
90
+ streamingChunks: log.streamingChunks,
91
+ streamingChunksPath: log.streamingChunksPath,
92
+ };
93
+ }
94
+
95
+ type ParsedRequestPath = {
96
+ apiPath: string;
97
+ messagesPath: string;
98
+ isChatCompletionsV1: boolean;
99
+ isChatCompletions: boolean;
100
+ isMessages: boolean;
101
+ normalizedPath: string;
102
+ };
103
+
104
+ function parseRequestPath(req: Request, url: URL): ParsedRequestPath {
105
+ const apiPath = url.pathname.replace(/^\/proxy/, "") + url.search;
106
+ const messagesPath = apiPath.split("?")[0] ?? "";
107
+
108
+ const isChatCompletionsV1 = messagesPath === PATH_V1_CHAT_COMPLETIONS;
109
+ const isChatCompletions = messagesPath === PATH_CHAT_COMPLETIONS || isChatCompletionsV1;
110
+ const isMessages =
111
+ req.method === "POST" &&
112
+ (messagesPath === PATH_V1_MESSAGES ||
113
+ messagesPath === PATH_V1_CHAT_COMPLETIONS ||
114
+ messagesPath === PATH_CHAT_COMPLETIONS);
115
+
116
+ const normalizedPath =
117
+ isChatCompletions && !apiPath.startsWith("/v1/") ? "/v1" + apiPath : apiPath;
118
+
119
+ return {
120
+ apiPath,
121
+ messagesPath,
122
+ isChatCompletionsV1,
123
+ isChatCompletions,
124
+ isMessages,
125
+ normalizedPath,
126
+ };
127
+ }
128
+
129
+ function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
130
+ return upstreamBase + normalizedPath;
131
+ }
132
+
133
+ function selectUpstreamBase(
134
+ isChatCompletions: boolean,
135
+ matchedProviderConfig: ReturnType<typeof findProviderByModel>,
136
+ ): string {
137
+ let upstreamBase: string;
138
+
139
+ if (matchedProviderConfig) {
140
+ // Choose URL based on request format: ChatCompletions uses OpenAI URL, otherwise Anthropic URL
141
+ if (
142
+ isChatCompletions &&
143
+ matchedProviderConfig.openaiBaseUrl !== undefined &&
144
+ matchedProviderConfig.openaiBaseUrl !== ""
145
+ ) {
146
+ upstreamBase = matchedProviderConfig.openaiBaseUrl;
147
+ } else if (
148
+ !isChatCompletions &&
149
+ matchedProviderConfig.anthropicBaseUrl !== undefined &&
150
+ matchedProviderConfig.anthropicBaseUrl !== ""
151
+ ) {
152
+ upstreamBase = matchedProviderConfig.anthropicBaseUrl;
153
+ } else if (
154
+ matchedProviderConfig.baseUrl !== undefined &&
155
+ matchedProviderConfig.baseUrl !== ""
156
+ ) {
157
+ // Fallback to baseUrl for backward compatibility
158
+ upstreamBase = matchedProviderConfig.baseUrl;
159
+ } else {
160
+ // Fall back to defaults based on format
161
+ upstreamBase =
162
+ matchedProviderConfig.format === "openai" ? DEFAULT_OPENAI_UPSTREAM : DEFAULT_UPSTREAM;
163
+ }
164
+ } else {
165
+ // No provider config, use defaults
166
+ upstreamBase = isChatCompletions ? DEFAULT_OPENAI_UPSTREAM : DEFAULT_UPSTREAM;
167
+ }
168
+
169
+ return upstreamBase;
170
+ }
171
+
172
+ function injectAuthHeaders(
173
+ upstreamHeaders: Headers,
174
+ matchedProviderConfig: ReturnType<typeof findProviderByModel>,
175
+ ): void {
176
+ if (!matchedProviderConfig) return;
177
+
178
+ const apiKey = matchedProviderConfig.apiKey.replace(/^Bearer\s+/i, "").trim();
179
+ if (matchedProviderConfig.authHeader === AUTH_HEADER_X_API_KEY) {
180
+ upstreamHeaders.set(HEADER_X_API_KEY, apiKey);
181
+ upstreamHeaders.delete(HEADER_AUTHORIZATION);
182
+ } else {
183
+ upstreamHeaders.set(HEADER_AUTHORIZATION, `Bearer ${apiKey}`);
184
+ }
185
+ }
186
+
187
+ function handleNonStreamingResponse(
188
+ upstreamRes: Response,
189
+ responseBody: string,
190
+ startTime: number,
191
+ formatHandler: FormatHandler,
192
+ upstreamUrl: string,
193
+ log: CapturedLog,
194
+ ): Response {
195
+ const elapsedMs = Date.now() - startTime;
196
+ const tokens = formatHandler.extractTokens(responseBody);
197
+
198
+ log.elapsedMs = elapsedMs;
199
+ log.responseStatus = upstreamRes.status;
200
+ log.responseText = responseBody;
201
+ log.inputTokens = tokens.inputTokens;
202
+ log.outputTokens = tokens.outputTokens;
203
+ log.cacheCreationInputTokens = tokens.cacheCreationInputTokens;
204
+ log.cacheReadInputTokens = tokens.cacheReadInputTokens;
205
+
206
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
207
+
208
+ const responseHeaders = new Headers(upstreamRes.headers);
209
+ responseHeaders.delete(HEADER_CONTENT_ENCODING);
210
+ responseHeaders.delete(HEADER_CONTENT_LENGTH);
211
+
212
+ return new Response(responseBody, {
213
+ status: upstreamRes.status,
214
+ headers: responseHeaders,
215
+ });
216
+ }
217
+
218
+ function handleStreamingResponse(
219
+ upstreamRes: Response,
220
+ req: Request,
221
+ startTime: number,
222
+ formatHandler: FormatHandler,
223
+ upstreamUrl: string,
224
+ log: CapturedLog,
225
+ ): Response {
226
+ log.streaming = true;
227
+ log.responseStatus = upstreamRes.status;
228
+
229
+ const chunks: string[] = [];
230
+ const decoder = new TextDecoder();
231
+
232
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
233
+ transform(chunk, controller) {
234
+ controller.enqueue(chunk);
235
+ chunks.push(decoder.decode(chunk, { stream: true }));
236
+ },
237
+ flush() {
238
+ const full = chunks.join("");
239
+ log.elapsedMs = Date.now() - startTime;
240
+ log.responseText = formatHandler.extractStream(full, log, log.model ?? undefined, true);
241
+ // Persist chunks to disk
242
+ if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
243
+ const chunkPath = writeChunks(
244
+ log.id,
245
+ log.streamingChunks.chunks,
246
+ log.streamingChunks.truncated,
247
+ );
248
+ log.streamingChunksPath = chunkPath;
249
+ }
250
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
251
+ emitLogUpdate(log);
252
+ },
253
+ });
254
+
255
+ if (upstreamRes.body === null) {
256
+ return new Response("No response body", { status: STATUS_BAD_GATEWAY });
257
+ }
258
+
259
+ const loggedStream = upstreamRes.body.pipeThrough(transform);
260
+
261
+ req.signal?.addEventListener("abort", () => {
262
+ if (log.responseText === null && chunks.length > 0) {
263
+ const full = chunks.join("");
264
+ log.elapsedMs = Date.now() - startTime;
265
+ log.responseText = formatHandler.extractStream(full, log, log.model ?? undefined, true);
266
+ // Persist chunks to disk on abort
267
+ if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
268
+ const chunkPath = writeChunks(
269
+ log.id,
270
+ log.streamingChunks.chunks,
271
+ log.streamingChunks.truncated,
272
+ );
273
+ log.streamingChunksPath = chunkPath;
274
+ }
275
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
276
+ emitLogUpdate(log);
277
+ }
278
+ });
279
+
280
+ const responseHeaders = new Headers(upstreamRes.headers);
281
+ responseHeaders.delete(HEADER_CONTENT_ENCODING);
282
+ responseHeaders.delete(HEADER_CONTENT_LENGTH);
283
+
284
+ return new Response(loggedStream, {
285
+ status: upstreamRes.status,
286
+ headers: responseHeaders,
287
+ });
288
+ }
289
+
290
+ export async function handleProxy(req: Request): Promise<Response> {
291
+ const url = new URL(req.url);
292
+ const parsed = parseRequestPath(req, url);
293
+
294
+ let requestBody: string | null = null;
295
+ if (req.body && req.method !== "GET" && req.method !== "HEAD") {
296
+ requestBody = await req.text();
297
+ }
298
+
299
+ // Extract model once and reuse
300
+ const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
301
+
302
+ const matchedProviderConfig = findProviderByModelFromConfig(requestBody);
303
+ const upstreamBase = selectUpstreamBase(parsed.isChatCompletions, matchedProviderConfig);
304
+ const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
305
+ const upstreamHost = getHostFromUrl(upstreamBase);
306
+ const startTime = Date.now();
307
+
308
+ const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(req.headers);
309
+ upstreamHeaders.set(HEADER_HOST, upstreamHost);
310
+ injectAuthHeaders(upstreamHeaders, matchedProviderConfig);
311
+
312
+ const provider = model !== null ? registry.findProvider(model) : null;
313
+
314
+ // Only proxy requests matching a registered provider
315
+ if (model === null || provider === null) {
316
+ return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
317
+ }
318
+
319
+ // Get the format handler based on provider format (preferred) or request path fallback
320
+ let formatHandler: FormatHandler | null;
321
+ if (matchedProviderConfig?.format) {
322
+ formatHandler = formatRegistry.get(matchedProviderConfig.format) ?? null;
323
+ } else {
324
+ formatHandler = formatForPath(parsed.apiPath);
325
+ }
326
+ if (formatHandler === null) {
327
+ return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
328
+ }
329
+
330
+ const clientInfo = await getClientInfo(req);
331
+ const upstreamHeadersObj: Record<string, string> = {};
332
+ upstreamHeaders.forEach((value, key) => {
333
+ upstreamHeadersObj[key.toLowerCase()] = value;
334
+ });
335
+ const log = await createLog(
336
+ req.method,
337
+ parsed.apiPath,
338
+ requestBody,
339
+ req.headers,
340
+ clientInfo,
341
+ rawHeaders,
342
+ upstreamHeadersObj,
343
+ formatHandler.format,
344
+ );
345
+
346
+ let upstreamRes: Response;
347
+ try {
348
+ upstreamRes = await fetch(upstreamUrl, {
349
+ method: req.method,
350
+ headers: upstreamHeaders,
351
+ body: requestBody,
352
+ });
353
+ } catch (err) {
354
+ log.elapsedMs = Date.now() - startTime;
355
+ log.responseStatus = STATUS_BAD_GATEWAY;
356
+ log.responseText = String(err);
357
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
358
+ return new Response(`Proxy error: ${err}`, { status: STATUS_BAD_GATEWAY });
359
+ }
360
+
361
+ const isStream =
362
+ upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
363
+
364
+ if (!isStream) {
365
+ const responseBody = await upstreamRes.text();
366
+ return handleNonStreamingResponse(
367
+ upstreamRes,
368
+ responseBody,
369
+ startTime,
370
+ formatHandler,
371
+ upstreamUrl,
372
+ log,
373
+ );
374
+ }
375
+
376
+ return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
377
+ }
378
+
379
+ /**
380
+ * Find provider configuration by model name (from persistent store).
381
+ */
382
+ function findProviderByModelFromConfig(
383
+ requestBody: string | null,
384
+ ): ReturnType<typeof findProviderByModel> {
385
+ if (requestBody === null) return null;
386
+ const model = extractModelFromBody(requestBody);
387
+ if (model === null) return null;
388
+ return findProviderByModel(model);
389
+ }
@@ -0,0 +1,187 @@
1
+ import { readFile, writeFile, stat, readdir, mkdir } from "node:fs/promises";
2
+ import { createReadStream, existsSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { resolveLogDir } from "./logger";
6
+
7
+ type LogIndexEntry = {
8
+ id: number;
9
+ file: string;
10
+ lineStart: number;
11
+ lineEnd: number;
12
+ };
13
+
14
+ type LogIndex = {
15
+ version: number;
16
+ entries: Record<number, LogIndexEntry>;
17
+ maxId: number;
18
+ };
19
+
20
+ const INDEX_VERSION = 1;
21
+ const INDEX_FILE = "logs.idx";
22
+
23
+ export { resolveLogDir as getLogDir };
24
+
25
+ function getIndexPath(): string {
26
+ return join(resolveLogDir(), INDEX_FILE);
27
+ }
28
+
29
+ let cachedIndex: LogIndex | null = null;
30
+
31
+ function createEmptyIndex(): LogIndex {
32
+ return {
33
+ version: INDEX_VERSION,
34
+ entries: {},
35
+ maxId: 0,
36
+ };
37
+ }
38
+
39
+ function isLogIndex(obj: unknown): obj is LogIndex {
40
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
41
+ const versionDesc = Object.getOwnPropertyDescriptor(obj, "version");
42
+ const entriesDesc = Object.getOwnPropertyDescriptor(obj, "entries");
43
+ const maxIdDesc = Object.getOwnPropertyDescriptor(obj, "maxId");
44
+ return (
45
+ versionDesc !== undefined &&
46
+ typeof versionDesc.value === "number" &&
47
+ entriesDesc !== undefined &&
48
+ typeof entriesDesc.value === "object" &&
49
+ entriesDesc.value !== null &&
50
+ maxIdDesc !== undefined &&
51
+ typeof maxIdDesc.value === "number"
52
+ );
53
+ }
54
+
55
+ export async function loadIndex(): Promise<LogIndex> {
56
+ if (cachedIndex !== null) return cachedIndex;
57
+
58
+ const indexPath = getIndexPath();
59
+ if (!existsSync(indexPath)) {
60
+ cachedIndex = createEmptyIndex();
61
+ return cachedIndex;
62
+ }
63
+
64
+ try {
65
+ const content = await readFile(indexPath, "utf-8");
66
+ const parsed: unknown = JSON.parse(content);
67
+ if (isLogIndex(parsed)) {
68
+ cachedIndex = parsed;
69
+ } else {
70
+ cachedIndex = createEmptyIndex();
71
+ }
72
+ return cachedIndex;
73
+ } catch (err) {
74
+ // eslint-disable-next-line no-console
75
+ console.error("[logIndex] Failed to load index:", err);
76
+ cachedIndex = createEmptyIndex();
77
+ return cachedIndex;
78
+ }
79
+ }
80
+
81
+ export async function saveIndex(index: LogIndex): Promise<void> {
82
+ const indexPath = getIndexPath();
83
+ const dir = dirname(indexPath);
84
+
85
+ try {
86
+ await mkdir(dir, { recursive: true });
87
+ } catch {
88
+ // Ignore
89
+ }
90
+
91
+ try {
92
+ await writeFile(indexPath, JSON.stringify(index), "utf-8");
93
+ } catch (err) {
94
+ // eslint-disable-next-line no-console
95
+ console.error("[logIndex] Failed to save index:", err);
96
+ }
97
+ }
98
+
99
+ export async function addToIndex(
100
+ id: number,
101
+ file: string,
102
+ lineStart: number,
103
+ lineEnd: number,
104
+ ): Promise<void> {
105
+ const index = await loadIndex();
106
+ index.entries[id] = { id, file, lineStart, lineEnd };
107
+ if (id > index.maxId) {
108
+ index.maxId = id;
109
+ }
110
+ await saveIndex(index);
111
+ }
112
+
113
+ export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
114
+ const index = await loadIndex();
115
+ return index.entries[id] ?? null;
116
+ }
117
+
118
+ export async function rebuildIndex(): Promise<LogIndex> {
119
+ const logDir = resolveLogDir();
120
+ const newIndex = createEmptyIndex();
121
+
122
+ if (!existsSync(logDir)) {
123
+ cachedIndex = newIndex;
124
+ await saveIndex(newIndex);
125
+ return newIndex;
126
+ }
127
+
128
+ let currentId = 0;
129
+
130
+ const files = (await readdir(logDir)).filter((f) => f.endsWith(".jsonl")).sort();
131
+
132
+ for (const file of files) {
133
+ const filePath = join(logDir, file);
134
+ try {
135
+ const fileStream = createInterface({
136
+ input: createReadStream(filePath),
137
+ crlfDelay: Infinity,
138
+ });
139
+
140
+ let lineNumber = 0;
141
+
142
+ for await (const line of fileStream) {
143
+ lineNumber++;
144
+ if (line.trim() === "") continue;
145
+
146
+ try {
147
+ const entry: unknown = JSON.parse(line);
148
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
149
+ const idDesc = Object.getOwnPropertyDescriptor(entry, "id");
150
+ if (idDesc !== undefined && typeof idDesc.value === "number") {
151
+ const entryId = idDesc.value;
152
+ currentId = Math.max(currentId, entryId);
153
+ newIndex.entries[entryId] = {
154
+ id: entryId,
155
+ file,
156
+ lineStart: lineNumber,
157
+ lineEnd: lineNumber,
158
+ };
159
+ }
160
+ }
161
+ } catch {
162
+ // Skip malformed lines
163
+ }
164
+ }
165
+ } catch {
166
+ // Skip files that can't be read
167
+ }
168
+ }
169
+
170
+ newIndex.maxId = currentId;
171
+ cachedIndex = newIndex;
172
+ await saveIndex(newIndex);
173
+ return newIndex;
174
+ }
175
+
176
+ export async function getNextLogId(): Promise<number> {
177
+ const index = await loadIndex();
178
+ return index.maxId + 1;
179
+ }
180
+
181
+ export function getCurrentLogFile(): string {
182
+ const now = new Date();
183
+ const yyyy = now.getUTCFullYear();
184
+ const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
185
+ const dd = String(now.getUTCDate()).padStart(2, "0");
186
+ return `${yyyy}-${mm}-${dd}.jsonl`;
187
+ }
@@ -0,0 +1,99 @@
1
+ import { readdir, stat, unlink, appendFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const LOG_DIR_ENV = process.env["LOG_DIR"];
5
+ const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
6
+
7
+ function getUserDataDir(): string {
8
+ if (process.platform === "win32") {
9
+ return (
10
+ process.env["APPDATA"] ?? path.join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector")
11
+ );
12
+ }
13
+ return process.env["HOME"] ?? path.join("/tmp");
14
+ }
15
+
16
+ let resolvedLogDir: string | null = null;
17
+
18
+ export function resolveLogDir(): string {
19
+ if (resolvedLogDir !== null) return resolvedLogDir;
20
+
21
+ const base = getUserDataDir();
22
+ if (LOG_DIR_ENV !== undefined) {
23
+ resolvedLogDir = path.isAbsolute(LOG_DIR_ENV) ? LOG_DIR_ENV : path.join(base, LOG_DIR_ENV);
24
+ } else {
25
+ resolvedLogDir = path.join(base, ".llm-inspector", "logs");
26
+ }
27
+ return resolvedLogDir;
28
+ }
29
+
30
+ export function getLogFilePath(): string {
31
+ const date = new Date();
32
+ const yyyy = date.getUTCFullYear();
33
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
34
+ const dd = String(date.getUTCDate()).padStart(2, "0");
35
+ return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
36
+ }
37
+
38
+ export async function initLogger(): Promise<void> {
39
+ const dir = resolveLogDir();
40
+ const retentionMs = RETENTION_DAYS * 24 * 60 * 60 * 1000;
41
+ const cutoff = Date.now() - retentionMs;
42
+
43
+ try {
44
+ const entries = await readdir(dir);
45
+ for (const entry of entries) {
46
+ if (!entry.endsWith(".jsonl")) continue;
47
+ const fullPath = path.join(dir, entry);
48
+ try {
49
+ const s = await stat(fullPath);
50
+ if (s.mtimeMs < cutoff) {
51
+ await unlink(fullPath);
52
+ }
53
+ } catch (err) {
54
+ // eslint-disable-next-line no-console
55
+ console.error("[logger] Failed to stat/remove log file:", err);
56
+ }
57
+ }
58
+ } catch (err) {
59
+ // eslint-disable-next-line no-console
60
+ console.error("[logger] Failed to initialize logger:", err);
61
+ }
62
+ }
63
+
64
+ // Write buffer for batching async writes
65
+ let writeBuffer: string[] = [];
66
+ let writeScheduled = false;
67
+
68
+ async function flushWriteBuffer(): Promise<void> {
69
+ if (writeBuffer.length === 0) return;
70
+
71
+ const toWrite = writeBuffer.join("");
72
+ writeBuffer = [];
73
+
74
+ try {
75
+ const filePath = getLogFilePath();
76
+ await mkdir(path.dirname(filePath), { recursive: true });
77
+ await appendFile(filePath, toWrite, "utf-8");
78
+ } catch (err) {
79
+ // eslint-disable-next-line no-console
80
+ console.error("[logger] Failed to write log entries:", err);
81
+ }
82
+ }
83
+
84
+ function scheduleFlush(): void {
85
+ if (writeScheduled) return;
86
+ writeScheduled = true;
87
+ // Use Promise.resolve().then to flush after current event loop tick
88
+ // This batches multiple synchronous appendLogEntry calls
89
+ void Promise.resolve().then(() => {
90
+ writeScheduled = false;
91
+ void flushWriteBuffer();
92
+ });
93
+ }
94
+
95
+ export function appendLogEntry(entry: Record<string, unknown>): void {
96
+ const line = JSON.stringify(entry) + "\n";
97
+ writeBuffer.push(line);
98
+ scheduleFlush();
99
+ }