svedocs 0.1.0-beta.10

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/LICENSE +5 -0
  2. package/README.md +85 -0
  3. package/dist/ai/openai-compatible.d.ts +13 -0
  4. package/dist/ai/openai-compatible.d.ts.map +1 -0
  5. package/dist/ai/openai-compatible.js +92 -0
  6. package/dist/ai/providers.d.ts +20 -0
  7. package/dist/ai/providers.d.ts.map +1 -0
  8. package/dist/ai/providers.js +185 -0
  9. package/dist/ai/rate-limit.d.ts +18 -0
  10. package/dist/ai/rate-limit.d.ts.map +1 -0
  11. package/dist/ai/rate-limit.js +52 -0
  12. package/dist/ai/response.d.ts +4 -0
  13. package/dist/ai/response.d.ts.map +1 -0
  14. package/dist/ai/response.js +151 -0
  15. package/dist/ai/runtime.d.ts +32 -0
  16. package/dist/ai/runtime.d.ts.map +1 -0
  17. package/dist/ai/runtime.js +111 -0
  18. package/dist/ai/types.d.ts +93 -0
  19. package/dist/ai/types.d.ts.map +1 -0
  20. package/dist/ai/types.js +1 -0
  21. package/dist/ai.d.ts +7 -0
  22. package/dist/ai.d.ts.map +1 -0
  23. package/dist/ai.js +6 -0
  24. package/dist/ambient.d.ts +5 -0
  25. package/dist/cloudflare.d.ts +29 -0
  26. package/dist/cloudflare.d.ts.map +1 -0
  27. package/dist/cloudflare.js +149 -0
  28. package/dist/config.d.ts +373 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +299 -0
  31. package/dist/core/checks.d.ts +4 -0
  32. package/dist/core/checks.d.ts.map +1 -0
  33. package/dist/core/checks.js +267 -0
  34. package/dist/core/config.d.ts +6 -0
  35. package/dist/core/config.d.ts.map +1 -0
  36. package/dist/core/config.js +178 -0
  37. package/dist/core/content.d.ts +7 -0
  38. package/dist/core/content.d.ts.map +1 -0
  39. package/dist/core/content.js +340 -0
  40. package/dist/core/i18n.d.ts +5 -0
  41. package/dist/core/i18n.d.ts.map +1 -0
  42. package/dist/core/i18n.js +117 -0
  43. package/dist/core/links.d.ts +5 -0
  44. package/dist/core/links.d.ts.map +1 -0
  45. package/dist/core/links.js +60 -0
  46. package/dist/core/messages.d.ts +149 -0
  47. package/dist/core/messages.d.ts.map +1 -0
  48. package/dist/core/messages.js +163 -0
  49. package/dist/core/navigation.d.ts +5 -0
  50. package/dist/core/navigation.d.ts.map +1 -0
  51. package/dist/core/navigation.js +144 -0
  52. package/dist/core/routes.d.ts +36 -0
  53. package/dist/core/routes.d.ts.map +1 -0
  54. package/dist/core/routes.js +152 -0
  55. package/dist/core/search.d.ts +5 -0
  56. package/dist/core/search.d.ts.map +1 -0
  57. package/dist/core/search.js +41 -0
  58. package/dist/core/types.d.ts +468 -0
  59. package/dist/core/types.d.ts.map +1 -0
  60. package/dist/core/types.js +4 -0
  61. package/dist/core/utils.d.ts +13 -0
  62. package/dist/core/utils.d.ts.map +1 -0
  63. package/dist/core/utils.js +60 -0
  64. package/dist/core.d.ts +9 -0
  65. package/dist/core.d.ts.map +1 -0
  66. package/dist/core.js +8 -0
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +3 -0
  70. package/dist/mdx/ast.d.ts +32 -0
  71. package/dist/mdx/ast.d.ts.map +1 -0
  72. package/dist/mdx/ast.js +148 -0
  73. package/dist/mdx/code.d.ts +17 -0
  74. package/dist/mdx/code.d.ts.map +1 -0
  75. package/dist/mdx/code.js +327 -0
  76. package/dist/mdx/compile.d.ts +29 -0
  77. package/dist/mdx/compile.d.ts.map +1 -0
  78. package/dist/mdx/compile.js +59 -0
  79. package/dist/mdx/diff.d.ts +9 -0
  80. package/dist/mdx/diff.d.ts.map +1 -0
  81. package/dist/mdx/diff.js +171 -0
  82. package/dist/mdx/headings.d.ts +4 -0
  83. package/dist/mdx/headings.d.ts.map +1 -0
  84. package/dist/mdx/headings.js +42 -0
  85. package/dist/mdx/links.d.ts +4 -0
  86. package/dist/mdx/links.d.ts.map +1 -0
  87. package/dist/mdx/links.js +144 -0
  88. package/dist/mdx/utils.d.ts +10 -0
  89. package/dist/mdx/utils.d.ts.map +1 -0
  90. package/dist/mdx/utils.js +64 -0
  91. package/dist/og/image.d.ts +25 -0
  92. package/dist/og/image.d.ts.map +1 -0
  93. package/dist/og/image.js +224 -0
  94. package/dist/og/metadata.d.ts +7 -0
  95. package/dist/og/metadata.d.ts.map +1 -0
  96. package/dist/og/metadata.js +198 -0
  97. package/dist/og/response.d.ts +4 -0
  98. package/dist/og/response.d.ts.map +1 -0
  99. package/dist/og/response.js +49 -0
  100. package/dist/og/rss.d.ts +4 -0
  101. package/dist/og/rss.d.ts.map +1 -0
  102. package/dist/og/rss.js +96 -0
  103. package/dist/og/sitemap.d.ts +7 -0
  104. package/dist/og/sitemap.d.ts.map +1 -0
  105. package/dist/og/sitemap.js +90 -0
  106. package/dist/og/types.d.ts +65 -0
  107. package/dist/og/types.d.ts.map +1 -0
  108. package/dist/og/types.js +1 -0
  109. package/dist/og.d.ts +6 -0
  110. package/dist/og.d.ts.map +1 -0
  111. package/dist/og.js +5 -0
  112. package/dist/routes.d.ts +2 -0
  113. package/dist/routes.d.ts.map +1 -0
  114. package/dist/routes.js +1 -0
  115. package/dist/search/algolia.d.ts +10 -0
  116. package/dist/search/algolia.d.ts.map +1 -0
  117. package/dist/search/algolia.js +82 -0
  118. package/dist/search/cloudflare-sync.d.ts +5 -0
  119. package/dist/search/cloudflare-sync.d.ts.map +1 -0
  120. package/dist/search/cloudflare-sync.js +169 -0
  121. package/dist/search/cloudflare.d.ts +11 -0
  122. package/dist/search/cloudflare.d.ts.map +1 -0
  123. package/dist/search/cloudflare.js +126 -0
  124. package/dist/search/local.d.ts +9 -0
  125. package/dist/search/local.d.ts.map +1 -0
  126. package/dist/search/local.js +165 -0
  127. package/dist/search/runtime.d.ts +41 -0
  128. package/dist/search/runtime.d.ts.map +1 -0
  129. package/dist/search/runtime.js +152 -0
  130. package/dist/search/types.d.ts +172 -0
  131. package/dist/search/types.d.ts.map +1 -0
  132. package/dist/search/types.js +1 -0
  133. package/dist/search/typesense.d.ts +11 -0
  134. package/dist/search/typesense.d.ts.map +1 -0
  135. package/dist/search/typesense.js +91 -0
  136. package/dist/search/utils.d.ts +11 -0
  137. package/dist/search/utils.d.ts.map +1 -0
  138. package/dist/search/utils.js +93 -0
  139. package/dist/search.d.ts +8 -0
  140. package/dist/search.d.ts.map +1 -0
  141. package/dist/search.js +7 -0
  142. package/dist/svelte.d.ts +27 -0
  143. package/dist/svelte.d.ts.map +1 -0
  144. package/dist/svelte.js +45 -0
  145. package/dist/testing.d.ts +3 -0
  146. package/dist/testing.d.ts.map +1 -0
  147. package/dist/testing.js +23 -0
  148. package/dist/theme/Article.svelte +145 -0
  149. package/dist/theme/Article.svelte.d.ts +36 -0
  150. package/dist/theme/Article.svelte.d.ts.map +1 -0
  151. package/dist/theme/AskAiPanel.svelte +241 -0
  152. package/dist/theme/AskAiPanel.svelte.d.ts +31 -0
  153. package/dist/theme/AskAiPanel.svelte.d.ts.map +1 -0
  154. package/dist/theme/Brand.svelte +19 -0
  155. package/dist/theme/Brand.svelte.d.ts +22 -0
  156. package/dist/theme/Brand.svelte.d.ts.map +1 -0
  157. package/dist/theme/Button.svelte +34 -0
  158. package/dist/theme/Button.svelte.d.ts +41 -0
  159. package/dist/theme/Button.svelte.d.ts.map +1 -0
  160. package/dist/theme/Checkbox.svelte +63 -0
  161. package/dist/theme/Checkbox.svelte.d.ts +45 -0
  162. package/dist/theme/Checkbox.svelte.d.ts.map +1 -0
  163. package/dist/theme/CodeBlock.svelte +6 -0
  164. package/dist/theme/CodeBlock.svelte.d.ts +22 -0
  165. package/dist/theme/CodeBlock.svelte.d.ts.map +1 -0
  166. package/dist/theme/DocPage.svelte +15 -0
  167. package/dist/theme/DocPage.svelte.d.ts +33 -0
  168. package/dist/theme/DocPage.svelte.d.ts.map +1 -0
  169. package/dist/theme/DocsApp.svelte +194 -0
  170. package/dist/theme/DocsApp.svelte.d.ts +60 -0
  171. package/dist/theme/DocsApp.svelte.d.ts.map +1 -0
  172. package/dist/theme/DocsLayout.svelte +99 -0
  173. package/dist/theme/DocsLayout.svelte.d.ts +39 -0
  174. package/dist/theme/DocsLayout.svelte.d.ts.map +1 -0
  175. package/dist/theme/DocsShell.svelte +91 -0
  176. package/dist/theme/DocsShell.svelte.d.ts +34 -0
  177. package/dist/theme/DocsShell.svelte.d.ts.map +1 -0
  178. package/dist/theme/ErrorPage.svelte +113 -0
  179. package/dist/theme/ErrorPage.svelte.d.ts +34 -0
  180. package/dist/theme/ErrorPage.svelte.d.ts.map +1 -0
  181. package/dist/theme/FloatingToolbar.svelte +8 -0
  182. package/dist/theme/FloatingToolbar.svelte.d.ts +22 -0
  183. package/dist/theme/FloatingToolbar.svelte.d.ts.map +1 -0
  184. package/dist/theme/Footer.svelte +18 -0
  185. package/dist/theme/Footer.svelte.d.ts +24 -0
  186. package/dist/theme/Footer.svelte.d.ts.map +1 -0
  187. package/dist/theme/FooterLinks.svelte +37 -0
  188. package/dist/theme/FooterLinks.svelte.d.ts +22 -0
  189. package/dist/theme/FooterLinks.svelte.d.ts.map +1 -0
  190. package/dist/theme/FormField.svelte +41 -0
  191. package/dist/theme/FormField.svelte.d.ts +35 -0
  192. package/dist/theme/FormField.svelte.d.ts.map +1 -0
  193. package/dist/theme/HomePage.svelte +284 -0
  194. package/dist/theme/HomePage.svelte.d.ts +65 -0
  195. package/dist/theme/HomePage.svelte.d.ts.map +1 -0
  196. package/dist/theme/Input.svelte +36 -0
  197. package/dist/theme/Input.svelte.d.ts +34 -0
  198. package/dist/theme/Input.svelte.d.ts.map +1 -0
  199. package/dist/theme/LayoutShell.svelte +120 -0
  200. package/dist/theme/LayoutShell.svelte.d.ts +43 -0
  201. package/dist/theme/LayoutShell.svelte.d.ts.map +1 -0
  202. package/dist/theme/LocalizedLink.svelte +21 -0
  203. package/dist/theme/LocalizedLink.svelte.d.ts +38 -0
  204. package/dist/theme/LocalizedLink.svelte.d.ts.map +1 -0
  205. package/dist/theme/MobileNav.svelte +20 -0
  206. package/dist/theme/MobileNav.svelte.d.ts +26 -0
  207. package/dist/theme/MobileNav.svelte.d.ts.map +1 -0
  208. package/dist/theme/Navbar.svelte +69 -0
  209. package/dist/theme/Navbar.svelte.d.ts +29 -0
  210. package/dist/theme/Navbar.svelte.d.ts.map +1 -0
  211. package/dist/theme/PageLayout.svelte +49 -0
  212. package/dist/theme/PageLayout.svelte.d.ts +34 -0
  213. package/dist/theme/PageLayout.svelte.d.ts.map +1 -0
  214. package/dist/theme/PageShell.svelte +98 -0
  215. package/dist/theme/PageShell.svelte.d.ts +35 -0
  216. package/dist/theme/PageShell.svelte.d.ts.map +1 -0
  217. package/dist/theme/PageTools.svelte +87 -0
  218. package/dist/theme/PageTools.svelte.d.ts +25 -0
  219. package/dist/theme/PageTools.svelte.d.ts.map +1 -0
  220. package/dist/theme/RenderError.svelte +62 -0
  221. package/dist/theme/RenderError.svelte.d.ts +31 -0
  222. package/dist/theme/RenderError.svelte.d.ts.map +1 -0
  223. package/dist/theme/RootLayout.svelte +274 -0
  224. package/dist/theme/RootLayout.svelte.d.ts +47 -0
  225. package/dist/theme/RootLayout.svelte.d.ts.map +1 -0
  226. package/dist/theme/RouteRenderError.svelte +53 -0
  227. package/dist/theme/RouteRenderError.svelte.d.ts +34 -0
  228. package/dist/theme/RouteRenderError.svelte.d.ts.map +1 -0
  229. package/dist/theme/SafeRenderError.svelte +49 -0
  230. package/dist/theme/SafeRenderError.svelte.d.ts +33 -0
  231. package/dist/theme/SafeRenderError.svelte.d.ts.map +1 -0
  232. package/dist/theme/ScopeSwitcher.svelte +107 -0
  233. package/dist/theme/ScopeSwitcher.svelte.d.ts +26 -0
  234. package/dist/theme/ScopeSwitcher.svelte.d.ts.map +1 -0
  235. package/dist/theme/SearchDialog.svelte +200 -0
  236. package/dist/theme/SearchDialog.svelte.d.ts +31 -0
  237. package/dist/theme/SearchDialog.svelte.d.ts.map +1 -0
  238. package/dist/theme/Select.svelte +37 -0
  239. package/dist/theme/Select.svelte.d.ts +42 -0
  240. package/dist/theme/Select.svelte.d.ts.map +1 -0
  241. package/dist/theme/SidebarTree.svelte +148 -0
  242. package/dist/theme/SidebarTree.svelte.d.ts +24 -0
  243. package/dist/theme/SidebarTree.svelte.d.ts.map +1 -0
  244. package/dist/theme/SocialNav.svelte +17 -0
  245. package/dist/theme/SocialNav.svelte.d.ts +22 -0
  246. package/dist/theme/SocialNav.svelte.d.ts.map +1 -0
  247. package/dist/theme/TableOfContents.svelte +73 -0
  248. package/dist/theme/TableOfContents.svelte.d.ts +25 -0
  249. package/dist/theme/TableOfContents.svelte.d.ts.map +1 -0
  250. package/dist/theme/Textarea.svelte +33 -0
  251. package/dist/theme/Textarea.svelte.d.ts +33 -0
  252. package/dist/theme/Textarea.svelte.d.ts.map +1 -0
  253. package/dist/theme/ThemeInit.svelte +16 -0
  254. package/dist/theme/ThemeInit.svelte.d.ts +23 -0
  255. package/dist/theme/ThemeInit.svelte.d.ts.map +1 -0
  256. package/dist/theme/ThemeToggle.svelte +37 -0
  257. package/dist/theme/ThemeToggle.svelte.d.ts +23 -0
  258. package/dist/theme/ThemeToggle.svelte.d.ts.map +1 -0
  259. package/dist/theme/TopNav.svelte +23 -0
  260. package/dist/theme/TopNav.svelte.d.ts +22 -0
  261. package/dist/theme/TopNav.svelte.d.ts.map +1 -0
  262. package/dist/theme/base.css +240 -0
  263. package/dist/theme/defaults.d.ts +4 -0
  264. package/dist/theme/defaults.d.ts.map +1 -0
  265. package/dist/theme/defaults.js +55 -0
  266. package/dist/theme/forms.d.ts +4 -0
  267. package/dist/theme/forms.d.ts.map +1 -0
  268. package/dist/theme/forms.js +3 -0
  269. package/dist/theme/headless.d.ts +62 -0
  270. package/dist/theme/headless.d.ts.map +1 -0
  271. package/dist/theme/headless.js +1064 -0
  272. package/dist/theme/index.d.ts +41 -0
  273. package/dist/theme/index.d.ts.map +1 -0
  274. package/dist/theme/index.js +38 -0
  275. package/dist/theme/portal.d.ts +6 -0
  276. package/dist/theme/portal.d.ts.map +1 -0
  277. package/dist/theme/portal.js +59 -0
  278. package/dist/theme/styles.css +4232 -0
  279. package/dist/theme/types.d.ts +363 -0
  280. package/dist/theme/types.d.ts.map +1 -0
  281. package/dist/theme/types.js +1 -0
  282. package/dist/virtual.d.ts +65 -0
  283. package/dist/vite.d.ts +16 -0
  284. package/dist/vite.d.ts.map +1 -0
  285. package/dist/vite.js +386 -0
  286. package/package.json +132 -0
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 svedocs contributors
4
+
5
+ See the repository root license for the full text.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # svedocs
2
+
3
+ Integrated SvelteKit documentation framework package.
4
+
5
+ ## Exports
6
+
7
+ - `svedocs/config`: config schema and `defineConfig`.
8
+ - `svedocs/core`: content manifest, scoped navigation, checks, search records, and shared types.
9
+ - `svedocs/vite`: virtual modules and content refresh Vite plugin.
10
+ - `svedocs/theme`: default Svelte theme components, replaceable theme component map, and Tailwind CSS v4 styles.
11
+ - `svedocs/theme/headless`: unstyled theme behavior controllers for custom themes.
12
+ - `svedocs/theme/types`: public theme component prop and component-map types.
13
+ - `svedocs/svelte`: `mdsvex`-based Svelte-compatible authoring helpers.
14
+ - `svedocs/search`: weighted local search, scope filters, Cloudflare AI Search provider, and indexing sync.
15
+ - `svedocs/ai`: Ask AI providers, SSE responses, and rate limiting helpers.
16
+ - `svedocs/og`: SEO metadata, sitemap/robots, opt-in RSS, SVG/PNG/Satori OG generation.
17
+ - `svedocs/cloudflare`: build presets, wrangler config, and binding type helpers.
18
+
19
+ All rendering, theme, search, AI, SEO, OG, and Cloudflare capabilities are intentionally kept inside this package.
20
+
21
+ ## Theme Development
22
+
23
+ The default theme is optional and replaceable. Import the full bundled CSS when you want the standard look:
24
+
25
+ ```svelte
26
+ <script lang="ts">
27
+ import 'svedocs/theme/styles.css';
28
+ </script>
29
+ ```
30
+
31
+ Custom themes can import only `svedocs/theme/base.css` for reset, accessibility, prose, and code structure, or skip theme CSS entirely and own every style.
32
+
33
+ Register replacement components in the Vite plugin, not `svedocs.config.ts`, because Svelte component paths are build-time imports:
34
+
35
+ ```ts
36
+ import { svedocs } from 'svedocs/vite';
37
+ import svedocsConfig from './svedocs.config';
38
+
39
+ svedocs({
40
+ config: svedocsConfig,
41
+ theme: {
42
+ components: {
43
+ Navbar: '$lib/theme/Navbar.svelte',
44
+ Article: '$lib/theme/Article.svelte',
45
+ Search: '$lib/theme/Search.svelte',
46
+ AskAi: '$lib/theme/AskAi.svelte',
47
+ Error: '$lib/theme/Error.svelte'
48
+ }
49
+ }
50
+ });
51
+ ```
52
+
53
+ Generated routes import `virtual:svedocs/theme-components` and pass the map to `DocsApp`. You can replace broad page layouts such as `Root`, `Layout`, `Docs`, `Page`, `Home`, and `Error`, or smaller pieces such as `Navbar`, `Sidebar`, `Article`, `Toc`, `Search`, `AskAi`, `Footer`, `ThemeToggle`, `PageTools`, and `RenderError`.
54
+
55
+ Use `svedocs/theme/types` for stable props and `svedocs/theme/headless` for unstyled behavior controllers such as search, Ask AI, ToC tracking, theme mode, mobile nav, page tools, and code-copy behavior. Replacement page layouts should keep passing `pages`, `tree`, `search`, `config`, `loadSearch`, and `themeComponents` into nested default components so navigation highlighting, mobile menus, and runtime panels stay connected.
56
+
57
+ Generated templates include `src/routes/+error.svelte`. Register `theme.components.Error` to customize full-route error pages and `theme.components.RenderError` to customize local render-boundary failures inside layouts, articles, and tools.
58
+
59
+ ## MDX/SVX Components
60
+
61
+ The Vite plugin loads `svedocs.config.ts` by default. You can also pass a config object explicitly and register shared authoring components or layouts:
62
+
63
+ ```ts
64
+ import { svedocs } from 'svedocs/vite';
65
+ import svedocsConfig from './svedocs.config';
66
+
67
+ svedocs({
68
+ config: svedocsConfig,
69
+ components: {
70
+ Callout: '$lib/Callout.svelte'
71
+ },
72
+ layouts: {
73
+ feature: '$lib/FeatureLayout.svelte'
74
+ },
75
+ theme: {
76
+ components: {
77
+ Navbar: '$lib/theme/Navbar.svelte',
78
+ Article: '$lib/theme/Article.svelte'
79
+ }
80
+ }
81
+ })
82
+ ```
83
+
84
+ `.svx` and `.mdx` files can then use `<Callout />` without local imports.
85
+ Registered theme components are exposed through `virtual:svedocs/theme-components` and can replace default navigation, article, search, Ask AI, ToC, and footer rendering.
@@ -0,0 +1,13 @@
1
+ import type { AiProvider } from './types.js';
2
+ export interface OpenAiCompatibleProviderOptions {
3
+ apiKey: string;
4
+ model: string;
5
+ baseUrl?: string;
6
+ systemPrompt?: string;
7
+ citationLimit?: number;
8
+ temperature?: number;
9
+ headers?: Record<string, string>;
10
+ fetch?: typeof fetch;
11
+ }
12
+ export declare function createOpenAiCompatibleProvider(options: OpenAiCompatibleProviderOptions): AiProvider;
13
+ //# sourceMappingURL=openai-compatible.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compatible.d.ts","sourceRoot":"","sources":["../../src/ai/openai-compatible.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAYD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,UAAU,CAgDnG"}
@@ -0,0 +1,92 @@
1
+ import { filterSearchRecords, searchRecords } from '../search.js';
2
+ export function createOpenAiCompatibleProvider(options) {
3
+ const citationLimit = options.citationLimit ?? 5;
4
+ const systemPrompt = options.systemPrompt ?? [
5
+ 'Answer from the documentation sources provided by svedocs.',
6
+ 'Cite relevant pages by title and URL.',
7
+ 'If the sources are insufficient, say what is missing.'
8
+ ].join(' ');
9
+ return {
10
+ name: 'openai-compatible',
11
+ async ask(input) {
12
+ const ranked = createOpenAiContextRecords(input, citationLimit);
13
+ const citations = ranked.map((result) => ({
14
+ title: result.title,
15
+ url: result.url,
16
+ ...(result.section ? { section: result.section } : {})
17
+ }));
18
+ const history = (input.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
19
+ const lastIsCurrent = history.length > 0
20
+ && history[history.length - 1]?.role === 'user'
21
+ && history[history.length - 1]?.content === input.question;
22
+ const trailing = history.length > 0 && lastIsCurrent ? history.slice(0, -1) : history;
23
+ const response = await (options.fetch ?? fetch)(createChatCompletionsEndpoint(options.baseUrl), {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ authorization: `Bearer ${options.apiKey}`,
28
+ ...(options.headers ?? {})
29
+ },
30
+ body: JSON.stringify({
31
+ model: options.model,
32
+ ...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
33
+ messages: [
34
+ { role: 'system', content: systemPrompt },
35
+ ...trailing,
36
+ { role: 'user', content: createPrompt(input, ranked) }
37
+ ]
38
+ })
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`OpenAI-compatible provider returned ${response.status}: ${(await response.text()).slice(0, 500)}`);
42
+ }
43
+ const payload = await response.json();
44
+ return {
45
+ answer: readChatCompletionAnswer(payload),
46
+ citations
47
+ };
48
+ }
49
+ };
50
+ }
51
+ function createOpenAiContextRecords(input, citationLimit) {
52
+ const ranked = searchRecords(input.records ?? [], {
53
+ query: input.question,
54
+ limit: citationLimit,
55
+ ...(input.scope ?? {})
56
+ });
57
+ if (ranked.length > 0)
58
+ return ranked;
59
+ return filterSearchRecords(input.records ?? [], input.scope).slice(0, citationLimit).map((record, index) => ({
60
+ id: record.id,
61
+ title: record.title,
62
+ url: record.url,
63
+ ...(record.section ? { section: record.section } : {}),
64
+ excerpt: record.content.slice(0, 220),
65
+ score: 1 / (index + 1),
66
+ metadata: record.metadata
67
+ }));
68
+ }
69
+ function createChatCompletionsEndpoint(baseUrl = 'https://api.openai.com/v1') {
70
+ return `${baseUrl.replace(/\/$/, '')}/chat/completions`;
71
+ }
72
+ function createPrompt(input, sources) {
73
+ const context = sources.length > 0
74
+ ? sources.map((source, index) => [
75
+ `[${index + 1}] ${source.section ? `${source.title}: ${source.section}` : source.title}`,
76
+ `URL: ${source.url}`,
77
+ source.excerpt
78
+ ].join('\n')).join('\n\n')
79
+ : 'No local documentation source matched.';
80
+ return [
81
+ `Question: ${input.question}`,
82
+ '',
83
+ 'Sources:',
84
+ context
85
+ ].join('\n');
86
+ }
87
+ function readChatCompletionAnswer(payload) {
88
+ return payload.output_text
89
+ ?? payload.choices?.[0]?.message?.content
90
+ ?? payload.choices?.[0]?.text
91
+ ?? 'No answer was returned.';
92
+ }
@@ -0,0 +1,20 @@
1
+ import type { SvedocsSearchRecord } from '../core.js';
2
+ import type { AiProvider, AiSearchBinding, AskCitation, CloudflareWorkersAiBinding } from './types.js';
3
+ export declare function createMockAiProvider(): AiProvider;
4
+ export declare function createCloudflareAiSearchAiProvider(input: {
5
+ binding: AiSearchBinding;
6
+ instanceName?: string;
7
+ namespace?: boolean;
8
+ systemPrompt?: string;
9
+ model?: string;
10
+ maxResults?: number;
11
+ }): AiProvider;
12
+ export declare function createWorkersAiProvider(input: {
13
+ ai: CloudflareWorkersAiBinding;
14
+ model?: string;
15
+ systemPrompt?: string;
16
+ citationLimit?: number;
17
+ }): AiProvider;
18
+ export declare function rankAskCitations(records: SvedocsSearchRecord[], question: string, limit?: number): AskCitation[];
19
+ export declare const mockAiProvider: AiProvider;
20
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/ai/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAItD,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,WAAW,EAGX,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB,wBAAgB,oBAAoB,IAAI,UAAU,CAajD;AAED,wBAAgB,kCAAkC,CAAC,KAAK,EAAE;IACxD,OAAO,EAAE,eAAe,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,UAAU,CAuDb;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,EAAE,EAAE,0BAA0B,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,UAAU,CA2Bb;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,mBAAmB,EAAE,EAC9B,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAI,GACR,WAAW,EAAE,CAMf;AAED,eAAO,MAAM,cAAc,YAAyB,CAAC"}
@@ -0,0 +1,185 @@
1
+ import { createCloudflareAiSearchScopeFilters, normalizeCloudflareAiSearchResults } from '../search/cloudflare.js';
2
+ import { matchesSearchScope, searchRecords } from '../search.js';
3
+ export function createMockAiProvider() {
4
+ return {
5
+ name: 'mock',
6
+ ask(input) {
7
+ const citations = rankAskCitations(input.records ?? [], input.question, 3);
8
+ return {
9
+ answer: citations.length > 0
10
+ ? `I found ${citations.length} relevant source${citations.length === 1 ? '' : 's'} in this documentation. Connect Cloudflare AI Search to generate a hosted answer.`
11
+ : `Ask AI is configured. Connect Cloudflare AI Search to answer: ${input.question}`,
12
+ citations
13
+ };
14
+ }
15
+ };
16
+ }
17
+ export function createCloudflareAiSearchAiProvider(input) {
18
+ const maxResults = input.maxResults ?? 5;
19
+ const systemPrompt = input.systemPrompt ?? 'Answer from the documentation. Cite the pages you used.';
20
+ async function ask(question) {
21
+ const conversation = buildChatMessages(systemPrompt, question);
22
+ const instance = resolveAiChatInstance(input.binding, input.instanceName, input.namespace);
23
+ if (!instance.chatCompletions) {
24
+ const result = await instance.search({
25
+ messages: conversation,
26
+ ai_search_options: {
27
+ retrieval: createCloudflareAiSearchRetrieval(maxResults, question.scope)
28
+ }
29
+ });
30
+ return {
31
+ answer: result.response ?? result.answer ?? 'No answer was returned.',
32
+ citations: createCloudflareAiSearchCitations(result, question, maxResults)
33
+ };
34
+ }
35
+ const result = await instance.chatCompletions({
36
+ messages: conversation,
37
+ ...(input.model ? { model: input.model } : {}),
38
+ ai_search_options: {
39
+ retrieval: createCloudflareAiSearchRetrieval(maxResults, question.scope)
40
+ }
41
+ });
42
+ return {
43
+ answer: readAiSearchAnswer(result),
44
+ citations: readCloudflareAiSearchCitations(result, question, maxResults)
45
+ };
46
+ }
47
+ return {
48
+ name: 'cloudflare-ai-search',
49
+ ask,
50
+ async stream(question) {
51
+ const instance = resolveAiChatInstance(input.binding, input.instanceName, input.namespace);
52
+ if (!instance.chatCompletions) {
53
+ return createSvedocsAnswerStream(await ask(question));
54
+ }
55
+ const result = await instance.chatCompletions({
56
+ messages: buildChatMessages(systemPrompt, question),
57
+ ...(input.model ? { model: input.model } : {}),
58
+ stream: true,
59
+ ai_search_options: {
60
+ retrieval: createCloudflareAiSearchRetrieval(maxResults, question.scope)
61
+ }
62
+ });
63
+ if (result instanceof Response || result instanceof ReadableStream)
64
+ return result;
65
+ return createSvedocsAnswerStream({
66
+ answer: readAiSearchAnswer(result),
67
+ citations: readCloudflareAiSearchCitations(result, question, maxResults)
68
+ });
69
+ }
70
+ };
71
+ }
72
+ export function createWorkersAiProvider(input) {
73
+ const systemPrompt = input.systemPrompt ?? 'Answer from the documentation context. Keep answers concise and cite relevant pages.';
74
+ return {
75
+ name: 'cloudflare-workers-ai',
76
+ async ask(question) {
77
+ const citations = rankAskCitations(question.records ?? [], question.question, input.citationLimit ?? 5);
78
+ const context = citations.map((citation) => `- ${citation.section ?? citation.title}: ${citation.url}`).join('\n');
79
+ const history = (question.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
80
+ const userContent = [
81
+ `Question: ${question.question}`,
82
+ '',
83
+ 'Sources:',
84
+ context || 'No local source matched.'
85
+ ].join('\n');
86
+ const result = await input.ai.run(input.model ?? '@cf/meta/llama-3.1-8b-instruct', {
87
+ messages: [
88
+ { role: 'system', content: systemPrompt },
89
+ ...history,
90
+ { role: 'user', content: userContent }
91
+ ]
92
+ });
93
+ return {
94
+ answer: readWorkersAiAnswer(result),
95
+ citations
96
+ };
97
+ }
98
+ };
99
+ }
100
+ export function rankAskCitations(records, question, limit = 3) {
101
+ return searchRecords(records, { query: question, limit }).map((result) => ({
102
+ title: result.title,
103
+ url: result.url,
104
+ ...(result.section ? { section: result.section } : {})
105
+ }));
106
+ }
107
+ export const mockAiProvider = createMockAiProvider();
108
+ function createSvedocsAnswerStream(result) {
109
+ const encoder = new TextEncoder();
110
+ return new ReadableStream({
111
+ start(controller) {
112
+ controller.enqueue(encoder.encode(`event: answer\ndata: ${JSON.stringify({ answer: result.answer })}\n\n`));
113
+ controller.enqueue(encoder.encode(`event: citations\ndata: ${JSON.stringify({ citations: result.citations })}\n\n`));
114
+ controller.enqueue(encoder.encode('event: done\ndata: {}\n\n'));
115
+ controller.close();
116
+ }
117
+ });
118
+ }
119
+ function resolveAiChatInstance(binding, instanceName = 'svedocs', namespace = false) {
120
+ return namespace
121
+ ? binding.get(instanceName)
122
+ : binding;
123
+ }
124
+ function normalizeCitations(citations) {
125
+ return (citations ?? []).map((citation) => ({
126
+ title: citation.title ?? 'Source',
127
+ url: citation.url ?? '#',
128
+ ...(citation.section ? { section: citation.section } : {})
129
+ }));
130
+ }
131
+ function createCloudflareAiSearchRetrieval(maxResults, scope) {
132
+ const filters = createCloudflareAiSearchScopeFilters(scope);
133
+ return {
134
+ retrieval_type: 'hybrid',
135
+ max_num_results: maxResults,
136
+ ...(filters ? { filters } : {})
137
+ };
138
+ }
139
+ function readCloudflareAiSearchCitations(result, input, maxResults) {
140
+ const scopedCitations = createCloudflareAiSearchCitations(result, input, maxResults);
141
+ const citations = normalizeCitations(result.citations);
142
+ if (hasSearchScope(input.scope)) {
143
+ return scopedCitations.length > 0 ? scopedCitations : citations.slice(0, maxResults);
144
+ }
145
+ return citations.length > 0 ? citations.slice(0, maxResults) : scopedCitations;
146
+ }
147
+ function createCloudflareAiSearchCitations(result, input, maxResults) {
148
+ return normalizeCloudflareAiSearchResults(result, input.question)
149
+ .filter((searchResult) => matchesSearchScope({ metadata: searchResult.metadata }, input.scope))
150
+ .slice(0, maxResults)
151
+ .map((searchResult) => ({
152
+ title: searchResult.title,
153
+ url: searchResult.url,
154
+ ...(searchResult.section ? { section: searchResult.section } : {})
155
+ }));
156
+ }
157
+ function hasSearchScope(scope) {
158
+ return Boolean(scope?.locale || scope?.kind);
159
+ }
160
+ function readAiSearchAnswer(result) {
161
+ return (result.response ??
162
+ result.answer ??
163
+ result.choices?.[0]?.message?.content ??
164
+ result.choices?.[0]?.text ??
165
+ 'No answer was returned.');
166
+ }
167
+ function readWorkersAiAnswer(result) {
168
+ if (typeof result === 'string')
169
+ return result;
170
+ return result.response ?? result.answer ?? result.result ?? 'No answer was returned.';
171
+ }
172
+ function buildChatMessages(systemPrompt, input) {
173
+ const history = (input.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
174
+ const lastIsCurrentQuestion = history.length > 0 &&
175
+ history[history.length - 1]?.role === 'user' &&
176
+ history[history.length - 1]?.content === input.question;
177
+ const messages = [
178
+ { role: 'system', content: systemPrompt },
179
+ ...history
180
+ ];
181
+ if (!lastIsCurrentQuestion) {
182
+ messages.push({ role: 'user', content: input.question });
183
+ }
184
+ return messages;
185
+ }
@@ -0,0 +1,18 @@
1
+ import type { AiRateLimiter, AiRateLimitStore, CloudflareKvNamespace } from './types.js';
2
+ export declare function createMemoryRateLimiter(input: {
3
+ windowMs: number;
4
+ max: number;
5
+ }): AiRateLimiter;
6
+ export declare function createStoreRateLimiter(input: {
7
+ windowMs: number;
8
+ max: number;
9
+ store: AiRateLimitStore;
10
+ prefix?: string;
11
+ }): AiRateLimiter;
12
+ export declare function createCloudflareKvRateLimiter(input: {
13
+ namespace: CloudflareKvNamespace;
14
+ windowMs: number;
15
+ max: number;
16
+ prefix?: string;
17
+ }): AiRateLimiter;
18
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/ai/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEzF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,aAAa,CAchB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,CAqBhB;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,CAchB"}
@@ -0,0 +1,52 @@
1
+ export function createMemoryRateLimiter(input) {
2
+ const buckets = new Map();
3
+ return createStoreRateLimiter({
4
+ windowMs: input.windowMs,
5
+ max: input.max,
6
+ store: {
7
+ async get(key) {
8
+ return buckets.get(key);
9
+ },
10
+ async put(key, value) {
11
+ buckets.set(key, value);
12
+ }
13
+ }
14
+ });
15
+ }
16
+ export function createStoreRateLimiter(input) {
17
+ return {
18
+ async check({ key }) {
19
+ const now = Date.now();
20
+ const storeKey = `${input.prefix ?? 'svedocs:ai'}:${key}`;
21
+ const existing = await input.store.get(storeKey);
22
+ if (!existing || existing.resetAt <= now) {
23
+ await input.store.put(storeKey, { count: 1, resetAt: now + input.windowMs }, Math.ceil(input.windowMs / 1000));
24
+ return { allowed: true };
25
+ }
26
+ const next = { count: existing.count + 1, resetAt: existing.resetAt };
27
+ await input.store.put(storeKey, next, Math.ceil((existing.resetAt - now) / 1000));
28
+ if (next.count > input.max) {
29
+ return {
30
+ allowed: false,
31
+ retryAfter: Math.ceil((existing.resetAt - now) / 1000)
32
+ };
33
+ }
34
+ return { allowed: true };
35
+ }
36
+ };
37
+ }
38
+ export function createCloudflareKvRateLimiter(input) {
39
+ return createStoreRateLimiter({
40
+ windowMs: input.windowMs,
41
+ max: input.max,
42
+ ...(input.prefix ? { prefix: input.prefix } : {}),
43
+ store: {
44
+ async get(key) {
45
+ return input.namespace.get(key, { type: 'json' }).then((value) => value ?? undefined);
46
+ },
47
+ async put(key, value, ttlSeconds) {
48
+ await input.namespace.put(key, JSON.stringify(value), { expirationTtl: ttlSeconds });
49
+ }
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,4 @@
1
+ import type { SvedocsSearchRecord } from '../core.js';
2
+ import type { AiProvider, CreateAskResponseOptions } from './types.js';
3
+ export declare function createAskResponse(provider: AiProvider, request: Request, recordsOrOptions?: SvedocsSearchRecord[] | CreateAskResponseOptions): Promise<Response>;
4
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/ai/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGtD,OAAO,KAAK,EAAE,UAAU,EAA0B,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE/F,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,OAAO,EAChB,gBAAgB,GAAE,mBAAmB,EAAE,GAAG,wBAA6B,GACtE,OAAO,CAAC,QAAQ,CAAC,CA2CnB"}
@@ -0,0 +1,151 @@
1
+ import { filterSearchRecords } from '../search.js';
2
+ export async function createAskResponse(provider, request, recordsOrOptions = []) {
3
+ const options = Array.isArray(recordsOrOptions) ? { records: recordsOrOptions } : recordsOrOptions;
4
+ const stream = options.stream ?? wantsStream(request);
5
+ try {
6
+ const body = await request.json().catch(() => ({}));
7
+ const messages = sanitizeMessages(body.messages);
8
+ const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user');
9
+ const question = (body.question?.trim() ?? lastUserMessage?.content.trim() ?? '');
10
+ if (!question) {
11
+ return askErrorResponse('Question is required.', 400, stream);
12
+ }
13
+ if (options.rateLimiter) {
14
+ const limit = await options.rateLimiter.check({
15
+ key: createRateLimitKey(request, options.rateLimitKey),
16
+ request
17
+ });
18
+ if (!limit.allowed) {
19
+ return askErrorResponse('Ask AI rate limit exceeded.', 429, stream, limit.retryAfter);
20
+ }
21
+ }
22
+ const scope = createRequestScope(request, body, options.scope);
23
+ const records = filterSearchRecords(options.records ?? [], scope);
24
+ const askInput = {
25
+ question,
26
+ records,
27
+ scope,
28
+ ...(messages.length > 0 ? { messages } : {}),
29
+ ...(options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}),
30
+ ...(options.maxResults ? { maxResults: options.maxResults } : {})
31
+ };
32
+ if (stream && provider.stream) {
33
+ return streamProviderResponse(await provider.stream(askInput));
34
+ }
35
+ const result = await provider.ask(askInput);
36
+ return stream ? streamAskResult(result) : jsonResponse(result);
37
+ }
38
+ catch (error) {
39
+ return askErrorResponse(error instanceof Error ? error.message : 'Ask AI failed.', 500, stream);
40
+ }
41
+ }
42
+ function sanitizeMessages(messages) {
43
+ if (!Array.isArray(messages))
44
+ return [];
45
+ return messages
46
+ .filter((message) => Boolean(message)
47
+ && (message.role === 'user' || message.role === 'assistant')
48
+ && typeof message.content === 'string'
49
+ && message.content.trim().length > 0)
50
+ .map((message) => ({ role: message.role, content: message.content }));
51
+ }
52
+ function createRequestScope(request, body, configured) {
53
+ const url = new URL(request.url);
54
+ const base = typeof configured === 'function' ? configured(request) : configured;
55
+ const locale = readScopeParam(url, 'locale');
56
+ const kind = readScopeParam(url, 'kind');
57
+ return cleanScope({
58
+ ...(base ?? {}),
59
+ ...(locale ? { locale } : {}),
60
+ ...(kind ? { kind } : {}),
61
+ ...(body.locale ? { locale: body.locale } : {}),
62
+ ...(body.kind ? { kind: body.kind } : {})
63
+ });
64
+ }
65
+ function cleanScope(scope) {
66
+ return {
67
+ ...(scope.locale?.trim() ? { locale: scope.locale.trim() } : {}),
68
+ ...(scope.kind?.trim() ? { kind: scope.kind.trim() } : {})
69
+ };
70
+ }
71
+ function readScopeParam(url, key) {
72
+ return url.searchParams.get(key)?.trim() || undefined;
73
+ }
74
+ function wantsStream(request) {
75
+ const url = new URL(request.url);
76
+ return url.searchParams.get('stream') === '1' || request.headers.get('accept')?.includes('text/event-stream') === true;
77
+ }
78
+ function createRateLimitKey(request, key) {
79
+ if (typeof key === 'function')
80
+ return key(request);
81
+ if (typeof key === 'string')
82
+ return key;
83
+ return request.headers.get('cf-connecting-ip')
84
+ ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
85
+ ?? 'anonymous';
86
+ }
87
+ function streamProviderResponse(result) {
88
+ if (result instanceof Response) {
89
+ return new Response(result.body, {
90
+ status: result.status,
91
+ headers: {
92
+ 'content-type': result.headers.get('content-type') ?? 'text/event-stream; charset=utf-8',
93
+ 'cache-control': result.headers.get('cache-control') ?? 'no-cache, no-transform'
94
+ }
95
+ });
96
+ }
97
+ return new Response(result, {
98
+ headers: {
99
+ 'content-type': 'text/event-stream; charset=utf-8',
100
+ 'cache-control': 'no-cache, no-transform'
101
+ }
102
+ });
103
+ }
104
+ function streamAskResult(result) {
105
+ return eventStreamResponse((send) => {
106
+ send('answer', { answer: result.answer });
107
+ send('citations', { citations: result.citations });
108
+ send('done', {});
109
+ });
110
+ }
111
+ function askErrorResponse(message, status, stream, retryAfter) {
112
+ if (stream) {
113
+ return eventStreamResponse((send) => {
114
+ send('error', { error: message, status, ...(retryAfter ? { retryAfter } : {}) });
115
+ send('done', {});
116
+ }, status, retryAfter);
117
+ }
118
+ return jsonResponse({
119
+ error: message,
120
+ ...(retryAfter ? { retryAfter } : {})
121
+ }, status, retryAfter);
122
+ }
123
+ function eventStreamResponse(write, status = 200, retryAfter) {
124
+ const encoder = new TextEncoder();
125
+ const stream = new ReadableStream({
126
+ start(controller) {
127
+ const send = (event, value) => {
128
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(value)}\n\n`));
129
+ };
130
+ write(send);
131
+ controller.close();
132
+ }
133
+ });
134
+ return new Response(stream, {
135
+ status,
136
+ headers: {
137
+ 'content-type': 'text/event-stream; charset=utf-8',
138
+ 'cache-control': 'no-cache, no-transform',
139
+ ...(retryAfter ? { 'retry-after': String(retryAfter) } : {})
140
+ }
141
+ });
142
+ }
143
+ function jsonResponse(value, status = 200, retryAfter) {
144
+ return new Response(JSON.stringify(value), {
145
+ status,
146
+ headers: {
147
+ 'content-type': 'application/json; charset=utf-8',
148
+ ...(retryAfter ? { 'retry-after': String(retryAfter) } : {})
149
+ }
150
+ });
151
+ }