@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,211 @@
1
+ import { useState, useEffect, useCallback, useRef, type JSX } from "react";
2
+ import { z } from "zod";
3
+ import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
4
+ import { ProxyViewer } from "./ProxyViewer";
5
+
6
+ type logsResponse = {
7
+ logs: CapturedLog[];
8
+ total: number;
9
+ offset: number;
10
+ limit: number;
11
+ };
12
+
13
+ const LogsResponseSchema = z.object({
14
+ logs: z.array(CapturedLogSchema),
15
+ total: z.number(),
16
+ offset: z.number(),
17
+ limit: z.number(),
18
+ });
19
+
20
+ type SSEUpdate =
21
+ | {
22
+ type: "init";
23
+ logs: CapturedLog[];
24
+ }
25
+ | {
26
+ type: "update";
27
+ log: CapturedLog;
28
+ };
29
+
30
+ const SSEUpdateSchema = z.union([
31
+ z.object({
32
+ type: z.literal("init"),
33
+ logs: z.array(CapturedLogSchema),
34
+ }),
35
+ z.object({
36
+ type: z.literal("update"),
37
+ log: CapturedLogSchema,
38
+ }),
39
+ ]);
40
+
41
+ function extractSessions(logs: CapturedLog[]): string[] {
42
+ const set = new Set<string>();
43
+ for (const l of logs) {
44
+ if (l.sessionId !== null && l.sessionId !== "") set.add(l.sessionId);
45
+ }
46
+ return [...set];
47
+ }
48
+
49
+ function extractModels(logs: CapturedLog[]): string[] {
50
+ const set = new Set<string>();
51
+ for (const l of logs) {
52
+ if (l.model !== null && l.model !== "") set.add(l.model);
53
+ }
54
+ return [...set];
55
+ }
56
+
57
+ export function ProxyViewerContainer(): JSX.Element {
58
+ const [logs, setLogs] = useState<CapturedLog[]>([]);
59
+ const [sessions, setSessions] = useState<string[]>([]);
60
+ const [models, setModels] = useState<string[]>([]);
61
+ const [selectedSession, setSelectedSession] = useState("__all__");
62
+ const [selectedModel, setSelectedModel] = useState("__all__");
63
+ const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
64
+ const [error, setError] = useState<string | null>(null);
65
+ const eventSourceRef = useRef<EventSource | null>(null);
66
+
67
+ const fetchSessionsAndModels = useCallback(async () => {
68
+ try {
69
+ const [sessionsRes, modelsRes] = await Promise.all([
70
+ fetch("/api/sessions"),
71
+ fetch("/api/models"),
72
+ ]);
73
+ if (sessionsRes.ok && modelsRes.ok) {
74
+ const sessionsJson: unknown = await sessionsRes.json();
75
+ const modelsJson: unknown = await modelsRes.json();
76
+ const sessionsResult = z.array(z.string()).safeParse(sessionsJson);
77
+ const modelsResult = z.array(z.string()).safeParse(modelsJson);
78
+ if (sessionsResult.success) setSessions(sessionsResult.data);
79
+ if (modelsResult.success) setModels(modelsResult.data);
80
+ }
81
+ } catch {
82
+ // SSE already has sessions/models from init
83
+ }
84
+ }, []);
85
+
86
+ const connectSSE = useCallback(() => {
87
+ // Clean up existing connection
88
+ if (eventSourceRef.current) {
89
+ eventSourceRef.current.close();
90
+ }
91
+
92
+ const params = new URLSearchParams();
93
+ if (selectedSession !== "__all__") params.set("sessionId", selectedSession);
94
+ if (selectedModel !== "__all__") params.set("model", selectedModel);
95
+
96
+ const es = new EventSource(`/api/logs/stream?${params}`);
97
+ eventSourceRef.current = es;
98
+
99
+ es.onmessage = (event: MessageEvent) => {
100
+ try {
101
+ const rawData = String(event.data);
102
+ const parsed: unknown = JSON.parse(rawData);
103
+ const updateResult = SSEUpdateSchema.safeParse(parsed);
104
+ if (!updateResult.success) {
105
+ setError("Failed to parse SSE data");
106
+ return;
107
+ }
108
+ const update = updateResult.data;
109
+ if (update.type === "init") {
110
+ setLogs(update.logs);
111
+ setSessions(extractSessions(update.logs));
112
+ setModels(extractModels(update.logs));
113
+ setError(null);
114
+ } else if (update.type === "update") {
115
+ setLogs((prev) => {
116
+ // Filter by current selection before adding
117
+ const sessionMatch =
118
+ selectedSession === "__all__" || update.log.sessionId === selectedSession;
119
+ const modelMatch = selectedModel === "__all__" || update.log.model === selectedModel;
120
+ if (!sessionMatch || !modelMatch) return prev;
121
+
122
+ // Update existing log or add as new
123
+ const existsIndex = prev.findIndex((l) => l.id === update.log.id);
124
+ if (existsIndex >= 0) {
125
+ // Replace the existing log with updated data
126
+ return prev.map((l) => (l.id === update.log.id ? update.log : l));
127
+ }
128
+ // Add to end (newest)
129
+ return [...prev, update.log];
130
+ });
131
+ // Update sessions and models
132
+ setSessions((prev) => {
133
+ const sessionId = update.log.sessionId;
134
+ if (sessionId !== null && sessionId !== undefined && sessionId !== "") {
135
+ return prev.includes(sessionId) ? prev : [...prev, sessionId];
136
+ }
137
+ return prev;
138
+ });
139
+ setModels((prev) => {
140
+ const model = update.log.model;
141
+ if (model !== null && model !== undefined && model !== "") {
142
+ return prev.includes(model) ? prev : [...prev, model];
143
+ }
144
+ return prev;
145
+ });
146
+ }
147
+ } catch {
148
+ setError("Failed to parse SSE data");
149
+ }
150
+ };
151
+
152
+ es.onerror = () => {
153
+ setError("SSE connection lost, reconnecting...");
154
+ es.close();
155
+ // Reconnect after 3 seconds
156
+ setTimeout(connectSSE, 3000);
157
+ };
158
+
159
+ void fetchSessionsAndModels();
160
+ }, [selectedSession, selectedModel, fetchSessionsAndModels]);
161
+
162
+ useEffect(() => {
163
+ connectSSE();
164
+ return () => {
165
+ if (eventSourceRef.current) {
166
+ eventSourceRef.current.close();
167
+ eventSourceRef.current = null;
168
+ }
169
+ };
170
+ }, [connectSSE]);
171
+
172
+ const handleClearAll = useCallback(() => {
173
+ void (async () => {
174
+ try {
175
+ const res = await fetch("/api/logs", { method: "DELETE" });
176
+ if (!res.ok) {
177
+ setError("Failed to clear logs");
178
+ return;
179
+ }
180
+ setLogs([]);
181
+ setSessions([]);
182
+ setModels([]);
183
+ setError(null);
184
+ } catch (err) {
185
+ setError(err instanceof Error ? err.message : "Unknown error clearing logs");
186
+ }
187
+ })();
188
+ }, []);
189
+
190
+ return (
191
+ <>
192
+ {error !== null && (
193
+ <div className="fixed top-4 right-4 bg-destructive text-destructive-foreground px-4 py-2 rounded-md text-sm z-50">
194
+ {error}
195
+ </div>
196
+ )}
197
+ <ProxyViewer
198
+ logs={logs}
199
+ sessions={sessions}
200
+ models={models}
201
+ selectedSession={selectedSession}
202
+ selectedModel={selectedModel}
203
+ onSessionChange={setSelectedSession}
204
+ onModelChange={setSelectedModel}
205
+ onClearAll={handleClearAll}
206
+ viewMode={viewMode}
207
+ onViewModeChange={setViewMode}
208
+ />
209
+ </>
210
+ );
211
+ }
@@ -0,0 +1,186 @@
1
+ import { type JSX, useState } from "react";
2
+ import { Button } from "../ui/button";
3
+ import {
4
+ Eye,
5
+ EyeOff,
6
+ Pencil,
7
+ Trash2,
8
+ RotateCw,
9
+ CheckCircle,
10
+ XCircle,
11
+ Minus,
12
+ ExternalLink,
13
+ } from "lucide-react";
14
+ import type { ProviderConfig } from "../../proxy/providers";
15
+
16
+ // Known provider documentation links
17
+ const KNOWN_PROVIDER_DOCS: Record<string, string> = {
18
+ deepseek: "https://api-docs.deepseek.com/zh-cn/",
19
+ };
20
+
21
+ type TestResult = {
22
+ success: boolean;
23
+ error?: string;
24
+ };
25
+
26
+ type NotConfigured = { notConfigured: true };
27
+
28
+ type StreamingTestResults = {
29
+ nonStreaming: TestResult | NotConfigured;
30
+ streaming: TestResult | NotConfigured;
31
+ };
32
+
33
+ type TestResults = {
34
+ anthropic: StreamingTestResults;
35
+ openai: StreamingTestResults;
36
+ };
37
+
38
+ type ProviderCardProps = {
39
+ provider: ProviderConfig;
40
+ testResults?: TestResults;
41
+ isTesting?: boolean;
42
+ onEdit: (provider: ProviderConfig) => void;
43
+ onDelete: (providerId: string) => void;
44
+ onTest?: (providerId: string) => void;
45
+ };
46
+
47
+ function maskApiKey(apiKey: string): string {
48
+ if (apiKey.length <= 8) return "••••••••";
49
+ return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
50
+ }
51
+
52
+ function hasSuccessField(result: TestResult | NotConfigured): result is TestResult {
53
+ return Object.prototype.hasOwnProperty.call(result, "success");
54
+ }
55
+
56
+ function TestStatus({ result }: { result: TestResult | NotConfigured }): JSX.Element {
57
+ if (!hasSuccessField(result)) {
58
+ return (
59
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
60
+ <Minus className="size-3" />
61
+ <span>Not configured</span>
62
+ </div>
63
+ );
64
+ }
65
+ if (result.success) {
66
+ return (
67
+ <div className="flex items-center gap-1 text-xs text-green-600">
68
+ <CheckCircle className="size-3" />
69
+ <span>Connected</span>
70
+ </div>
71
+ );
72
+ }
73
+ return (
74
+ <div className="flex items-center gap-1 text-xs text-red-600" title={result.error}>
75
+ <XCircle className="size-3" />
76
+ <span className="truncate">{result.error}</span>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ export function ProviderCard({
82
+ provider,
83
+ testResults,
84
+ isTesting,
85
+ onEdit,
86
+ onDelete,
87
+ onTest,
88
+ }: ProviderCardProps): JSX.Element {
89
+ const [showApiKey, setShowApiKey] = useState(false);
90
+
91
+ return (
92
+ <div className="border rounded-lg p-4 flex flex-col gap-3 bg-card">
93
+ <div className="flex items-start justify-between gap-2">
94
+ <div className="flex items-center gap-2 min-w-0">
95
+ <span className="font-medium truncate">
96
+ {provider.model !== undefined && provider.model !== ""
97
+ ? `${provider.model} (${provider.name})`
98
+ : provider.name}
99
+ </span>
100
+ </div>
101
+ {Object.entries(KNOWN_PROVIDER_DOCS).map(([keyword, url]) =>
102
+ provider.name.toLowerCase().includes(keyword) ? (
103
+ <a
104
+ key={keyword}
105
+ href={url}
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ className="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1 text-xs"
109
+ title="View API documentation"
110
+ >
111
+ <ExternalLink className="size-3" />
112
+ <span className="sr-only">Docs</span>
113
+ </a>
114
+ ) : null,
115
+ )}
116
+ </div>
117
+
118
+ <div className="flex items-center gap-2">
119
+ <code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate">
120
+ {showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
121
+ </code>
122
+ <button
123
+ type="button"
124
+ onClick={() => setShowApiKey((s) => !s)}
125
+ className="text-muted-foreground hover:text-foreground transition-colors p-1"
126
+ aria-label={showApiKey ? "Hide API key" : "Show API key"}
127
+ >
128
+ {showApiKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
129
+ </button>
130
+ </div>
131
+
132
+ {provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
133
+ <div className="flex items-center justify-between gap-2">
134
+ <div className="text-xs text-muted-foreground min-w-0 flex-1">
135
+ <span className="font-medium">Anthropic:</span>{" "}
136
+ <span className="truncate">{provider.anthropicBaseUrl}</span>
137
+ </div>
138
+ {testResults && <TestStatus result={testResults.anthropic.nonStreaming} />}
139
+ </div>
140
+ )}
141
+
142
+ {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
143
+ <div className="flex items-center justify-between gap-2">
144
+ <div className="text-xs text-muted-foreground min-w-0 flex-1">
145
+ <span className="font-medium">OpenAI:</span>{" "}
146
+ <span className="truncate">{provider.openaiBaseUrl}</span>
147
+ </div>
148
+ {testResults && <TestStatus result={testResults.openai.nonStreaming} />}
149
+ </div>
150
+ )}
151
+
152
+ <div className="flex gap-2 pt-1 border-t">
153
+ {onTest !== undefined && (
154
+ <Button
155
+ variant="outline"
156
+ size="sm"
157
+ onClick={() => onTest(provider.id)}
158
+ className="text-xs h-7 gap-1"
159
+ disabled={isTesting ?? false}
160
+ >
161
+ <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
162
+ {(isTesting ?? false) ? "Testing..." : "Test"}
163
+ </Button>
164
+ )}
165
+ <Button
166
+ variant="outline"
167
+ size="sm"
168
+ onClick={() => onEdit(provider)}
169
+ className="text-xs h-7 gap-1"
170
+ >
171
+ <Pencil className="size-3" />
172
+ Edit
173
+ </Button>
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={() => onDelete(provider.id)}
178
+ className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
179
+ >
180
+ <Trash2 className="size-3" />
181
+ Delete
182
+ </Button>
183
+ </div>
184
+ </div>
185
+ );
186
+ }
@@ -0,0 +1,259 @@
1
+ import { type JSX, useState, useEffect } from "react";
2
+ import { Button } from "../ui/button";
3
+ import type { ProviderConfig } from "../../proxy/providers";
4
+
5
+ // Known provider presets - maps provider name keywords to their API URLs
6
+ const KNOWN_PROVIDER_PRESETS: Record<string, { format: "anthropic" | "openai"; baseUrl: string }> =
7
+ {
8
+ deepseek: {
9
+ format: "openai",
10
+ baseUrl: "https://api.deepseek.com",
11
+ },
12
+ minimax: {
13
+ format: "anthropic",
14
+ baseUrl: "https://api.minimaxi.com/anthropic",
15
+ },
16
+ alibaba: {
17
+ format: "openai",
18
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode",
19
+ },
20
+ };
21
+
22
+ // MiniMax model options
23
+ const MINIMAX_MODELS = [
24
+ "MiniMax M2.7",
25
+ "MiniMax M2.7-highspeed",
26
+ "MiniMax M2.5",
27
+ "MiniMax M2.5-highspeed",
28
+ "MiniMax M2.1",
29
+ "MiniMax M2.1-highspeed",
30
+ "MiniMax M2",
31
+ ];
32
+
33
+ // Alibaba model options
34
+ const ALIBABA_MODELS = ["glm-5", "glm-5.1", "qwen3.6-plus", "qwen3.7-max"];
35
+
36
+ type ProviderFormProps = {
37
+ provider?: ProviderConfig;
38
+ onSubmit: (data: {
39
+ name: string;
40
+ apiKey: string;
41
+ model?: string;
42
+ format: "anthropic" | "openai";
43
+ baseUrl?: string;
44
+ }) => void;
45
+ onCancel: () => void;
46
+ };
47
+
48
+ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps): JSX.Element {
49
+ const [name, setName] = useState(provider?.name ?? "Provider Name");
50
+ const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
51
+ const [model, setModel] = useState(provider?.model ?? "");
52
+ const [format, setFormat] = useState<"anthropic" | "openai">(provider?.format ?? "anthropic");
53
+ const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? "");
54
+ const [errors, setErrors] = useState<Record<string, string>>({});
55
+ const [isSubmitting, setIsSubmitting] = useState(false);
56
+
57
+ // Track if URL field has been manually edited (to avoid overriding user edits)
58
+ const [manualBaseUrlOverride, setManualBaseUrlOverride] = useState(false);
59
+
60
+ // Check if MiniMax is detected
61
+ const isMiniMax = name.toLowerCase().includes("minimax");
62
+ // Check if Alibaba is detected
63
+ const isAlibaba = name.toLowerCase().includes("alibaba");
64
+
65
+ useEffect(() => {
66
+ if (provider) {
67
+ setName(provider.name);
68
+ setApiKey(provider.apiKey);
69
+ setModel(provider.model ?? "");
70
+ setFormat(provider.format ?? "anthropic");
71
+ setBaseUrl(provider.baseUrl ?? "");
72
+ setManualBaseUrlOverride(false);
73
+ }
74
+ }, [provider]);
75
+
76
+ // Detect known provider presets and auto-fill URLs
77
+ useEffect(() => {
78
+ const lowerName = name.toLowerCase();
79
+ for (const [keyword, preset] of Object.entries(KNOWN_PROVIDER_PRESETS)) {
80
+ if (lowerName.includes(keyword)) {
81
+ if (!manualBaseUrlOverride) {
82
+ setFormat(preset.format);
83
+ setBaseUrl(preset.baseUrl);
84
+ }
85
+ // For MiniMax, auto-select the first model if not already set
86
+ if (keyword === "minimax" && !model) {
87
+ setModel(MINIMAX_MODELS[0] ?? "");
88
+ }
89
+ // For Alibaba, auto-select the first model if not already set
90
+ if (keyword === "alibaba" && !model) {
91
+ setModel(ALIBABA_MODELS[0] ?? "");
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ }, [name]);
97
+
98
+ function validate(): boolean {
99
+ const newErrors: Record<string, string> = {};
100
+ if (!name.trim()) {
101
+ newErrors.name = "Name is required";
102
+ }
103
+ if (!apiKey.trim()) {
104
+ newErrors.apiKey = "API key is required";
105
+ }
106
+ if (!model.trim()) {
107
+ newErrors.model = "Model is required";
108
+ }
109
+ if (!baseUrl.trim()) {
110
+ newErrors.baseUrl = "Base URL is required";
111
+ } else if (!isValidUrl(baseUrl.trim())) {
112
+ newErrors.baseUrl = "Invalid URL format";
113
+ }
114
+ setErrors(newErrors);
115
+ return Object.keys(newErrors).length === 0;
116
+ }
117
+
118
+ function isValidUrl(str: string): boolean {
119
+ try {
120
+ new URL(str);
121
+ return true;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function handleSubmit(e: React.FormEvent): void {
128
+ e.preventDefault();
129
+ if (!validate()) return;
130
+ setIsSubmitting(true);
131
+ try {
132
+ onSubmit({
133
+ name: name.trim(),
134
+ apiKey: apiKey.trim(),
135
+ model: model.trim() || undefined,
136
+ format,
137
+ baseUrl: baseUrl.trim() || undefined,
138
+ });
139
+ } finally {
140
+ setIsSubmitting(false);
141
+ }
142
+ }
143
+
144
+ return (
145
+ <form onSubmit={handleSubmit} className="space-y-4">
146
+ <div className="space-y-2">
147
+ <label htmlFor="provider-name" className="text-sm font-medium">
148
+ Name <span className="text-destructive">*</span>
149
+ </label>
150
+ <input
151
+ id="provider-name"
152
+ type="text"
153
+ value={name}
154
+ onChange={(e) => setName(e.target.value)}
155
+ placeholder="Model Name"
156
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
157
+ />
158
+ {errors.name !== undefined && <p className="text-xs text-destructive">{errors.name}</p>}
159
+ </div>
160
+
161
+ <div className="space-y-2">
162
+ <label htmlFor="provider-apikey" className="text-sm font-medium">
163
+ API Key <span className="text-destructive">*</span>
164
+ </label>
165
+ <input
166
+ id="provider-apikey"
167
+ type="password"
168
+ value={apiKey}
169
+ onChange={(e) => setApiKey(e.target.value)}
170
+ placeholder="sk-ant-..."
171
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
172
+ />
173
+ {errors.apiKey !== undefined && <p className="text-xs text-destructive">{errors.apiKey}</p>}
174
+ </div>
175
+
176
+ <div className="space-y-2">
177
+ <label htmlFor="provider-model" className="text-sm font-medium">
178
+ Model <span className="text-destructive">*</span>
179
+ </label>
180
+ {isMiniMax || isAlibaba ? (
181
+ <select
182
+ id="provider-model"
183
+ value={model}
184
+ onChange={(e) => setModel(e.target.value)}
185
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
186
+ >
187
+ {(isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((m) => (
188
+ <option key={m} value={m}>
189
+ {m}
190
+ </option>
191
+ ))}
192
+ </select>
193
+ ) : (
194
+ <input
195
+ id="provider-model"
196
+ type="text"
197
+ value={model}
198
+ onChange={(e) => setModel(e.target.value)}
199
+ placeholder=""
200
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
201
+ />
202
+ )}
203
+ {errors.model !== undefined && <p className="text-xs text-destructive">{errors.model}</p>}
204
+ </div>
205
+
206
+ <div className="space-y-2">
207
+ <label htmlFor="provider-format" className="text-sm font-medium">
208
+ API Format <span className="text-destructive">*</span>
209
+ </label>
210
+ <select
211
+ id="provider-format"
212
+ value={format}
213
+ onChange={(e) => setFormat(e.target.value === "anthropic" ? "anthropic" : "openai")}
214
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
215
+ >
216
+ <option value="anthropic">Anthropic (/v1/messages)</option>
217
+ <option value="openai">OpenAI (/v1/chat/completions)</option>
218
+ </select>
219
+ <p className="text-xs text-muted-foreground">
220
+ {format === "anthropic"
221
+ ? "Use Anthropic format for /v1/messages endpoint"
222
+ : "Use OpenAI format for /v1/chat/completions endpoint"}
223
+ </p>
224
+ </div>
225
+
226
+ <div className="space-y-2">
227
+ <label htmlFor="provider-base-url" className="text-sm font-medium">
228
+ Base URL <span className="text-destructive">*</span>
229
+ </label>
230
+ <input
231
+ id="provider-base-url"
232
+ type="text"
233
+ value={baseUrl}
234
+ onChange={(e) => {
235
+ setManualBaseUrlOverride(true);
236
+ setBaseUrl(e.target.value);
237
+ }}
238
+ placeholder={
239
+ format === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1"
240
+ }
241
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
242
+ />
243
+ {errors.baseUrl !== undefined && (
244
+ <p className="text-xs text-destructive">{errors.baseUrl}</p>
245
+ )}
246
+ <p className="text-xs text-muted-foreground">Base URL for the provider API.</p>
247
+ </div>
248
+
249
+ <div className="flex gap-2 justify-end pt-2">
250
+ <Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
251
+ Cancel
252
+ </Button>
253
+ <Button type="submit" disabled={isSubmitting}>
254
+ {isSubmitting ? "Saving..." : provider ? "Update Provider" : "Add Provider"}
255
+ </Button>
256
+ </div>
257
+ </form>
258
+ );
259
+ }