@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,70 @@
1
+ import type { FormatHandler, ParsedRequest } from "../handler";
2
+ import type { CapturedLog, TokenUsage } from "../../schemas";
3
+ import { OpenAIRequestSchema, OpenAIResponseSchema, parseOpenAIResponse } from "./schemas";
4
+ import { extractOpenAIStream } from "./stream";
5
+
6
+ export const OpenAIFormatHandler: FormatHandler = {
7
+ format: "openai",
8
+
9
+ parseRequest(rawBody: string, headers?: Headers): ParsedRequest | null {
10
+ try {
11
+ const json: unknown = JSON.parse(rawBody);
12
+ const result = OpenAIRequestSchema.safeParse(json);
13
+ if (result.success) {
14
+ return {
15
+ model: result.data.model,
16
+ sessionId: headers?.get("x-session-affinity") ?? null,
17
+ };
18
+ }
19
+ return null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ },
24
+
25
+ extractTokens(responseBody: string): TokenUsage {
26
+ const parsed = parseOpenAIResponse(responseBody);
27
+ if (parsed) {
28
+ return {
29
+ inputTokens: parsed.usage.prompt_tokens,
30
+ outputTokens: parsed.usage.completion_tokens,
31
+ cacheCreationInputTokens: null,
32
+ cacheReadInputTokens: null,
33
+ };
34
+ }
35
+ return {
36
+ inputTokens: null,
37
+ outputTokens: null,
38
+ cacheCreationInputTokens: null,
39
+ cacheReadInputTokens: null,
40
+ };
41
+ },
42
+
43
+ extractStream(
44
+ raw: string,
45
+ log: CapturedLog,
46
+ fallbackModel?: string,
47
+ collectChunks?: boolean,
48
+ ): string {
49
+ return extractOpenAIStream(raw, log, fallbackModel, collectChunks);
50
+ },
51
+
52
+ detectFormat(rawBody: string | null): boolean {
53
+ if (rawBody === null) return false;
54
+ try {
55
+ const json: unknown = JSON.parse(rawBody);
56
+ if (typeof json === "object" && json !== null && !Array.isArray(json)) {
57
+ const keys = Object.keys(json);
58
+ if (keys.includes("model") && keys.includes("messages")) {
59
+ // OpenAI doesn't use "system" or "tools" keys at root level
60
+ if (!keys.includes("system") && !keys.includes("tools")) {
61
+ return true;
62
+ }
63
+ }
64
+ }
65
+ return false;
66
+ } catch {
67
+ return false;
68
+ }
69
+ },
70
+ };
@@ -0,0 +1,25 @@
1
+ // Register format handler
2
+ import "./register";
3
+
4
+ // Schemas
5
+ export {
6
+ OpenAIRequestSchema,
7
+ OpenAIResponseSchema,
8
+ OpenAISSERawChunkSchema,
9
+ OpenAIMessage,
10
+ OpenAIMessageContent,
11
+ OpenAIToolDefinition,
12
+ OpenAIChoice,
13
+ OpenAIChoiceDelta,
14
+ parseOpenAIResponse,
15
+ } from "./schemas";
16
+
17
+ // Handler
18
+ export { OpenAIFormatHandler } from "./handler";
19
+
20
+ // Stream extraction
21
+ export { extractOpenAIStream } from "./stream";
22
+
23
+ // Providers
24
+ export { openaiProvider } from "./provider";
25
+ export { alibabaProvider } from "./alibabaProvider";
@@ -0,0 +1,50 @@
1
+ import type { CapturedLog } from "../../schemas";
2
+ import type { ProviderProtocol } from "../protocol";
3
+ import { registry } from "../providers";
4
+ import { extractOpenAIStream } from "./stream";
5
+
6
+ const DEFAULT_OPENAI_UPSTREAM = "https://api.openai.com/v1";
7
+
8
+ /**
9
+ * OpenAI Provider Implementation
10
+ */
11
+ export const openaiProvider: ProviderProtocol = {
12
+ name: "openai",
13
+
14
+ matches(model: string): boolean {
15
+ const modelLower = model.toLowerCase();
16
+ // Match OpenAI models
17
+ if (modelLower.startsWith("openai-")) {
18
+ return true;
19
+ }
20
+ // Also match common OpenAI model names
21
+ if (
22
+ modelLower.includes("gpt-") ||
23
+ modelLower.includes("o1") ||
24
+ modelLower.includes("o2") ||
25
+ modelLower.includes("o3")
26
+ ) {
27
+ return true;
28
+ }
29
+ return false;
30
+ },
31
+
32
+ getUpstreamBase(_isChatCompletions: boolean, providerConfig?: { baseUrl?: string }): string {
33
+ if (providerConfig?.baseUrl !== undefined) {
34
+ return providerConfig.baseUrl;
35
+ }
36
+ return DEFAULT_OPENAI_UPSTREAM;
37
+ },
38
+
39
+ extractStream(
40
+ raw: string,
41
+ log: CapturedLog,
42
+ fallbackModel?: string,
43
+ collectChunks?: boolean,
44
+ ): string {
45
+ return extractOpenAIStream(raw, log, fallbackModel, collectChunks);
46
+ },
47
+ };
48
+
49
+ // Register the provider
50
+ registry.register(openaiProvider);
@@ -0,0 +1,4 @@
1
+ import { formatRegistry } from "../registry";
2
+ import { OpenAIFormatHandler } from "./handler";
3
+
4
+ formatRegistry.register(OpenAIFormatHandler);
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+ import { JsonValueSchema } from "../jsonSchema";
3
+
4
+ // OpenAI Chat Completions Schemas
5
+
6
+ export const OpenAIMessageContent = z.union([
7
+ z.string(),
8
+ z.array(
9
+ z.discriminatedUnion("type", [
10
+ z.object({ type: z.literal("text"), text: z.string() }),
11
+ z.object({
12
+ type: z.literal("image_url"),
13
+ image_url: z.object({ url: z.string(), detail: z.string().optional() }),
14
+ }),
15
+ ]),
16
+ ),
17
+ ]);
18
+
19
+ export const OpenAIMessage = z.object({
20
+ role: z.enum(["system", "user", "assistant", "tool"]),
21
+ content: OpenAIMessageContent,
22
+ name: z.string().optional(),
23
+ reasoning_content: z.string().optional(),
24
+ });
25
+
26
+ export const OpenAIFunctionCall = z.object({
27
+ name: z.string(),
28
+ arguments: z.string(),
29
+ });
30
+
31
+ const OpenAIMessageWithFunctionCall = OpenAIMessage.extend({
32
+ content: z
33
+ .union([z.string(), z.array(z.object({ type: z.literal("text"), text: z.string() }))])
34
+ .optional(),
35
+ function_call: OpenAIFunctionCall.optional(),
36
+ });
37
+
38
+ export const OpenAIToolDefinition = z.object({
39
+ type: z.literal("function"),
40
+ function: z.object({
41
+ name: z.string(),
42
+ description: z.string().optional(),
43
+ parameters: z.record(z.string(), JsonValueSchema),
44
+ }),
45
+ });
46
+
47
+ export const OpenAIRequestSchema = z.object({
48
+ model: z.string(),
49
+ messages: z.array(OpenAIMessage),
50
+ temperature: z.number().optional(),
51
+ max_tokens: z.number().optional(),
52
+ stream: z.boolean().optional(),
53
+ tools: z.array(OpenAIToolDefinition).optional(),
54
+ tool_choice: z
55
+ .union([
56
+ z.object({ type: z.literal("auto") }),
57
+ z.object({ type: z.literal("none") }),
58
+ z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) }),
59
+ ])
60
+ .optional(),
61
+ user: z.string().optional(),
62
+ });
63
+
64
+ export const OpenAIChoiceDelta = z.object({
65
+ role: z.enum(["assistant"]).optional(),
66
+ content: z.string().nullable().optional(),
67
+ reasoning_content: z.string().nullable().optional(),
68
+ function_call: z
69
+ .object({ name: z.string().optional(), arguments: z.string().optional() })
70
+ .optional(),
71
+ tool_calls: z
72
+ .array(
73
+ z.object({
74
+ index: z.number(),
75
+ id: z.string().optional(),
76
+ type: z.literal("function").optional(),
77
+ function: z.object({
78
+ name: z.string().optional(),
79
+ arguments: z.string().optional(),
80
+ }),
81
+ }),
82
+ )
83
+ .optional(),
84
+ });
85
+
86
+ export const OpenAIChoice = z.object({
87
+ index: z.number(),
88
+ message: z
89
+ .object({
90
+ role: z.enum(["assistant"]),
91
+ content: z.string().nullable(),
92
+ reasoning_content: z.string().optional(),
93
+ function_call: z.object({ name: z.string(), arguments: z.string() }).optional(),
94
+ })
95
+ .optional(),
96
+ delta: OpenAIChoiceDelta.optional(),
97
+ finish_reason: z.string().nullable(),
98
+ });
99
+
100
+ export const OpenAIResponseSchema = z.object({
101
+ id: z.string(),
102
+ object: z.literal("chat.completion"),
103
+ created: z.number(),
104
+ model: z.string(),
105
+ choices: z.array(OpenAIChoice),
106
+ usage: z.object({
107
+ prompt_tokens: z.number(),
108
+ completion_tokens: z.number(),
109
+ total_tokens: z.number(),
110
+ }),
111
+ });
112
+
113
+ // Schema for actual OpenAI SSE chunk format (not wrapped in event/data)
114
+ export const OpenAISSERawChunkSchema = z.object({
115
+ id: z.string(),
116
+ object: z.literal("chat.completion.chunk"),
117
+ created: z.number(),
118
+ model: z.string(),
119
+ choices: z.array(
120
+ z.object({
121
+ index: z.number(),
122
+ delta: OpenAIChoiceDelta,
123
+ finish_reason: z.string().nullable().optional(),
124
+ }),
125
+ ),
126
+ usage: z
127
+ .object({
128
+ prompt_tokens: z.number(),
129
+ completion_tokens: z.number(),
130
+ total_tokens: z.number(),
131
+ })
132
+ .nullable()
133
+ .optional(),
134
+ });
135
+
136
+ export type OpenAIResponse = z.infer<typeof OpenAIResponseSchema>;
137
+
138
+ /**
139
+ * Parse an OpenAI response body.
140
+ */
141
+ export function parseOpenAIResponse(rawBody: string): OpenAIResponse | null {
142
+ try {
143
+ const json: unknown = JSON.parse(rawBody);
144
+ const result = OpenAIResponseSchema.safeParse(json);
145
+ if (result.success) return result.data;
146
+ return null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
@@ -0,0 +1,153 @@
1
+ import type { CapturedLog, JsonValue } from "../../schemas";
2
+ import { JsonValueSchema } from "../jsonSchema";
3
+ import { OpenAISSERawChunkSchema } from "./schemas";
4
+
5
+ /**
6
+ * Extract OpenAI streaming response and return normalized JSON.
7
+ * Optionally collects SSE chunks into log.streamingChunks for debugging/display.
8
+ */
9
+ export function extractOpenAIStream(
10
+ raw: string,
11
+ log: CapturedLog,
12
+ fallbackModel?: string,
13
+ collectChunks: boolean = true,
14
+ ): string {
15
+ let id = "";
16
+ let model = "";
17
+ let completionContent = "";
18
+ let reasoningContent = "";
19
+ let finishReason: string | null = null;
20
+ let promptTokens = 0;
21
+ let completionTokens = 0;
22
+ let usageCaptured = false;
23
+ let started = false;
24
+ const toolCalls: Array<{
25
+ index: number;
26
+ id?: string;
27
+ type: "function";
28
+ function: { name: string; arguments: string };
29
+ }> = [];
30
+
31
+ // Chunk collection state
32
+ const MAX_CHUNKS = 1000;
33
+ let chunkIndex = 0;
34
+ let streamStartMs = 0;
35
+ const chunks: Array<{ index: number; timestamp: number; type: string; data: JsonValue }> = [];
36
+
37
+ for (const line of raw.split("\n")) {
38
+ if (!line.startsWith("data: ")) continue;
39
+ const dataStr = line.slice(6).trim();
40
+ if (dataStr === "[DONE]") break;
41
+
42
+ try {
43
+ const parsed: unknown = JSON.parse(dataStr);
44
+ const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
45
+ if (!chunkResult.success) continue;
46
+ const chunk = chunkResult.data;
47
+
48
+ // Track stream start for timestamps
49
+ if (chunkIndex === 0) streamStartMs = Date.now();
50
+
51
+ // Collect chunks if enabled
52
+ if (collectChunks === true && chunks.length < MAX_CHUNKS) {
53
+ const jsonResult = JsonValueSchema.safeParse(chunk);
54
+ if (jsonResult.success) {
55
+ chunks.push({
56
+ index: chunkIndex,
57
+ timestamp: Date.now() - streamStartMs,
58
+ type: "chat.completion.chunk",
59
+ data: jsonResult.data,
60
+ });
61
+ }
62
+ }
63
+ chunkIndex++;
64
+
65
+ if (!started) {
66
+ id = chunk.id;
67
+ model = chunk.model;
68
+ started = true;
69
+ }
70
+
71
+ if (!usageCaptured && chunk.usage !== undefined && chunk.usage !== null) {
72
+ promptTokens = chunk.usage.prompt_tokens;
73
+ completionTokens = chunk.usage.completion_tokens;
74
+ log.inputTokens = promptTokens;
75
+ usageCaptured = true;
76
+ }
77
+
78
+ for (const choice of chunk.choices) {
79
+ const delta = choice.delta;
80
+ if (delta.content !== undefined && delta.content !== null) {
81
+ completionContent += delta.content;
82
+ }
83
+ if (delta.reasoning_content !== undefined && delta.reasoning_content !== null) {
84
+ reasoningContent += delta.reasoning_content;
85
+ }
86
+ if (choice.finish_reason !== undefined && choice.finish_reason !== null) {
87
+ finishReason = choice.finish_reason;
88
+ }
89
+ // Accumulate tool_calls
90
+ if (delta.tool_calls !== undefined && delta.tool_calls !== null) {
91
+ for (const tc of delta.tool_calls) {
92
+ // Find or create tool call entry by index
93
+ let existing = toolCalls.find((t) => t.index === tc.index);
94
+ if (!existing) {
95
+ existing = {
96
+ index: tc.index,
97
+ type: "function",
98
+ function: { name: "", arguments: "" },
99
+ };
100
+ toolCalls.push(existing);
101
+ }
102
+ if (tc.id !== undefined) existing.id = tc.id;
103
+ if (tc.function?.name !== undefined) existing.function.name += tc.function.name;
104
+ if (tc.function?.arguments !== undefined)
105
+ existing.function.arguments += tc.function.arguments;
106
+ }
107
+ }
108
+ }
109
+ } catch {
110
+ // non-JSON SSE line, skip
111
+ }
112
+ }
113
+
114
+ // Store collected chunks on the log
115
+ if (collectChunks === true) {
116
+ log.streamingChunks = {
117
+ chunks,
118
+ truncated: chunkIndex > MAX_CHUNKS,
119
+ };
120
+ }
121
+
122
+ if (!usageCaptured) {
123
+ completionTokens = Math.ceil(completionContent.length / 4);
124
+ }
125
+
126
+ log.outputTokens = completionTokens;
127
+
128
+ const message: Record<string, unknown> = {
129
+ role: "assistant",
130
+ content: completionContent,
131
+ };
132
+ if (reasoningContent) message.reasoning_content = reasoningContent;
133
+ if (toolCalls.length > 0) message.tool_calls = toolCalls;
134
+
135
+ return JSON.stringify({
136
+ id,
137
+ object: "chat.completion",
138
+ created: Math.floor(Date.now() / 1000),
139
+ model: model !== "" ? model : (fallbackModel ?? ""),
140
+ choices: [
141
+ {
142
+ index: 0,
143
+ message,
144
+ finish_reason: finishReason,
145
+ },
146
+ ],
147
+ usage: {
148
+ prompt_tokens: promptTokens,
149
+ completion_tokens: completionTokens,
150
+ total_tokens: promptTokens + completionTokens,
151
+ },
152
+ });
153
+ }
@@ -0,0 +1,50 @@
1
+ import type { CapturedLog, RequestFormat } from "../schemas";
2
+
3
+ /**
4
+ * Provider Protocol Interface
5
+ *
6
+ * Defines the contract that all providers must implement to support
7
+ * different LLM APIs (Anthropic, OpenAI, etc.).
8
+ */
9
+ export type ProviderProtocol = {
10
+ /** Provider identifier (e.g., "anthropic", "openai") */
11
+ name: string;
12
+
13
+ /**
14
+ * Check if this provider handles the given model.
15
+ * @param model - Model name from request
16
+ * @returns true if this provider should handle the model
17
+ */
18
+ matches(model: string): boolean;
19
+
20
+ /**
21
+ * Get the upstream base URL for this provider.
22
+ * @param isChatCompletions - Whether the request uses OpenAI chat completions format
23
+ * @param providerConfig - Optional provider configuration for custom base URL
24
+ * @returns Upstream base URL
25
+ */
26
+ getUpstreamBase(isChatCompletions: boolean, providerConfig?: { baseUrl?: string }): string;
27
+
28
+ /**
29
+ * Extract structured response from streaming data.
30
+ * @param raw - Raw SSE stream data
31
+ * @param log - Captured log entry to update
32
+ * @param fallbackModel - Fallback model name if not in stream
33
+ * @param collectChunks - Whether to collect SSE chunks into log.streamingChunks
34
+ * @returns JSON string of structured response
35
+ */
36
+ extractStream(
37
+ raw: string,
38
+ log: CapturedLog,
39
+ fallbackModel?: string,
40
+ collectChunks?: boolean,
41
+ ): string;
42
+
43
+ /**
44
+ * Optional request transformation hook.
45
+ * @param body - Original request body
46
+ * @param apiFormat - Request format (anthropic/openai/unknown)
47
+ * @returns Transformed body or null to use original
48
+ */
49
+ transformRequest?(body: string, apiFormat: RequestFormat): string | null;
50
+ };
@@ -0,0 +1,51 @@
1
+ import type { ProviderProtocol } from "./protocol";
2
+
3
+ /**
4
+ * Provider Registry
5
+ *
6
+ * Singleton registry for provider implementations.
7
+ * Providers register themselves at module load time.
8
+ */
9
+ class ProviderRegistryImpl {
10
+ private providers: ProviderProtocol[] = [];
11
+
12
+ /**
13
+ * Register a provider with the registry.
14
+ * @param provider - Provider implementation to register
15
+ */
16
+ register(provider: ProviderProtocol): void {
17
+ this.providers.push(provider);
18
+ }
19
+
20
+ /**
21
+ * Find the first provider that matches the given model.
22
+ * @param model - Model name to match
23
+ * @returns Matching provider or null if none found
24
+ */
25
+ findProvider(model: string): ProviderProtocol | null {
26
+ let fallback: ProviderProtocol | null = null;
27
+
28
+ for (const provider of this.providers) {
29
+ if (provider.matches(model)) {
30
+ // Non-anthropic providers win immediately (explicit match)
31
+ if (provider.name !== "anthropic") {
32
+ return provider;
33
+ }
34
+ // Anthropic is the fallback — only use if nothing else matches
35
+ fallback = provider;
36
+ }
37
+ }
38
+ return fallback;
39
+ }
40
+
41
+ /**
42
+ * Get all registered providers.
43
+ * @returns Array of all registered providers
44
+ */
45
+ getAll(): ProviderProtocol[] {
46
+ return [...this.providers];
47
+ }
48
+ }
49
+
50
+ /** Global registry instance */
51
+ export const registry = new ProviderRegistryImpl();
@@ -0,0 +1,3 @@
1
+ // Provider types and registry
2
+ export type { ProviderProtocol } from "../protocol";
3
+ export { registry } from "../providerRegistry";
@@ -0,0 +1,61 @@
1
+ import type { FormatHandler } from "./handler";
2
+ import type { RequestFormat } from "../schemas";
3
+ import { PATH_V1_CHAT_COMPLETIONS, PATH_CHAT_COMPLETIONS, PATH_V1_MESSAGES } from "../constants";
4
+
5
+ /**
6
+ * Format registry — maps request paths and formats to their FormatHandler.
7
+ *
8
+ * Registered at module load time by each format's init module.
9
+ */
10
+ class FormatRegistryImpl {
11
+ private handlers = new Map<RequestFormat, FormatHandler>();
12
+ private pathMap = new Map<string, RequestFormat>();
13
+
14
+ register(handler: FormatHandler): void {
15
+ this.handlers.set(handler.format, handler);
16
+ }
17
+
18
+ registerPath(path: string, format: RequestFormat): void {
19
+ this.pathMap.set(path, format);
20
+ }
21
+
22
+ /** Get handler by format identifier */
23
+ get(format: RequestFormat): FormatHandler | undefined {
24
+ return this.handlers.get(format);
25
+ }
26
+
27
+ /** Get handler matching a request path */
28
+ getByPath(path: string): FormatHandler | undefined {
29
+ const messagesPath = path.split("?")[0] ?? "";
30
+ // DeepSeek /v1/messages is chat completions replay
31
+ if (messagesPath === PATH_V1_MESSAGES) return this.handlers.get("anthropic");
32
+ if (messagesPath === PATH_V1_CHAT_COMPLETIONS || messagesPath === PATH_CHAT_COMPLETIONS) {
33
+ return this.handlers.get("openai");
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /** Detect format from request body content */
39
+ detectFormat(rawBody: string | null): RequestFormat {
40
+ if (rawBody === null) return "unknown";
41
+ for (const handler of this.handlers.values()) {
42
+ if (handler.detectFormat(rawBody)) return handler.format;
43
+ }
44
+ return "unknown";
45
+ }
46
+ }
47
+
48
+ export const formatRegistry = new FormatRegistryImpl();
49
+
50
+ /**
51
+ * Get the FormatHandler for a request path.
52
+ * Returns null if no format matches the path.
53
+ */
54
+ export function formatForPath(path: string): FormatHandler | null {
55
+ return formatRegistry.getByPath(path) ?? null;
56
+ }
57
+
58
+ // Register known paths
59
+ formatRegistry.registerPath(PATH_V1_MESSAGES, "anthropic");
60
+ formatRegistry.registerPath(PATH_V1_CHAT_COMPLETIONS, "openai");
61
+ formatRegistry.registerPath(PATH_CHAT_COMPLETIONS, "openai");