blume 2.0.0 → 2.0.1

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 (203) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f7t03s3g.js → chunk-27g6wdth.js} +2 -2
  5. package/dist/cli/{chunk-mnqj32sj.js → chunk-2hn4b8z7.js} +13 -13
  6. package/dist/cli/chunk-2hn4b8z7.js.map +12 -0
  7. package/dist/cli/{chunk-by2290sx.js → chunk-5shv93fd.js} +2 -2
  8. package/dist/cli/{chunk-a9kptbw5.js → chunk-6crbhc3x.js} +3 -3
  9. package/dist/cli/{chunk-a9kptbw5.js.map → chunk-6crbhc3x.js.map} +1 -1
  10. package/dist/cli/{chunk-mwt1k8n7.js → chunk-6hsn950k.js} +20 -20
  11. package/dist/cli/chunk-6hsn950k.js.map +10 -0
  12. package/dist/cli/{chunk-11j0384y.js → chunk-6vm74dry.js} +13 -13
  13. package/dist/cli/{chunk-11j0384y.js.map → chunk-6vm74dry.js.map} +3 -3
  14. package/dist/cli/{chunk-j8mw0za6.js → chunk-79jhk4py.js} +8 -8
  15. package/dist/cli/{chunk-j8mw0za6.js.map → chunk-79jhk4py.js.map} +2 -2
  16. package/dist/cli/{chunk-nk3ts2xk.js → chunk-82bbrxdn.js} +2 -2
  17. package/dist/cli/{chunk-2q1dwty4.js → chunk-ah61y8py.js} +8 -8
  18. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-ah61y8py.js.map} +4 -4
  19. package/dist/cli/{chunk-zxccj738.js → chunk-ce574jw2.js} +1 -1
  20. package/dist/cli/{chunk-y3e45rc8.js → chunk-ch6g3ar0.js} +3 -3
  21. package/dist/cli/{chunk-beat36xx.js → chunk-dh8cwk36.js} +5 -5
  22. package/dist/cli/{chunk-beat36xx.js.map → chunk-dh8cwk36.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-epjnccmv.js} +13 -13
  24. package/dist/cli/{chunk-ernrthtr.js → chunk-f2z5v128.js} +13 -13
  25. package/dist/cli/{chunk-zg2gtj10.js → chunk-fs23ddbb.js} +2 -2
  26. package/dist/cli/{chunk-7ez8ny0t.js → chunk-fxypxtvm.js} +2 -2
  27. package/dist/cli/{chunk-tzne8qfq.js → chunk-fz5wtpmh.js} +13 -13
  28. package/dist/cli/{chunk-b5aj94ah.js → chunk-hdpx1tax.js} +4 -4
  29. package/dist/cli/{chunk-d80hr03s.js → chunk-jwyddg7y.js} +9 -9
  30. package/dist/cli/{chunk-d80hr03s.js.map → chunk-jwyddg7y.js.map} +2 -2
  31. package/dist/cli/{chunk-fh5hj5jt.js → chunk-kdp5q7ke.js} +15 -15
  32. package/dist/cli/{chunk-6k8vp3ta.js → chunk-kpf8rrjc.js} +9 -9
  33. package/dist/cli/{chunk-6k8vp3ta.js.map → chunk-kpf8rrjc.js.map} +3 -3
  34. package/dist/cli/{chunk-5a2z0198.js → chunk-m3vmjgmq.js} +9 -9
  35. package/dist/cli/{chunk-5a2z0198.js.map → chunk-m3vmjgmq.js.map} +2 -2
  36. package/dist/cli/{chunk-bctazmbk.js → chunk-mb2919y2.js} +4 -4
  37. package/dist/cli/{chunk-79njf86q.js → chunk-q5163e60.js} +13 -13
  38. package/dist/cli/{chunk-xaz13gwg.js → chunk-qkqwkpte.js} +196 -208
  39. package/dist/cli/{chunk-xaz13gwg.js.map → chunk-qkqwkpte.js.map} +48 -48
  40. package/dist/cli/{chunk-sqn5t4q0.js → chunk-qs4q5p4e.js} +3 -3
  41. package/dist/cli/{chunk-bw22s759.js → chunk-qwsrynx5.js} +1 -1
  42. package/dist/cli/{chunk-z01ze5c1.js → chunk-s1p84fyh.js} +15 -15
  43. package/dist/cli/{chunk-pnnvybbk.js → chunk-s6jhgk0q.js} +5 -5
  44. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-s6jhgk0q.js.map} +2 -2
  45. package/dist/cli/{chunk-f2972sbt.js → chunk-vtk4a6dg.js} +1 -1
  46. package/dist/cli/{chunk-z1f5arsg.js → chunk-wgm7m9qk.js} +21 -21
  47. package/dist/cli/{chunk-z1f5arsg.js.map → chunk-wgm7m9qk.js.map} +4 -4
  48. package/dist/cli/{chunk-bnbmcwfb.js → chunk-wm7js3j9.js} +5 -5
  49. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-wm7js3j9.js.map} +3 -3
  50. package/dist/cli/{chunk-d1tadaw7.js → chunk-yt5n7ppj.js} +3 -3
  51. package/dist/cli/{chunk-pat2zzwc.js → chunk-yw7dm696.js} +1 -1
  52. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-yw7dm696.js.map} +1 -1
  53. package/dist/cli/{chunk-88cpgt6h.js → chunk-zxcczpyx.js} +1 -1
  54. package/dist/cli/{chunk-41za066z.js → chunk-zxh4d9vy.js} +4 -4
  55. package/dist/cli/index.js +17 -17
  56. package/dist/types/ai/agent-readability.d.ts +1 -1
  57. package/dist/types/ai/api/paths.d.ts +1 -1
  58. package/dist/types/ai/ask-context.d.ts +7 -7
  59. package/dist/types/ai/ask.d.ts +43 -43
  60. package/dist/types/ai/index.d.ts +3 -3
  61. package/dist/types/ai/openapi-components.d.ts +1 -1
  62. package/dist/types/ai/serializers.d.ts +1 -1
  63. package/dist/types/ai/visibility.d.ts +1 -1
  64. package/dist/types/core/config-input.d.ts +17 -17
  65. package/dist/types/core/config.d.ts +3 -3
  66. package/dist/types/core/data.d.ts +3 -3
  67. package/dist/types/core/i18n-ui.d.ts +6 -8
  68. package/dist/types/core/schema.d.ts +5 -5
  69. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  70. package/dist/types/search/documents.d.ts +1 -1
  71. package/dist/types/search/orama-index.d.ts +1 -1
  72. package/docs/02-deployment.mdx +4 -4
  73. package/docs/03-upgrading.mdx +22 -9
  74. package/docs/04-migrating.mdx +4 -4
  75. package/docs/08-faq.mdx +3 -3
  76. package/docs/advanced/custom-pages.mdx +2 -2
  77. package/docs/advanced/skills.mdx +1 -1
  78. package/docs/cli/audit.mdx +3 -3
  79. package/docs/cli/doctor.mdx +3 -3
  80. package/docs/cli/evals.mdx +7 -7
  81. package/docs/cli/index.mdx +2 -2
  82. package/docs/cli/translate.mdx +8 -8
  83. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +19 -19
  84. package/docs/configuration/customization.mdx +2 -2
  85. package/docs/configuration/index.mdx +2 -2
  86. package/docs/configuration/meta.ts +1 -1
  87. package/docs/configuration/search.mdx +1 -1
  88. package/docs/content/i18n.mdx +2 -2
  89. package/docs/content/islands.mdx +1 -1
  90. package/docs/discoverability/agent-discovery.mdx +1 -1
  91. package/docs/discoverability/index.mdx +1 -1
  92. package/docs/index.mdx +2 -2
  93. package/package.json +1 -1
  94. package/skills/blume/SKILL.md +5 -5
  95. package/skills/blume-migrate/SKILL.md +2 -2
  96. package/src/ai/agent-readability.ts +4 -4
  97. package/src/ai/api/paths.ts +1 -1
  98. package/src/ai/ask-context.ts +7 -7
  99. package/src/ai/ask-data.ts +2 -2
  100. package/src/ai/ask.ts +84 -71
  101. package/src/ai/cors.ts +3 -3
  102. package/src/ai/index.ts +16 -16
  103. package/src/ai/openapi-components.ts +1 -1
  104. package/src/ai/serializers.ts +1 -1
  105. package/src/ai/visibility.ts +1 -1
  106. package/src/astro/generate.ts +19 -18
  107. package/src/astro/module-types.ts +1 -1
  108. package/src/astro/runtime-deps.ts +6 -6
  109. package/src/astro/templates.ts +26 -26
  110. package/src/blume-modules.d.ts +2 -2
  111. package/src/cli/commands/audit.ts +1 -1
  112. package/src/cli/commands/doctor.ts +7 -5
  113. package/src/cli/commands/eval.ts +3 -3
  114. package/src/cli/commands/migrate.ts +2 -2
  115. package/src/cli/commands/translate.ts +3 -3
  116. package/src/cli/commands/upgrade.ts +2 -2
  117. package/src/cli/required-secrets.ts +3 -3
  118. package/src/components/copy-feedback.ts +1 -1
  119. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  120. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +23 -23
  121. package/src/components/islands/hooks.ts +14 -12
  122. package/src/components/layout/Header.astro +10 -10
  123. package/src/components/layout/PageLayout.astro +6 -6
  124. package/src/components/layout/Pagination.astro +7 -7
  125. package/src/components/layout/ReferenceLayout.astro +1 -1
  126. package/src/components/layout/RootLayout.astro +6 -6
  127. package/src/components/layout/Search.astro +13 -13
  128. package/src/components/layout/analytics-client.ts +1 -1
  129. package/src/components/layout/drawer-inert.ts +1 -1
  130. package/src/components/openapi/description.ts +2 -2
  131. package/src/core/code-fences.ts +1 -1
  132. package/src/core/config-input.ts +19 -19
  133. package/src/core/config.ts +3 -3
  134. package/src/core/data.ts +3 -3
  135. package/src/core/i18n-ui.ts +35 -9
  136. package/src/core/request-body.ts +1 -1
  137. package/src/core/schema.ts +25 -20
  138. package/src/core/server-features.ts +2 -2
  139. package/src/core/ui-packs/ar.ts +4 -5
  140. package/src/core/ui-packs/bg.ts +4 -5
  141. package/src/core/ui-packs/bn.ts +4 -5
  142. package/src/core/ui-packs/ca.ts +4 -5
  143. package/src/core/ui-packs/cs.ts +4 -5
  144. package/src/core/ui-packs/da.ts +4 -5
  145. package/src/core/ui-packs/de.ts +4 -5
  146. package/src/core/ui-packs/el.ts +4 -5
  147. package/src/core/ui-packs/es.ts +4 -5
  148. package/src/core/ui-packs/fa.ts +4 -5
  149. package/src/core/ui-packs/fi.ts +4 -5
  150. package/src/core/ui-packs/fr.ts +4 -5
  151. package/src/core/ui-packs/he.ts +4 -5
  152. package/src/core/ui-packs/hi.ts +4 -5
  153. package/src/core/ui-packs/hr.ts +4 -5
  154. package/src/core/ui-packs/hu.ts +4 -5
  155. package/src/core/ui-packs/id.ts +4 -5
  156. package/src/core/ui-packs/it.ts +4 -5
  157. package/src/core/ui-packs/ja.ts +4 -5
  158. package/src/core/ui-packs/ko.ts +4 -5
  159. package/src/core/ui-packs/nl.ts +4 -5
  160. package/src/core/ui-packs/no.ts +4 -5
  161. package/src/core/ui-packs/pl.ts +4 -5
  162. package/src/core/ui-packs/pt-br.ts +4 -5
  163. package/src/core/ui-packs/pt.ts +4 -5
  164. package/src/core/ui-packs/ro.ts +4 -5
  165. package/src/core/ui-packs/ru.ts +4 -5
  166. package/src/core/ui-packs/sk.ts +4 -5
  167. package/src/core/ui-packs/sr.ts +4 -5
  168. package/src/core/ui-packs/sv.ts +4 -5
  169. package/src/core/ui-packs/th.ts +4 -5
  170. package/src/core/ui-packs/tr.ts +4 -5
  171. package/src/core/ui-packs/uk.ts +4 -5
  172. package/src/core/ui-packs/vi.ts +4 -5
  173. package/src/core/ui-packs/zh-tw.ts +4 -5
  174. package/src/core/ui-packs/zh.ts +4 -5
  175. package/src/core/unrecognized-keys.ts +1 -1
  176. package/src/registry/eject.ts +19 -18
  177. package/src/search/documents.ts +2 -2
  178. package/src/search/orama-index.ts +1 -1
  179. package/src/translate/report.ts +1 -1
  180. package/src/upgrade/upgrade.ts +1 -1
  181. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  182. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  183. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-27g6wdth.js.map} +0 -0
  184. /package/dist/cli/{chunk-by2290sx.js.map → chunk-5shv93fd.js.map} +0 -0
  185. /package/dist/cli/{chunk-nk3ts2xk.js.map → chunk-82bbrxdn.js.map} +0 -0
  186. /package/dist/cli/{chunk-zxccj738.js.map → chunk-ce574jw2.js.map} +0 -0
  187. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-ch6g3ar0.js.map} +0 -0
  188. /package/dist/cli/{chunk-1w8dp3qb.js.map → chunk-epjnccmv.js.map} +0 -0
  189. /package/dist/cli/{chunk-ernrthtr.js.map → chunk-f2z5v128.js.map} +0 -0
  190. /package/dist/cli/{chunk-zg2gtj10.js.map → chunk-fs23ddbb.js.map} +0 -0
  191. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-fxypxtvm.js.map} +0 -0
  192. /package/dist/cli/{chunk-tzne8qfq.js.map → chunk-fz5wtpmh.js.map} +0 -0
  193. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-hdpx1tax.js.map} +0 -0
  194. /package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-kdp5q7ke.js.map} +0 -0
  195. /package/dist/cli/{chunk-bctazmbk.js.map → chunk-mb2919y2.js.map} +0 -0
  196. /package/dist/cli/{chunk-79njf86q.js.map → chunk-q5163e60.js.map} +0 -0
  197. /package/dist/cli/{chunk-sqn5t4q0.js.map → chunk-qs4q5p4e.js.map} +0 -0
  198. /package/dist/cli/{chunk-bw22s759.js.map → chunk-qwsrynx5.js.map} +0 -0
  199. /package/dist/cli/{chunk-z01ze5c1.js.map → chunk-s1p84fyh.js.map} +0 -0
  200. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-vtk4a6dg.js.map} +0 -0
  201. /package/dist/cli/{chunk-d1tadaw7.js.map → chunk-yt5n7ppj.js.map} +0 -0
  202. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-zxcczpyx.js.map} +0 -0
  203. /package/dist/cli/{chunk-41za066z.js.map → chunk-zxh4d9vy.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "The open-source docs framework for humans and agents.",
5
5
  "keywords": [
6
6
  "agents",
@@ -12,7 +12,7 @@ The core idea: **the framework _is_ the template.** There's no starter to clone
12
12
  ## What makes it different
13
13
 
14
14
  - **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.
15
- - **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional **Ask AI** assistant or an **MCP server** so coding agents read your docs directly.
15
+ - **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional in-page **assistant** or an **MCP server** so coding agents read your docs directly.
16
16
  - **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.
17
17
  - **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.
18
18
 
@@ -49,23 +49,23 @@ Navigation, search, and page metadata are inferred from your files as you add th
49
49
 
50
50
  ## Upgrading from Blume 1
51
51
 
52
- Blume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the Ask AI backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:
52
+ Blume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the assistant's model backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), Ask AI is renamed the assistant (`ai.ask` becomes `ai.assistant`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:
53
53
 
54
54
  ```bash
55
55
  npx blume@latest upgrade
56
56
  ```
57
57
 
58
- It bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--claude` or `--codex` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.
58
+ It bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--codex` or `--claude` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.
59
59
 
60
60
  ## Migrating from another framework
61
61
 
62
- To move a Mintlify, Fumadocs, Docusaurus, Starlight, or Nextra site to Blume, the user runs `npx blume migrate [source] --claude` (or `--codex`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.
62
+ To move a Mintlify, Fumadocs, Docusaurus, Starlight, or Nextra site to Blume, the user runs `npx blume migrate [source] --codex` (or `--claude`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.
63
63
 
64
64
  ## What's included
65
65
 
66
66
  - **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.
67
67
  - **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).
68
- - **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an Ask AI assistant, and an MCP server endpoint served by the docs site itself.
68
+ - **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.
69
69
  - **Navigation** — inferred from files, refined with `meta.ts` or config.
70
70
  - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.
71
71
  - **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
@@ -69,8 +69,8 @@ The single biggest shift for most sources — especially Mintlify — is that **
69
69
  - **`content`:** `root` (default `"docs"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `["**/*.{md,mdx}"]` / `["**/_*", "**/.*"]`), `sources` (an array of **adapters imported from `blume/sources`**: `filesystem({ root, include, exclude })`, `obsidian({ vault })`, `githubReleases({ owner, repo })`, `notion({ database })`, `sanity({ projectId, dataset, query })`, `contentful({ space, contentType })`, `payload({ url, collection })`, `strapi({ url, contentType })`, `mdxRemote({ github })`, `custom(source)`; every factory with an options object also takes `prefix` and `pollInterval`, while `custom(source)` takes a `ContentSource` instance that sets its own `prefix`. The 1.x `{ type: "…" }` objects were removed — rename `type` to the factory call and pass the other fields as its options — except `{ type: "custom", source }`, which becomes `custom(source)` with the instance as the only argument. `root`/`include`/`exclude` are shorthand for a single `filesystem()` and are **rejected beside `sources`** — move them into the `filesystem()` entry. OpenAPI/AsyncAPI/GraphQL are **not** among these; they're adapters in the top-level `reference` list), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.
70
70
  - **`basePath`** (top-level): a site-wide mount point (e.g. `"/docs"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).
71
71
  - **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the header mark when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see "Config-declared nesting" above).
72
- - **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: "algolia", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (Ask AI, Open in chat — `ai.ask.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openrouter({ model, reasoning })`, `llmgateway({ model })`, `inkeep({ model })`, or `openaiCompatible({ baseUrl, name, model, apiKeyEnv })`. Each adapter owns `model`, `apiKeyEnv`, `headers`, `reasoning`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`); there are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.ask` — a source that configured Ask AI that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` stay on `ai.ask`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).
73
- - **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: "static"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (Ask AI, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from "blume/deploy"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: "/docs" }` (leave `site` unset; see below). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from "blume/deploy"`) when a config uses both.
72
+ - **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: "algolia", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (the assistant, Open in chat — `ai.assistant.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openrouter({ model, reasoning })`, `llmgateway({ model })`, `inkeep({ model })`, or `openaiCompatible({ baseUrl, name, model, apiKeyEnv })`. Each adapter owns `model`, `apiKeyEnv`, `headers`, `reasoning`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`); there are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.assistant` — a source that configured an AI assistant that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` sit on `ai.assistant` itself; there is **no** `ai.ask` — Blume 2.0.0 and earlier used that name, so an older `blume.config.ts` moves the whole block to `ai.assistant`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).
73
+ - **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: "static"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (the assistant, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from "blume/deploy"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: "/docs" }` (leave `site` unset; see below). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from "blume/deploy"`) when a config uses both.
74
74
  - **Don't set `deployment.site`.** Blume auto-fills it: the dev server's `localhost` URL in dev, and the deployment URL (`VERCEL_PROJECT_PRODUCTION_URL`/`VERCEL_URL`) on Vercel. Hardcoding it in `blume.config.ts` overrides that auto-detection and pins the wrong absolute URL (canonical links, sitemap, OG, llms.txt) everywhere but the one host you typed — so leave it unset even when a source config had a `url`/`site` field. (Sitemap still generates in production because the deploy URL is present there.)
75
75
  - **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.
76
76
 
@@ -34,7 +34,7 @@ const usagePolicy = (
34
34
  };
35
35
 
36
36
  /**
37
- * The advertised Ask AI URL. An external endpoint is not served under
37
+ * The advertised assistant URL. An external endpoint is not served under
38
38
  * `deployment.base`, so a root-relative one absolutizes against the site
39
39
  * origin alone; the built-in route gets site and base via `abs`.
40
40
  */
@@ -154,7 +154,7 @@ const wellKnownArtifacts = (
154
154
  /**
155
155
  * Build `agent-readability.json`: a root manifest that indexes the project's
156
156
  * agent-facing surface — llms.txt, the raw-Markdown mirrors, the JSON docs
157
- * API and its OpenAPI description, the MCP server, Ask AI, sitemap, and feeds
157
+ * API and its OpenAPI description, the MCP server, the assistant, sitemap, and feeds
158
158
  * — so agents can discover and cite the docs without
159
159
  * scraping HTML. URLs are absolute when a `site` is configured and root-relative
160
160
  * (still under `deployment.base`) otherwise. Returns null when the manifest is
@@ -194,8 +194,8 @@ export const buildAgentReadability = (
194
194
  url: abs(config.agents.mcp.route),
195
195
  };
196
196
  }
197
- if (config.ai.ask?.enabled) {
198
- artifacts.askApi = askApiUrl(config.ai.ask.endpoint, site, abs);
197
+ if (config.ai.assistant?.enabled) {
198
+ artifacts.askApi = askApiUrl(config.ai.assistant.endpoint, site, abs);
199
199
  }
200
200
  Object.assign(artifacts, wellKnownArtifacts(config, abs));
201
201
  if (site && config.seo.sitemap) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Where the JSON docs API and its OpenAPI description are served. Base-less
3
3
  * (like every route Blume emits); callers layer `deployment.base` on top.
4
- * Under `/api/` alongside the Ask AI endpoint (`/api/ask`) so the namespace a
4
+ * Under `/api/` alongside the assistant endpoint (`/api/ask`) so the namespace a
5
5
  * Blume site reserves for live endpoints stays one prefix, and under its own
6
6
  * `docs` segment so a search provider's proxy at `/api/search` never collides.
7
7
  */
@@ -4,7 +4,7 @@ import type { FenceState } from "../core/code-fences.ts";
4
4
  import { buildOramaIndex, queryOramaIndex } from "../search/orama-index.ts";
5
5
  import type { OramaDoc } from "../search/orama-index.ts";
6
6
 
7
- /** A chat message as posted by the Ask AI island (`{ role, content }`). */
7
+ /** A chat message as posted by the assistant island (`{ role, content }`). */
8
8
  export interface AskMessage {
9
9
  content: string;
10
10
  role: string;
@@ -16,7 +16,7 @@ export interface AskPage {
16
16
  }
17
17
 
18
18
  /**
19
- * The self-contained snapshot the grounded Ask AI endpoint imports. Bundles the
19
+ * The self-contained snapshot the assistant's grounded endpoint imports. Bundles the
20
20
  * search documents so retrieval works regardless of the configured search
21
21
  * provider and needs no filesystem access at request time. Serialized to
22
22
  * `generated/ask-data.json` and built by {@link buildAskData}.
@@ -47,7 +47,7 @@ const CONTEXT_BUDGET = 10_000;
47
47
  const MIN_EXCERPT_CHARS = 200;
48
48
 
49
49
  /**
50
- * How much retrieved documentation a question carries (the `ai.ask.retrieval`
50
+ * How much retrieved documentation a question carries (the `ai.assistant.retrieval`
51
51
  * config). Every field falls back to the built-in default, so a partial object
52
52
  * only changes what it names. Injected characters dominate time-to-first-token
53
53
  * on a self-hosted backend, and the three knobs aren't interchangeable: the
@@ -309,7 +309,7 @@ const interleave = (lists: OramaDoc[][], limit: number): OramaDoc[] => {
309
309
  *
310
310
  * Pages are indexed whole (one document each), so a naive head slice of a long
311
311
  * page returns its intro and misses sections below the fold — the exact failure
312
- * where "How does Ask AI work?" retrieves the right page but only sees its
312
+ * where "How does the assistant work?" retrieves the right page but only sees its
313
313
  * opening paragraph. This centers the window on the densest cluster of query
314
314
  * terms so the injected text is the part that actually answers the question.
315
315
  * Exported for testing; {@link createAskContext} is the runtime entry point.
@@ -585,7 +585,7 @@ export const sectionExcerpt = (
585
585
  ): string => excerptPage(parsePage(content), query, max);
586
586
 
587
587
  /**
588
- * Build the request-time grounding function for the Ask AI endpoint.
588
+ * Build the request-time grounding function for the assistant endpoint.
589
589
  *
590
590
  * Lexical retrieval over Orama (the same index/ranking the search dialog and MCP
591
591
  * server use). The index is built once and memoized across requests. Returns a
@@ -593,12 +593,12 @@ export const sectionExcerpt = (
593
593
  * viewing — or `undefined` when there is nothing to ground on, so the endpoint
594
594
  * can fall back to its plain prompt.
595
595
  *
596
- * `options.instructions` (the `ai.ask.instructions` config) is appended after
596
+ * `options.instructions` (the `ai.assistant.instructions` config) is appended after
597
597
  * the base instruction rather than replacing it: the base carries the
598
598
  * functional contract (answer only from the excerpts, cite pages as Markdown
599
599
  * links) that the panel's citation rendering depends on.
600
600
  *
601
- * `options.retrieval` (the `ai.ask.retrieval` config) sizes how much
601
+ * `options.retrieval` (the `ai.assistant.retrieval` config) sizes how much
602
602
  * documentation each question carries; omitted fields keep today's defaults.
603
603
  */
604
604
  export const createAskContext = (
@@ -3,8 +3,8 @@ import { buildSearchDocuments } from "../search/documents.ts";
3
3
  import type { AskData } from "./ask-context.ts";
4
4
 
5
5
  /**
6
- * Build the grounding snapshot the Ask AI endpoint serves. Like the MCP server,
7
- * Ask AI is independent of on-page search, so documents are indexed even when the
6
+ * Build the grounding snapshot the assistant endpoint serves. Like the MCP server,
7
+ * the assistant is independent of on-page search, so documents are indexed even when the
8
8
  * search provider is `none` (`includeWhenDisabled`). `locale` is kept (unlike the
9
9
  * MCP snapshot) so retrieval can be filtered to the current page's language, and
10
10
  * content is kept as Markdown so grounding sees fenced code examples — the model
package/src/ai/ask.ts CHANGED
@@ -9,7 +9,7 @@ import { unrecognizedKeysMessage } from "../core/unrecognized-keys.ts";
9
9
  * `reasoning` values minus `provider-default`, which is what omitting the
10
10
  * option means.
11
11
  */
12
- export const askReasoningLevels = [
12
+ export const assistantReasoningLevels = [
13
13
  "none",
14
14
  "minimal",
15
15
  "low",
@@ -19,14 +19,17 @@ export const askReasoningLevels = [
19
19
  ] as const;
20
20
 
21
21
  /** How much the model reasons before answering (an adapter's `reasoning`). */
22
- export type AskReasoning = (typeof askReasoningLevels)[number];
22
+ export type AssistantReasoning = (typeof assistantReasoningLevels)[number];
23
23
 
24
24
  /**
25
25
  * The AI SDK's `providerOptions` shape, forwarded to `streamText` verbatim:
26
26
  * `{ [provider]: { [option]: value } }`. The escape hatch for model controls
27
27
  * Blume doesn't name, so a new provider knob never needs a Blume field.
28
28
  */
29
- export type AskProviderOptions = Record<string, Record<string, JsonValue>>;
29
+ export type AssistantProviderOptions = Record<
30
+ string,
31
+ Record<string, JsonValue>
32
+ >;
30
33
 
31
34
  /** The AI SDK provider package the OpenAI-compatible adapters install. */
32
35
  const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
@@ -35,8 +38,8 @@ const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
35
38
  // Shared options
36
39
  // ---------------------------------------------------------------------------
37
40
 
38
- /** The options every Ask AI adapter accepts. */
39
- export interface AskAdapterOptions {
41
+ /** The options every assistant adapter accepts. */
42
+ export interface AssistantAdapterOptions {
40
43
  /**
41
44
  * Name of the env var holding the provider's API key. Each adapter has its
42
45
  * own default; set this only to point at a different variable.
@@ -53,7 +56,7 @@ export interface AskAdapterOptions {
53
56
  * Options passed to `streamText` as its `providerOptions`, untouched, in the
54
57
  * AI SDK's own shape (`{ openai: { textVerbosity: "low" } }`, say).
55
58
  */
56
- providerOptions?: AskProviderOptions;
59
+ providerOptions?: AssistantProviderOptions;
57
60
  }
58
61
 
59
62
  /**
@@ -72,7 +75,7 @@ const sharedOptions = (apiKeyEnv: string) => ({
72
75
  providerOptions: providerOptionsSchema.optional(),
73
76
  });
74
77
 
75
- const reasoningOption = z.enum(askReasoningLevels).optional();
78
+ const reasoningOption = z.enum(assistantReasoningLevels).optional();
76
79
 
77
80
  // ---------------------------------------------------------------------------
78
81
  // gateway()
@@ -83,7 +86,7 @@ const GATEWAY_API_KEY_ENV = "AI_GATEWAY_API_KEY";
83
86
  const DEFAULT_GATEWAY_MODEL = "openai/gpt-5.5";
84
87
 
85
88
  /** Options for {@link gateway}. */
86
- export interface AskGatewayOptions extends AskAdapterOptions {
89
+ export interface AssistantGatewayOptions extends AssistantAdapterOptions {
87
90
  /** A `provider/model` id routed by the gateway. Defaults to `openai/gpt-5.5`. */
88
91
  model?: string;
89
92
  /**
@@ -92,7 +95,7 @@ export interface AskGatewayOptions extends AskAdapterOptions {
92
95
  * control (OpenAI's `reasoning_effort`, for example); the model has to
93
96
  * offer the level you pick. Omitted keeps the model's default.
94
97
  */
95
- reasoning?: AskReasoning;
98
+ reasoning?: AssistantReasoning;
96
99
  }
97
100
 
98
101
  const gatewayOptionsSchema = z.strictObject({
@@ -101,7 +104,10 @@ const gatewayOptionsSchema = z.strictObject({
101
104
  reasoning: reasoningOption,
102
105
  });
103
106
 
104
- export type AskGatewayAdapter = AdapterDescriptor<"gateway", AskGatewayOptions>;
107
+ export type AssistantGatewayAdapter = AdapterDescriptor<
108
+ "gateway",
109
+ AssistantGatewayOptions
110
+ >;
105
111
 
106
112
  export const gatewayAdapterSchema = adapterDescriptorSchema(
107
113
  "gateway",
@@ -109,14 +115,14 @@ export const gatewayAdapterSchema = adapterDescriptorSchema(
109
115
  );
110
116
 
111
117
  /**
112
- * Route Ask AI through the Vercel AI Gateway (the default). `model` is a
118
+ * Route the assistant through the Vercel AI Gateway (the default). `model` is a
113
119
  * `provider/model` string; the key is `AI_GATEWAY_API_KEY`, or Vercel's OIDC
114
120
  * token when deployed there. Needs no provider SDK beyond the `ai` package
115
121
  * Blume ships.
116
122
  */
117
123
  export const gateway = (
118
- options: AskGatewayOptions = {}
119
- ): AskGatewayAdapter => ({
124
+ options: AssistantGatewayOptions = {}
125
+ ): AssistantGatewayAdapter => ({
120
126
  kind: "gateway",
121
127
  options,
122
128
  requiredSecrets: [options.apiKeyEnv ?? GATEWAY_API_KEY_ENV],
@@ -130,7 +136,7 @@ export const gateway = (
130
136
  const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
131
137
 
132
138
  /** Options for {@link openrouter}. */
133
- export interface AskOpenRouterOptions extends AskAdapterOptions {
139
+ export interface AssistantOpenRouterOptions extends AssistantAdapterOptions {
134
140
  /** The OpenRouter model id (`anthropic/claude-sonnet-4-5`). */
135
141
  model: string;
136
142
  /**
@@ -138,7 +144,7 @@ export interface AskOpenRouterOptions extends AskAdapterOptions {
138
144
  * OpenRouter's `reasoning.effort`, because its provider ignores the AI
139
145
  * SDK's call-level option. Omitted keeps the model's default.
140
146
  */
141
- reasoning?: AskReasoning;
147
+ reasoning?: AssistantReasoning;
142
148
  }
143
149
 
144
150
  const openrouterOptionsSchema = z.strictObject({
@@ -147,9 +153,9 @@ const openrouterOptionsSchema = z.strictObject({
147
153
  reasoning: reasoningOption,
148
154
  });
149
155
 
150
- export type AskOpenRouterAdapter = AdapterDescriptor<
156
+ export type AssistantOpenRouterAdapter = AdapterDescriptor<
151
157
  "openrouter",
152
- AskOpenRouterOptions
158
+ AssistantOpenRouterOptions
153
159
  >;
154
160
 
155
161
  export const openrouterAdapterSchema = adapterDescriptorSchema(
@@ -158,12 +164,12 @@ export const openrouterAdapterSchema = adapterDescriptorSchema(
158
164
  );
159
165
 
160
166
  /**
161
- * Route Ask AI through OpenRouter. Reads `OPENROUTER_API_KEY` and needs
167
+ * Route the assistant through OpenRouter. Reads `OPENROUTER_API_KEY` and needs
162
168
  * `@openrouter/ai-sdk-provider` installed in the project.
163
169
  */
164
170
  export const openrouter = (
165
- options: AskOpenRouterOptions
166
- ): AskOpenRouterAdapter => ({
171
+ options: AssistantOpenRouterOptions
172
+ ): AssistantOpenRouterAdapter => ({
167
173
  kind: "openrouter",
168
174
  options,
169
175
  requiredSecrets: [options.apiKeyEnv ?? OPENROUTER_API_KEY_ENV],
@@ -177,7 +183,7 @@ export const openrouter = (
177
183
  const LLMGATEWAY_API_KEY_ENV = "LLMGATEWAY_API_KEY";
178
184
 
179
185
  /** Options for {@link llmgateway}. */
180
- export interface AskLlmGatewayOptions extends AskAdapterOptions {
186
+ export interface AssistantLlmGatewayOptions extends AssistantAdapterOptions {
181
187
  /** Overrides the preset endpoint (`https://api.llmgateway.io/v1`). */
182
188
  baseUrl?: string;
183
189
  /** The model id LLMGateway serves. */
@@ -186,7 +192,7 @@ export interface AskLlmGatewayOptions extends AskAdapterOptions {
186
192
  * How much the model reasons before answering, sent in the request as
187
193
  * `reasoning_effort`. Omitted keeps the model's default.
188
194
  */
189
- reasoning?: AskReasoning;
195
+ reasoning?: AssistantReasoning;
190
196
  }
191
197
 
192
198
  const llmgatewayOptionsSchema = z.strictObject({
@@ -196,9 +202,9 @@ const llmgatewayOptionsSchema = z.strictObject({
196
202
  reasoning: reasoningOption,
197
203
  });
198
204
 
199
- export type AskLlmGatewayAdapter = AdapterDescriptor<
205
+ export type AssistantLlmGatewayAdapter = AdapterDescriptor<
200
206
  "llmgateway",
201
- AskLlmGatewayOptions
207
+ AssistantLlmGatewayOptions
202
208
  >;
203
209
 
204
210
  export const llmgatewayAdapterSchema = adapterDescriptorSchema(
@@ -207,12 +213,12 @@ export const llmgatewayAdapterSchema = adapterDescriptorSchema(
207
213
  );
208
214
 
209
215
  /**
210
- * Route Ask AI through LLMGateway's OpenAI-compatible endpoint. Reads
216
+ * Route the assistant through LLMGateway's OpenAI-compatible endpoint. Reads
211
217
  * `LLMGATEWAY_API_KEY` and needs `@ai-sdk/openai-compatible` installed.
212
218
  */
213
219
  export const llmgateway = (
214
- options: AskLlmGatewayOptions
215
- ): AskLlmGatewayAdapter => ({
220
+ options: AssistantLlmGatewayOptions
221
+ ): AssistantLlmGatewayAdapter => ({
216
222
  kind: "llmgateway",
217
223
  options,
218
224
  requiredSecrets: [options.apiKeyEnv ?? LLMGATEWAY_API_KEY_ENV],
@@ -229,7 +235,7 @@ const INKEEP_API_KEY_ENV = "INKEEP_API_KEY";
229
235
  * Options for {@link inkeep}. No `reasoning`: Inkeep runs its own QA pipeline
230
236
  * behind an OpenAI-compatible endpoint with no reasoning control.
231
237
  */
232
- export interface AskInkeepOptions extends AskAdapterOptions {
238
+ export interface AssistantInkeepOptions extends AssistantAdapterOptions {
233
239
  /** Overrides the preset endpoint (`https://api.inkeep.com/v1`). */
234
240
  baseUrl?: string;
235
241
  /** The Inkeep QA model id. */
@@ -242,7 +248,10 @@ const inkeepOptionsSchema = z.strictObject({
242
248
  model: z.string().min(1),
243
249
  });
244
250
 
245
- export type AskInkeepAdapter = AdapterDescriptor<"inkeep", AskInkeepOptions>;
251
+ export type AssistantInkeepAdapter = AdapterDescriptor<
252
+ "inkeep",
253
+ AssistantInkeepOptions
254
+ >;
246
255
 
247
256
  export const inkeepAdapterSchema = adapterDescriptorSchema(
248
257
  "inkeep",
@@ -254,7 +263,9 @@ export const inkeepAdapterSchema = adapterDescriptorSchema(
254
263
  * dashboard, so Blume leaves it ungrounded and it takes no `reasoning`.
255
264
  * Reads `INKEEP_API_KEY` and needs `@ai-sdk/openai-compatible` installed.
256
265
  */
257
- export const inkeep = (options: AskInkeepOptions): AskInkeepAdapter => ({
266
+ export const inkeep = (
267
+ options: AssistantInkeepOptions
268
+ ): AssistantInkeepAdapter => ({
258
269
  kind: "inkeep",
259
270
  options,
260
271
  requiredSecrets: [options.apiKeyEnv ?? INKEEP_API_KEY_ENV],
@@ -266,7 +277,7 @@ export const inkeep = (options: AskInkeepOptions): AskInkeepAdapter => ({
266
277
  // ---------------------------------------------------------------------------
267
278
 
268
279
  /** Options for {@link openaiCompatible}. */
269
- export interface AskOpenAICompatibleOptions extends AskAdapterOptions {
280
+ export interface AssistantOpenAICompatibleOptions extends AssistantAdapterOptions {
270
281
  /** Name of the env var holding the endpoint's API key. */
271
282
  apiKeyEnv: string;
272
283
  /** The endpoint's base URL (`https://my-gateway.example.com/v1`). */
@@ -280,7 +291,7 @@ export interface AskOpenAICompatibleOptions extends AskAdapterOptions {
280
291
  * `reasoning_effort`, so the endpoint has to accept that parameter.
281
292
  * Omitted keeps the model's default.
282
293
  */
283
- reasoning?: AskReasoning;
294
+ reasoning?: AssistantReasoning;
284
295
  }
285
296
 
286
297
  const openaiCompatibleOptionsSchema = z.strictObject({
@@ -293,9 +304,9 @@ const openaiCompatibleOptionsSchema = z.strictObject({
293
304
  reasoning: reasoningOption,
294
305
  });
295
306
 
296
- export type AskOpenAICompatibleAdapter = AdapterDescriptor<
307
+ export type AssistantOpenAICompatibleAdapter = AdapterDescriptor<
297
308
  "openai-compatible",
298
- AskOpenAICompatibleOptions
309
+ AssistantOpenAICompatibleOptions
299
310
  >;
300
311
 
301
312
  export const openaiCompatibleAdapterSchema = adapterDescriptorSchema(
@@ -304,13 +315,13 @@ export const openaiCompatibleAdapterSchema = adapterDescriptorSchema(
304
315
  );
305
316
 
306
317
  /**
307
- * Route Ask AI through any OpenAI-compatible endpoint: supply its `baseUrl`,
318
+ * Route the assistant through any OpenAI-compatible endpoint: supply its `baseUrl`,
308
319
  * the `model` it serves, and the env var holding its key. Needs
309
320
  * `@ai-sdk/openai-compatible` installed.
310
321
  */
311
322
  export const openaiCompatible = (
312
- options: AskOpenAICompatibleOptions
313
- ): AskOpenAICompatibleAdapter => ({
323
+ options: AssistantOpenAICompatibleOptions
324
+ ): AssistantOpenAICompatibleAdapter => ({
314
325
  kind: "openai-compatible",
315
326
  options,
316
327
  requiredSecrets: [options.apiKeyEnv],
@@ -318,16 +329,16 @@ export const openaiCompatible = (
318
329
  });
319
330
 
320
331
  // ---------------------------------------------------------------------------
321
- // The `ai.ask.provider` schema
332
+ // The `ai.assistant.provider` schema
322
333
  // ---------------------------------------------------------------------------
323
334
 
324
- /** Which backend answers Ask AI: the value of `gateway()`, `openrouter()`, … */
325
- export type AskAdapter =
326
- | AskGatewayAdapter
327
- | AskOpenRouterAdapter
328
- | AskLlmGatewayAdapter
329
- | AskInkeepAdapter
330
- | AskOpenAICompatibleAdapter;
335
+ /** Which backend answers the assistant: the value of `gateway()`, `openrouter()`, … */
336
+ export type AssistantAdapter =
337
+ | AssistantGatewayAdapter
338
+ | AssistantOpenRouterAdapter
339
+ | AssistantLlmGatewayAdapter
340
+ | AssistantInkeepAdapter
341
+ | AssistantOpenAICompatibleAdapter;
331
342
 
332
343
  // ---------------------------------------------------------------------------
333
344
  // Blume 1 hints
@@ -354,29 +365,29 @@ const MOVED_TO_PROVIDER: ReadonlySet<string> = new Set([
354
365
  "reasoning",
355
366
  ]);
356
367
 
357
- /** The provider an `ai.ask` object names: a 1.x string or a descriptor's `kind`. */
358
- const askProviderNameProbe = z.looseObject({
368
+ /** The provider an `ai.assistant` object names: a 1.x string or a descriptor's `kind`. */
369
+ const assistantProviderNameProbe = z.looseObject({
359
370
  provider: z.union([
360
371
  z.string(),
361
372
  z.looseObject({ kind: z.string() }).transform(({ kind }) => kind),
362
373
  ]),
363
374
  });
364
375
 
365
- /** The factory a failing `ai.ask` object's provider names, or the gateway. */
376
+ /** The factory a failing `ai.assistant` object's provider names, or the gateway. */
366
377
  const providerFactory = (issue: z.core.$ZodRawIssue): string => {
367
- const probe = askProviderNameProbe.safeParse(issue.input);
378
+ const probe = assistantProviderNameProbe.safeParse(issue.input);
368
379
  return (
369
380
  (probe.success && FACTORY_BY_PROVIDER.get(probe.data.provider)) || "gateway"
370
381
  );
371
382
  };
372
383
 
373
384
  /**
374
- * Error params for `ai.ask`: a 1.x flat provider field (`model`, `apiKeyEnv`,
385
+ * Error params for `ai.assistant`: a 1.x flat provider field (`model`, `apiKeyEnv`,
375
386
  * `baseUrl`, `headers`, `reasoning`) names the adapter call it moves into —
376
387
  * the one the object's `provider` picks, 1.x string or descriptor — instead
377
388
  * of Zod's bare "Unrecognized key"; any other unknown key keeps its wording.
378
389
  */
379
- export const askMovedFieldsHint = {
390
+ export const assistantMovedFieldsHint = {
380
391
  error: (issue: z.core.$ZodRawIssue): string | undefined => {
381
392
  if (issue.code !== "unrecognized_keys") {
382
393
  return;
@@ -385,7 +396,7 @@ export const askMovedFieldsHint = {
385
396
  if (moved.length === 0) {
386
397
  return;
387
398
  }
388
- const fields = moved.map((key) => `ai.ask.${key}`).join(", ");
399
+ const fields = moved.map((key) => `ai.assistant.${key}`).join(", ");
389
400
  const hint = `${fields} moved into the provider adapter: \`provider: ${providerFactory(issue)}({ ${moved.join(", ")} })\`, imported from "blume/ai".`;
390
401
  const others = issue.keys.filter((key) => !MOVED_TO_PROVIDER.has(key));
391
402
  return others.length > 0
@@ -394,27 +405,27 @@ export const askMovedFieldsHint = {
394
405
  },
395
406
  };
396
407
 
397
- /** A value that is a 1.x provider name, for the `ai.ask.provider` hint. */
408
+ /** A value that is a 1.x provider name, for the `ai.assistant.provider` hint. */
398
409
  const providerNameProbe = z.string();
399
410
 
400
411
  /**
401
- * The message for an `ai.ask.provider` that isn't a descriptor: a 1.x
412
+ * The message for an `ai.assistant.provider` that isn't a descriptor: a 1.x
402
413
  * provider name names the factory that replaced it; anything else lists them.
403
414
  */
404
415
  const providerNotAdapterMessage = (issue: z.core.$ZodRawIssue): string => {
405
416
  const name = providerNameProbe.safeParse(issue.input);
406
417
  const factory = name.success ? FACTORY_BY_PROVIDER.get(name.data) : undefined;
407
418
  return factory
408
- ? `ai.ask.provider takes an adapter from "blume/ai", not a provider name: \`provider: ${factory}({ model })\`. The 1.x model, apiKeyEnv, baseUrl, headers, and reasoning fields move into the call.`
409
- : 'ai.ask.provider takes an adapter from "blume/ai": gateway(), openrouter(), llmgateway(), inkeep(), or openaiCompatible().';
419
+ ? `ai.assistant.provider takes an adapter from "blume/ai", not a provider name: \`provider: ${factory}({ model })\`. The 1.x model, apiKeyEnv, baseUrl, headers, and reasoning fields move into the call.`
420
+ : 'ai.assistant.provider takes an adapter from "blume/ai": gateway(), openrouter(), llmgateway(), inkeep(), or openaiCompatible().';
410
421
  };
411
422
 
412
423
  /**
413
- * `ai.ask.provider`: the descriptor an adapter factory returned, validated
424
+ * `ai.assistant.provider`: the descriptor an adapter factory returned, validated
414
425
  * against that adapter's own option schema. A value that isn't a descriptor
415
426
  * at all (a 1.x provider name) names the factory that replaced it.
416
427
  */
417
- export const askAdapterSchema = z.discriminatedUnion(
428
+ export const assistantAdapterSchema = z.discriminatedUnion(
418
429
  "kind",
419
430
  [
420
431
  gatewayAdapterSchema,
@@ -434,12 +445,12 @@ export const askAdapterSchema = z.discriminatedUnion(
434
445
  }
435
446
  );
436
447
 
437
- /** A resolved (post-defaults) `ai.ask.provider` descriptor. */
438
- export type AskAdapterConfig = z.output<typeof askAdapterSchema>;
439
- export type AskAdapterKind = AskAdapterConfig["kind"];
448
+ /** A resolved (post-defaults) `ai.assistant.provider` descriptor. */
449
+ export type AssistantAdapterConfig = z.output<typeof assistantAdapterSchema>;
450
+ export type AssistantAdapterKind = AssistantAdapterConfig["kind"];
440
451
 
441
- /** The provider when `ai.ask.provider` is unset: the gateway with its defaults. */
442
- export const DEFAULT_ASK_PROVIDER: AskAdapter = gateway();
452
+ /** The provider when `ai.assistant.provider` is unset: the gateway with its defaults. */
453
+ export const DEFAULT_ASSISTANT_PROVIDER: AssistantAdapter = gateway();
443
454
 
444
455
  // ---------------------------------------------------------------------------
445
456
  // Resolved backend
@@ -474,7 +485,7 @@ export interface AskBackendTemplate {
474
485
  export interface AskBackend {
475
486
  /** Whether Blume grounds answers in the docs (Inkeep retrieves itself). */
476
487
  grounded: boolean;
477
- kind: AskAdapterKind;
488
+ kind: AssistantAdapterKind;
478
489
  /** Human-readable adapter name for diagnostics ("AI Gateway"). */
479
490
  label: string;
480
491
  /** Appended to the missing-secret warning (an alternative credential, say). */
@@ -499,7 +510,7 @@ const headersLine = (headers?: Record<string, string>): string =>
499
510
  /** Refuse a request up front when the adapter's key env var is unset. */
500
511
  const keyCheck = (env: string): string => ` if (!${secretExpr(env)}) {
501
512
  return new Response(
502
- ${JSON.stringify(`Ask AI is not configured: set ${env}.`)},
513
+ ${JSON.stringify(`The assistant is not configured: set ${env}.`)},
503
514
  { status: 503 }
504
515
  );
505
516
  }`;
@@ -510,7 +521,7 @@ const keyCheck = (env: string): string => ` if (!${secretExpr(env)}) {
510
521
  */
511
522
  const callFields = (options: {
512
523
  providerOptions?: z.output<typeof providerOptionsSchema>;
513
- reasoning?: AskReasoning;
524
+ reasoning?: AssistantReasoning;
514
525
  }): string[] => {
515
526
  const fields: string[] = [];
516
527
  if (options.reasoning) {
@@ -542,7 +553,7 @@ const gatewayBackend = (
542
553
  keyCheck: ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
543
554
  if (!(${secretExpr(options.apiKeyEnv)} || getSecret("VERCEL_OIDC_TOKEN"))) {
544
555
  return new Response(
545
- ${JSON.stringify(`Ask AI is not configured: set ${options.apiKeyEnv} (or deploy on Vercel with OIDC).`)},
556
+ ${JSON.stringify(`The assistant is not configured: set ${options.apiKeyEnv} (or deploy on Vercel with OIDC).`)},
546
557
  { status: 503 }
547
558
  );
548
559
  }`,
@@ -587,7 +598,7 @@ const openrouterBackend = (
587
598
  * option as `reasoning_effort`.
588
599
  */
589
600
  const openaiCompatibleBackend = (
590
- kind: AskAdapterKind,
601
+ kind: AssistantAdapterKind,
591
602
  label: string,
592
603
  grounded: boolean,
593
604
  options: {
@@ -597,7 +608,7 @@ const openaiCompatibleBackend = (
597
608
  model: string;
598
609
  name: string;
599
610
  providerOptions?: z.output<typeof providerOptionsSchema>;
600
- reasoning?: AskReasoning;
611
+ reasoning?: AssistantReasoning;
601
612
  }
602
613
  ): AskBackend => ({
603
614
  grounded,
@@ -621,9 +632,11 @@ const openaiCompatibleBackend = (
621
632
  },
622
633
  });
623
634
 
624
- /** Resolve a parsed `ai.ask.provider` descriptor into its backend. */
635
+ /** Resolve a parsed `ai.assistant.provider` descriptor into its backend. */
625
636
  export const resolveAskBackend = (
626
- provider: AskAdapterConfig = askAdapterSchema.parse(DEFAULT_ASK_PROVIDER)
637
+ provider: AssistantAdapterConfig = assistantAdapterSchema.parse(
638
+ DEFAULT_ASSISTANT_PROVIDER
639
+ )
627
640
  ): AskBackend => {
628
641
  switch (provider.kind) {
629
642
  case "gateway": {