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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +2 -2
- package/dist/cli/{chunk-f7t03s3g.js → chunk-27g6wdth.js} +2 -2
- package/dist/cli/{chunk-mnqj32sj.js → chunk-2hn4b8z7.js} +13 -13
- package/dist/cli/chunk-2hn4b8z7.js.map +12 -0
- package/dist/cli/{chunk-by2290sx.js → chunk-5shv93fd.js} +2 -2
- package/dist/cli/{chunk-a9kptbw5.js → chunk-6crbhc3x.js} +3 -3
- package/dist/cli/{chunk-a9kptbw5.js.map → chunk-6crbhc3x.js.map} +1 -1
- package/dist/cli/{chunk-mwt1k8n7.js → chunk-6hsn950k.js} +20 -20
- package/dist/cli/chunk-6hsn950k.js.map +10 -0
- package/dist/cli/{chunk-11j0384y.js → chunk-6vm74dry.js} +13 -13
- package/dist/cli/{chunk-11j0384y.js.map → chunk-6vm74dry.js.map} +3 -3
- package/dist/cli/{chunk-j8mw0za6.js → chunk-79jhk4py.js} +8 -8
- package/dist/cli/{chunk-j8mw0za6.js.map → chunk-79jhk4py.js.map} +2 -2
- package/dist/cli/{chunk-nk3ts2xk.js → chunk-82bbrxdn.js} +2 -2
- package/dist/cli/{chunk-2q1dwty4.js → chunk-ah61y8py.js} +8 -8
- package/dist/cli/{chunk-2q1dwty4.js.map → chunk-ah61y8py.js.map} +4 -4
- package/dist/cli/{chunk-zxccj738.js → chunk-ce574jw2.js} +1 -1
- package/dist/cli/{chunk-y3e45rc8.js → chunk-ch6g3ar0.js} +3 -3
- package/dist/cli/{chunk-beat36xx.js → chunk-dh8cwk36.js} +5 -5
- package/dist/cli/{chunk-beat36xx.js.map → chunk-dh8cwk36.js.map} +2 -2
- package/dist/cli/{chunk-1w8dp3qb.js → chunk-epjnccmv.js} +13 -13
- package/dist/cli/{chunk-ernrthtr.js → chunk-f2z5v128.js} +13 -13
- package/dist/cli/{chunk-zg2gtj10.js → chunk-fs23ddbb.js} +2 -2
- package/dist/cli/{chunk-7ez8ny0t.js → chunk-fxypxtvm.js} +2 -2
- package/dist/cli/{chunk-tzne8qfq.js → chunk-fz5wtpmh.js} +13 -13
- package/dist/cli/{chunk-b5aj94ah.js → chunk-hdpx1tax.js} +4 -4
- package/dist/cli/{chunk-d80hr03s.js → chunk-jwyddg7y.js} +9 -9
- package/dist/cli/{chunk-d80hr03s.js.map → chunk-jwyddg7y.js.map} +2 -2
- package/dist/cli/{chunk-fh5hj5jt.js → chunk-kdp5q7ke.js} +15 -15
- package/dist/cli/{chunk-6k8vp3ta.js → chunk-kpf8rrjc.js} +9 -9
- package/dist/cli/{chunk-6k8vp3ta.js.map → chunk-kpf8rrjc.js.map} +3 -3
- package/dist/cli/{chunk-5a2z0198.js → chunk-m3vmjgmq.js} +9 -9
- package/dist/cli/{chunk-5a2z0198.js.map → chunk-m3vmjgmq.js.map} +2 -2
- package/dist/cli/{chunk-bctazmbk.js → chunk-mb2919y2.js} +4 -4
- package/dist/cli/{chunk-79njf86q.js → chunk-q5163e60.js} +13 -13
- package/dist/cli/{chunk-xaz13gwg.js → chunk-qkqwkpte.js} +196 -208
- package/dist/cli/{chunk-xaz13gwg.js.map → chunk-qkqwkpte.js.map} +48 -48
- package/dist/cli/{chunk-sqn5t4q0.js → chunk-qs4q5p4e.js} +3 -3
- package/dist/cli/{chunk-bw22s759.js → chunk-qwsrynx5.js} +1 -1
- package/dist/cli/{chunk-z01ze5c1.js → chunk-s1p84fyh.js} +15 -15
- package/dist/cli/{chunk-pnnvybbk.js → chunk-s6jhgk0q.js} +5 -5
- package/dist/cli/{chunk-pnnvybbk.js.map → chunk-s6jhgk0q.js.map} +2 -2
- package/dist/cli/{chunk-f2972sbt.js → chunk-vtk4a6dg.js} +1 -1
- package/dist/cli/{chunk-z1f5arsg.js → chunk-wgm7m9qk.js} +21 -21
- package/dist/cli/{chunk-z1f5arsg.js.map → chunk-wgm7m9qk.js.map} +4 -4
- package/dist/cli/{chunk-bnbmcwfb.js → chunk-wm7js3j9.js} +5 -5
- package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-wm7js3j9.js.map} +3 -3
- package/dist/cli/{chunk-d1tadaw7.js → chunk-yt5n7ppj.js} +3 -3
- package/dist/cli/{chunk-pat2zzwc.js → chunk-yw7dm696.js} +1 -1
- package/dist/cli/{chunk-pat2zzwc.js.map → chunk-yw7dm696.js.map} +1 -1
- package/dist/cli/{chunk-88cpgt6h.js → chunk-zxcczpyx.js} +1 -1
- package/dist/cli/{chunk-41za066z.js → chunk-zxh4d9vy.js} +4 -4
- package/dist/cli/index.js +17 -17
- package/dist/types/ai/agent-readability.d.ts +1 -1
- package/dist/types/ai/api/paths.d.ts +1 -1
- package/dist/types/ai/ask-context.d.ts +7 -7
- package/dist/types/ai/ask.d.ts +43 -43
- package/dist/types/ai/index.d.ts +3 -3
- package/dist/types/ai/openapi-components.d.ts +1 -1
- package/dist/types/ai/serializers.d.ts +1 -1
- package/dist/types/ai/visibility.d.ts +1 -1
- package/dist/types/core/config-input.d.ts +17 -17
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +3 -3
- package/dist/types/core/i18n-ui.d.ts +6 -8
- package/dist/types/core/schema.d.ts +5 -5
- package/dist/types/core/unrecognized-keys.d.ts +1 -1
- package/dist/types/search/documents.d.ts +1 -1
- package/dist/types/search/orama-index.d.ts +1 -1
- package/docs/02-deployment.mdx +4 -4
- package/docs/03-upgrading.mdx +22 -9
- package/docs/04-migrating.mdx +4 -4
- package/docs/08-faq.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +2 -2
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +3 -3
- package/docs/cli/doctor.mdx +3 -3
- package/docs/cli/evals.mdx +7 -7
- package/docs/cli/index.mdx +2 -2
- package/docs/cli/translate.mdx +8 -8
- package/docs/configuration/{ask-ai.mdx → assistant.mdx} +19 -19
- package/docs/configuration/customization.mdx +2 -2
- package/docs/configuration/index.mdx +2 -2
- package/docs/configuration/meta.ts +1 -1
- package/docs/configuration/search.mdx +1 -1
- package/docs/content/i18n.mdx +2 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/index.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume/SKILL.md +5 -5
- package/skills/blume-migrate/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +4 -4
- package/src/ai/api/paths.ts +1 -1
- package/src/ai/ask-context.ts +7 -7
- package/src/ai/ask-data.ts +2 -2
- package/src/ai/ask.ts +84 -71
- package/src/ai/cors.ts +3 -3
- package/src/ai/index.ts +16 -16
- package/src/ai/openapi-components.ts +1 -1
- package/src/ai/serializers.ts +1 -1
- package/src/ai/visibility.ts +1 -1
- package/src/astro/generate.ts +19 -18
- package/src/astro/module-types.ts +1 -1
- package/src/astro/runtime-deps.ts +6 -6
- package/src/astro/templates.ts +26 -26
- package/src/blume-modules.d.ts +2 -2
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/eval.ts +3 -3
- package/src/cli/commands/migrate.ts +2 -2
- package/src/cli/commands/translate.ts +3 -3
- package/src/cli/commands/upgrade.ts +2 -2
- package/src/cli/required-secrets.ts +3 -3
- package/src/components/copy-feedback.ts +1 -1
- package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
- package/src/components/islands/{ask-ai.tsx → assistant.tsx} +23 -23
- package/src/components/islands/hooks.ts +14 -12
- package/src/components/layout/Header.astro +10 -10
- package/src/components/layout/PageLayout.astro +6 -6
- package/src/components/layout/Pagination.astro +7 -7
- package/src/components/layout/ReferenceLayout.astro +1 -1
- package/src/components/layout/RootLayout.astro +6 -6
- package/src/components/layout/Search.astro +13 -13
- package/src/components/layout/analytics-client.ts +1 -1
- package/src/components/layout/drawer-inert.ts +1 -1
- package/src/components/openapi/description.ts +2 -2
- package/src/core/code-fences.ts +1 -1
- package/src/core/config-input.ts +19 -19
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +3 -3
- package/src/core/i18n-ui.ts +35 -9
- package/src/core/request-body.ts +1 -1
- package/src/core/schema.ts +25 -20
- package/src/core/server-features.ts +2 -2
- package/src/core/ui-packs/ar.ts +4 -5
- package/src/core/ui-packs/bg.ts +4 -5
- package/src/core/ui-packs/bn.ts +4 -5
- package/src/core/ui-packs/ca.ts +4 -5
- package/src/core/ui-packs/cs.ts +4 -5
- package/src/core/ui-packs/da.ts +4 -5
- package/src/core/ui-packs/de.ts +4 -5
- package/src/core/ui-packs/el.ts +4 -5
- package/src/core/ui-packs/es.ts +4 -5
- package/src/core/ui-packs/fa.ts +4 -5
- package/src/core/ui-packs/fi.ts +4 -5
- package/src/core/ui-packs/fr.ts +4 -5
- package/src/core/ui-packs/he.ts +4 -5
- package/src/core/ui-packs/hi.ts +4 -5
- package/src/core/ui-packs/hr.ts +4 -5
- package/src/core/ui-packs/hu.ts +4 -5
- package/src/core/ui-packs/id.ts +4 -5
- package/src/core/ui-packs/it.ts +4 -5
- package/src/core/ui-packs/ja.ts +4 -5
- package/src/core/ui-packs/ko.ts +4 -5
- package/src/core/ui-packs/nl.ts +4 -5
- package/src/core/ui-packs/no.ts +4 -5
- package/src/core/ui-packs/pl.ts +4 -5
- package/src/core/ui-packs/pt-br.ts +4 -5
- package/src/core/ui-packs/pt.ts +4 -5
- package/src/core/ui-packs/ro.ts +4 -5
- package/src/core/ui-packs/ru.ts +4 -5
- package/src/core/ui-packs/sk.ts +4 -5
- package/src/core/ui-packs/sr.ts +4 -5
- package/src/core/ui-packs/sv.ts +4 -5
- package/src/core/ui-packs/th.ts +4 -5
- package/src/core/ui-packs/tr.ts +4 -5
- package/src/core/ui-packs/uk.ts +4 -5
- package/src/core/ui-packs/vi.ts +4 -5
- package/src/core/ui-packs/zh-tw.ts +4 -5
- package/src/core/ui-packs/zh.ts +4 -5
- package/src/core/unrecognized-keys.ts +1 -1
- package/src/registry/eject.ts +19 -18
- package/src/search/documents.ts +2 -2
- package/src/search/orama-index.ts +1 -1
- package/src/translate/report.ts +1 -1
- package/src/upgrade/upgrade.ts +1 -1
- package/dist/cli/chunk-mnqj32sj.js.map +0 -12
- package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
- /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-27g6wdth.js.map} +0 -0
- /package/dist/cli/{chunk-by2290sx.js.map → chunk-5shv93fd.js.map} +0 -0
- /package/dist/cli/{chunk-nk3ts2xk.js.map → chunk-82bbrxdn.js.map} +0 -0
- /package/dist/cli/{chunk-zxccj738.js.map → chunk-ce574jw2.js.map} +0 -0
- /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-ch6g3ar0.js.map} +0 -0
- /package/dist/cli/{chunk-1w8dp3qb.js.map → chunk-epjnccmv.js.map} +0 -0
- /package/dist/cli/{chunk-ernrthtr.js.map → chunk-f2z5v128.js.map} +0 -0
- /package/dist/cli/{chunk-zg2gtj10.js.map → chunk-fs23ddbb.js.map} +0 -0
- /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-fxypxtvm.js.map} +0 -0
- /package/dist/cli/{chunk-tzne8qfq.js.map → chunk-fz5wtpmh.js.map} +0 -0
- /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-hdpx1tax.js.map} +0 -0
- /package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-kdp5q7ke.js.map} +0 -0
- /package/dist/cli/{chunk-bctazmbk.js.map → chunk-mb2919y2.js.map} +0 -0
- /package/dist/cli/{chunk-79njf86q.js.map → chunk-q5163e60.js.map} +0 -0
- /package/dist/cli/{chunk-sqn5t4q0.js.map → chunk-qs4q5p4e.js.map} +0 -0
- /package/dist/cli/{chunk-bw22s759.js.map → chunk-qwsrynx5.js.map} +0 -0
- /package/dist/cli/{chunk-z01ze5c1.js.map → chunk-s1p84fyh.js.map} +0 -0
- /package/dist/cli/{chunk-f2972sbt.js.map → chunk-vtk4a6dg.js.map} +0 -0
- /package/dist/cli/{chunk-d1tadaw7.js.map → chunk-yt5n7ppj.js.map} +0 -0
- /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-zxcczpyx.js.map} +0 -0
- /package/dist/cli/{chunk-41za066z.js.map → chunk-zxh4d9vy.js.map} +0 -0
package/package.json
CHANGED
package/skills/blume/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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. (`--
|
|
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] --
|
|
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
|
|
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`** (
|
|
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 (
|
|
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
|
|
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,
|
|
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.
|
|
198
|
-
artifacts.askApi = askApiUrl(config.ai.
|
|
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) {
|
package/src/ai/api/paths.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/src/ai/ask-context.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 = (
|
package/src/ai/ask-data.ts
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39
|
-
export interface
|
|
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?:
|
|
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(
|
|
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
|
|
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?:
|
|
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
|
|
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
|
|
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:
|
|
119
|
-
):
|
|
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
|
|
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?:
|
|
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
|
|
156
|
+
export type AssistantOpenRouterAdapter = AdapterDescriptor<
|
|
151
157
|
"openrouter",
|
|
152
|
-
|
|
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
|
|
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:
|
|
166
|
-
):
|
|
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
|
|
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?:
|
|
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
|
|
205
|
+
export type AssistantLlmGatewayAdapter = AdapterDescriptor<
|
|
200
206
|
"llmgateway",
|
|
201
|
-
|
|
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
|
|
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:
|
|
215
|
-
):
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
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?:
|
|
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
|
|
307
|
+
export type AssistantOpenAICompatibleAdapter = AdapterDescriptor<
|
|
297
308
|
"openai-compatible",
|
|
298
|
-
|
|
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
|
|
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:
|
|
313
|
-
):
|
|
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.
|
|
332
|
+
// The `ai.assistant.provider` schema
|
|
322
333
|
// ---------------------------------------------------------------------------
|
|
323
334
|
|
|
324
|
-
/** Which backend answers
|
|
325
|
-
export type
|
|
326
|
-
|
|
|
327
|
-
|
|
|
328
|
-
|
|
|
329
|
-
|
|
|
330
|
-
|
|
|
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.
|
|
358
|
-
const
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
409
|
-
: 'ai.
|
|
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.
|
|
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
|
|
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.
|
|
438
|
-
export type
|
|
439
|
-
export type
|
|
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.
|
|
442
|
-
export const
|
|
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:
|
|
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(`
|
|
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?:
|
|
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(`
|
|
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:
|
|
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?:
|
|
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.
|
|
635
|
+
/** Resolve a parsed `ai.assistant.provider` descriptor into its backend. */
|
|
625
636
|
export const resolveAskBackend = (
|
|
626
|
-
provider:
|
|
637
|
+
provider: AssistantAdapterConfig = assistantAdapterSchema.parse(
|
|
638
|
+
DEFAULT_ASSISTANT_PROVIDER
|
|
639
|
+
)
|
|
627
640
|
): AskBackend => {
|
|
628
641
|
switch (provider.kind) {
|
|
629
642
|
case "gateway": {
|