ADR-0049: Providers are dynamically-loaded modules from an operator-local store; catalog from a self-hosted models.dev mirror; @kaged/llm becomes resolver + loader + middleware

  • Status: Accepted
  • Date: 2026-06-25
  • Deciders: @karasu
  • Supersedes: ADR-0014 (partial — see § Relationship to ADR-0014)
  • Superseded by:

Context

Two problems converge on one answer.

The data problem. Model metadata comes from a bundled LiteLLM snapshot that chronically lags real provider lineups, and model discovery is N bespoke listModels() fetchers that disagree with what providers actually serve. ADR-0026's override system, built to correct stale data, has become the everyday workaround.

The integration problem. @kaged/llm hand-implements six wire protocols as pure-fetch adapters with their own SSE/partial-JSON parsers. ADR-0014 chose this to avoid the @ai-sdk/* tree, reasoning that OAuth providers have no package (so packages → two paths), that hand adapters give inline control, and that @ai-sdk/* carry Node/Bun friction. The standing cost: provider drift is ours, and the spec already records tech debt (the generic openai-responses adapter drops reasoning output).

models.dev (MIT, by Anomaly / the SST team) answers the data problem — an open catalog of 75+ providers, generated from TOML, published as api.json — and each provider entry names the AI SDK package and base URL that serves it (npm / api / env). That turns the npm field into instantiation instructions for a maintained package, and the "two paths" reasoning dissolves once the resolution is seen clearly: every provider — package-backed or custom — resolves to a single LanguageModelV2. Standard providers get it from the catalog-named @ai-sdk/* package; custom/OAuth providers get it from a plugin that produces the same LanguageModelV2. One interface; the package-vs-plugin split is only where the object comes from.

This is in production: OpenCode — a Bun/TypeScript project by the models.dev team — runs exactly this (AI SDK packages for catalog providers, per-model npm override, plus plugins for custom providers like opencode-antigravity-auth, 2.2k★), and it moved provider-auth out of core into plugins in 1.3.0.

The refinement that makes this lean. Rather than bundling provider packages into the daemon binary (ADR-0041 ships a bun build --compile executable), kaged loads them dynamically from an operator-local store, n8n-style. Bun's --compile bundles only statically imported packages; opaque/computed dynamic-import specifiers pass through to runtime and resolve against the real filesystem (confirmed in Bun's bundler docs; the same pattern OpenCode-style external loading relies on). So provider packages are installed on demand into a shared store and imported by absolute path at runtime — never compiled in, no per-provider bloat, and the whole "do the packages --compile cleanly" worry evaporates because they are never compiled at all.

This unifies the two populations: AI SDK packages and provider plugins become the same kind of thing — npm-installable modules in the store, dynamically imported, each yielding a LanguageModelV2.

Decision

Kaged integrates LLM providers as dynamically-loaded modules from an operator-local provider store ($KAGED_HOME/providers), not as code bundled into the daemon binary. A provider is an npm-installable module that yields a LanguageModelV2: for catalog providers, the @ai-sdk/* package the models.dev catalog names; for custom/OAuth providers (Antigravity is the reference), a kaged-convention provider plugin. The daemon installs them on demand — operator-gated, via bun into a single shared node_modules — and imports them at runtime by absolute path (opaque dynamic import, never bundled). @kaged/llm is repurposed into the resolver + loader + middleware: resolve a route → load the module from the store → wrap it in kaged's provider-agnostic middleware (Langfuse spans, x-litellm-model-id capture, spend/usage, retry/fallback). Model metadata comes from a vendored snapshot of a self-hosted models.dev mirror — delivered as kaged-modelsmodels.kaged.dev alongside this ADR. This supersedes ADR-0014's "no @ai-sdk/* tree / hand-written adapters" stance while preserving its durable call: LanguageModelV2 is the integration boundary.

Specifics

1. The provider store — $KAGED_HOME/providers

An operator-local, runtime-managed package store (distinct from the metadata catalog; the models.dev JSON lives separately under $KAGED_HOME/catalog / bundled in the image — see Q3 in § Resolutions to open questions).

$KAGED_HOME/providers/
  package.json        # single root; deps = installed @ai-sdk/* + provider plugins (versions via `latest` at v0; pin map is a future amendment)
  bun.lock            # reproducibility
  node_modules/       # ONE shared, deduped tree (@ai-sdk/provider, zod, … exist once)
  installed.json      # { "@ai-sdk/anthropic": { version, installed_at, source } } — provenance

One root package.json and one node_modules means transitive deps are shared across every provider — the disk-efficiency goal. v0 resolves versions via latest from the catalog's npm field — no pin map. The catalog names the package; bun add <pkg> resolves the version. A kaged-maintained pin map (npm → version) is a future amendment, added only if version churn (the LanguageModelV1V2 break was real) becomes operational pain. Trial by fire: start simple, grow machinery when experience justifies it.

2. Install flow — on demand, operator-gated

  • Enabling a provider whose package is absent resolves the catalog npm and runs bun add <pkg> into the store. This is an operator action, off the request hot path — surfaced in the UI as "install this provider's package?" (the agent-proposes/operator-confirms posture, applied to package install).
  • bun must be present to install. A --compile'd daemon is not the bun package manager. The container image (ADR-0041) includes bun; other distributions document the requirement.
  • Pre-seed common, lazy the tail. The image build pre-installs the default set (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/google, @ai-sdk/openai-compatible) into the store so a fresh daemon works out-of-box and offline for the common case; the 70-provider tail installs on demand.
  • Registry. bun add hits the npm registry at install-time — a third-party fetch, but operator-initiated and off the hot path, not per-request telemetry. Operators may point at a private registry/mirror via .npmrc/bunfig; the lockfile pins resolution. (Vendoring tarballs into kaged-models is possible later; out of scope now.)

3. Load + resolve — @kaged/llm as resolver + loader + middleware

  • resolveModel(route, credentials) → LanguageModelV2:
    1. Look up the provider in the store; read its package.json exports/main to compute the absolute entry path.
    2. await import(absEntryPath) — an opaque specifier, so --compile leaves it unbundled and Bun resolves it at runtime from disk. Cache the module.
    3. Instantiate the model (catalog api baseURL + env credentials for packages; plugin factory for plugins).
  • Middleware (wrapLanguageModel). Every resolved model — package or plugin — is wrapped once with kaged's middleware: Langfuse MODEL_GENERATION spans, x-litellm-model-id capture, spend events / usage hooks (ADR-0026), retry/fallback. This is where ADR-0014's "inline control" now lives — provider-agnostic, applied once.
  • Build config. The daemon is built with dynamic-import passthrough enabled for the store path (Bun's bundler option that lets opaque/computed specifiers reach runtime), so no provider package is ever embedded. (Superseded — see § Amendments 2026-06-29. This option does not resolve transitive bare specifiers in a --compile'd binary; standard drivers are now bundled in statically.)
  • Deletions. The six hand adapters + their SSE/partial-JSON parsers go; the openai-responses reasoning gap resolves by deletion. Mastra integration simplifies — Agent.model receives a real wrapped LanguageModelV2, so the translating shim largely collapses.

4. Two populations, one loader

  • AI SDK packages (stock). @ai-sdk/openai-compatible (+ baseURL) for the long tail; @ai-sdk/anthropic / google / openai / cerebras / … for first-party; per-model npm override honored (/v1/responses@ai-sdk/openai).
  • Provider plugins (kaged-convention). A module in the store that additionally exports: a (route, credentials) → LanguageModelV2 factory, a catalog contribution (in api.json shape, so its models appear in the picker and resolve metadata identically), an optional usage fetcher (ADR-0026 UsageReport), and its own auth flow (ADR-0028). Middleware wraps plugin models too.
  • Antigravity is the reference plugin — the most divergent provider (Cloud-Code proxy, /v1internal:streamGenerateContent, envelope, per-frame usage, OAuth + projectId) and the one absent from models.dev. fetchAntigravityUsage and its catalog rows travel with it.
  • Other OAuth providers. Catalog providers — including those reachable via OAuth such as Copilot (in models.dev, uses github_token) — resolve through whatever npm package models.dev names for them. Per-provider "package vs plugin" deliberation is not done in this ADR: v0 defers entirely to the catalog's npm field for every catalog provider, and Antigravity (absent from models.dev) is the only plugin. There is no compatibility surface to manage at this stage (single-user pre-alpha); future per-provider dispositions land in docs/specs/llm.md amendments when providers are actually added.

5. kaged-models — the self-hosted catalog mirror (metadata, not packages) — DELIVERED

The standalone kaged-models repo (carrying anomalyco/models.dev dev branch as a pinned git submodule) shipped alongside this ADR. It is live at models.kaged.dev with the following endpoints:

Endpoint Shape Purpose
GET https://models.kaged.dev/api.json Provider catalog Provider entries only — canonical name, label, npm package, api baseURL, env credential var names
GET https://models.kaged.dev/models.json Model-only metadata Model entries keyed by "provider/modelId"ModelMeta-equivalent data without the provider index
GET https://models.kaged.dev/catalog.json Combined Provider + model entries in a single document — the shape the daemon bundles into the image
GET https://models.kaged.dev/manifest.json Provenance { source_commit, fetched_at, schema_version } — submodule SHA, generation timestamp, models.dev schema version
GET https://models.kaged.dev/logos/{provider}.svg SVG Per-provider logo (and /logos/labs/{lab}.svg for lab sub-brands)

Pipeline (nightly CI): bump submodule → run models.dev's generate() → emit the four JSON files + /logos/** → render the site with kaged branding and MIT attribution → publish to the models branch on Cloudflare Pages → models.kaged.dev. Submodule SHA is provenance; manifest.json carries it.

Daemon consumption: the daemon image bundles a snapshot of catalog.json (combined) — never the upstream api.json alone, since the daemon needs both provider entries (for resolver inputs) and model entries (for the picker and metadata). The UI build separately syncs /logos/** from models.kaged.dev and serves them as static assets, so the daemon catalog does not need to carry logo URLs. Refresh is operator-chosen (cron / manual / never), never on the hot path. Whole-catalog sync, not progressive load.

The catalog drives: metadata (resolveModelMeta base layer flips LiteLLM → models.dev; ADR-0026 override/spend/usage machinery unchanged), the model list (replacing per-provider listModels, demoted to optional reconciliation), and the store (npm/api/env feed install + resolveModel).

6. Logos vendored and self-served

/logos/{provider}.svg + /logos/labs/{lab}.svg fetched at UI build time, stored as static assets in the UI, and served by the UI/SPA — never hot-linked. Nominative-use note rides with the EULA/legal work.

Relationship to ADR-0014

Supersedes ADR-0014's central decision ("hand-written @kaged/llm adapters; no @ai-sdk/* tree"). Preserves its durable one ("LanguageModelV2 is the boundary Mastra consumes") — now more central, since every provider resolves to one. Reversed because the premises changed: the catalog names a maintained package per provider; "two paths" is void once custom providers are plugins on the same interface; Bun friction is de-risked (OpenCode) and sidestepped (packages run from disk, never --compile'd in). ADR-0014's transparency/control goals are honored via inspectable OSS packages + a kaged-owned middleware stack rather than kaged-owned wire bytes.

Consequences

What this commits us to

  • The provider store ($KAGED_HOME/providers): shared node_modules, installed.json, an installProvider flow driving bun add, and bun in the image. No pin map at v0 (versions resolve via latest); a pin map is a future amendment if version churn demands it.
  • @kaged/llm repurposed: resolveModel (path-resolve + opaque dynamic import + cache), the wrapLanguageModel middleware stack, deletion of six adapters + parsers; tests replaced by loader + middleware tests.
  • A daemon build configured for dynamic-import passthrough so no provider package is embedded; image pre-seeds the default provider set.
  • The provider-plugin contract (in-store module: factory + catalog contribution + usage + own auth) and Antigravity as the first plugin, with a follow-up plugin-host/loader spec.
  • kaged-models ✅ (submodule, nightly build, Pages models branch, MIT attribution, manifest.json) — delivered alongside this ADR, live at models.kaged.dev (see §5 for endpoint map). The catalog-JSON/logo bundling into the daemon image is part of the implementation phase.
  • Rewrites: llm.md (store + loader + middleware; package vs plugin; listModels demotion); ADR-0026 references (models.dev base); ADR-0014 marked superseded.

What this forecloses

  • Hand-maintained wire adapters and per-provider model-list fetchers as the primary path.
  • Provider packages bundled into the binary (and the all-or-nothing bloat that implies).
  • Runtime dependence on third-party models.dev (mirrored) and on hand-tracking provider API drift (packages absorb it).
  • Antigravity (and, in time, other custom-auth providers) living in core.

What becomes easier

  • Adding a catalog provider is data → an on-demand install; no wire code, no binary rebuild, no embedded bloat.
  • Disk: one shared node_modules; only installed providers occupy space.
  • Provider drift, reasoning capture, new modalities — handled upstream by package maintainers.
  • Telemetry/spend/fallback is one middleware stack over every model.

What becomes harder

  • A runtime dependency on the AI SDK and its release cadence/breaking changes — bounded by the middleware seam (and, when adopted, the pin map). At v0 the bound is the middleware seam alone; the pin map is a future amendment.
  • Install-time dependency on bun + a registry; offline freshness limited to the pre-seeded set until the operator installs more.
  • Byte-level operator transparency softens (operator reads @ai-sdk/anthropic, OSS/inspectable, rather than kaged's fetch) — a documented, accepted trade.
  • Supply-chain surface: installing npm modules, especially third-party provider plugins, is an operator trust decision (cf. n8n community nodes).
  • A new in-store dynamic-loader + isolation story to spec.

Alternatives considered

Alternative A — Keep hand-written @kaged/llm adapters; models.dev as catalog data only

Why tempting: Maximal transparency; no AI SDK runtime dependency; ADR-0014 stands. Why rejected: Keeps the wire-maintenance the project wants gone and offloads none of it; the "two paths" objection is void once custom providers are plugins; the Bun objection is de-risked and sidestepped. Battle-tested packages are less long-term development than maintaining our own adapters for stable, known providers.

Alternative B — Adopt packages but bundle them into the binary

Why tempting: No store, no install flow, fully self-contained executable, works offline for everything. Why rejected: Every provider's package + transitive deps compiled into one binary = large fixed bloat regardless of which providers an operator uses, and it forces the "do all 75 providers' packages --compile cleanly" problem. The store + dynamic load gives offline-for-the-common-set (pre-seed) without paying for the tail.

Alternative C — Packages for everything; no provider-plugin slot

Why rejected: Antigravity's custom wire/auth fits no stock package; forcing it creates a fragile fork in core — the special-case we're removing. opencode-antigravity-auth exists because a plugin is the right home.

Alternative D — Runtime-fetch the catalog instead of bundling

Why rejected: A runtime call (even to our own host) is telemetry surface + offline failure + nondeterminism. Bundled snapshot + operator-chosen refresh gives freshness on the operator's terms.

Alternative E — Progressive catalog load (provider → models on demand)

Why rejected: api.json is one modest static file; partial loading adds a state machine and N round-trips for nothing. Sync the whole file. (Operator's own conclusion; affirmed.)

Alternative F — Provider plugins over the JSON-RPC subprocess transport (project-plugin style)

Why rejected: Per-token streaming over subprocess RPC is the wrong transport (serialization + IPC latency per chunk). Provider plugins load in-process from the store.

Resolutions to open questions

All eight open questions resolved at acceptance (2026-06-25). The operative decisions are recorded here so the body of the ADR can be read as the contract; the questions are preserved for institutional memory.

1. The spike — validated (later found incorrect — see § Amendments 2026-06-29)

The core technical bet — opaque dynamic-import of an installed @ai-sdk/* package from the --compile'd daemon under the store layout — has been tested and confirmed. Module resolution by absolute path, transitive-dep resolution against the shared node_modules, and a real stream all work. The absolute-path-over-bare-specifier sharp edge (a compiled binary has no ambient node_modules) is absorbed into §3 of the Decision. No further spike work blocks acceptance or implementation.

Correction (2026-06-29): this bet was wrong for the shipped --compile'd binary. Transitive-dep resolution against the on-disk node_modules does not work from a compiled binary (it works only under plain bun run, which is presumably what the spike exercised). Standard drivers are now bundled into the binary instead. See § Amendments 2026-06-29.

2. bun invocation for installs — bun required, vendor-bun deferred

bun is a hard runtime requirement for installs. The container image (ADR-0041) already ships it; host (non-container) operators must have bun on PATH. A vendored-bun fallback (download official binary to $KAGED_HOME/vendor/bun on first-run) is deferred — only built when an actual host-install requirement surfaces. Operators requiring host install without a system bun can install bun themselves; that is the documented posture until evidence forces the vendor path.

3. Naming — $KAGED_HOME/providers + $KAGED_HOME/catalog

Adopted as proposed. Packages live in providers/; models.dev metadata lives in catalog/. The two have different lifecycles (operator-managed npm tree vs. vendored snapshot) and must not be blurred into a single directory.

4. Pin map ownership + bump cadence — no pin map at v0

v0 resolves package versions via latest from the catalog's npm field; no kaged-maintained pin map ships. bun.lock in the store still pins resolved versions for reproducibility across installs on the same operator's machine — that is the floor-level guarantee. A curated kaged-owned pin map (the Mastra exact-pin pattern) is a future amendment, added only if version churn becomes operational pain. Trial by fire: ship simple, grow machinery when experience justifies it. Major AI SDK contract bumps (V2→V3-style) would still trigger an ADR amendment to the middleware seam, regardless of pin map.

5. Which OAuth providers become plugins vs ride a package — defer to the catalog

Not decided per-provider in this ADR. The binding principle is: every provider resolves to exactly one LanguageModelV2. For v0:

  • All catalog providers (every entry in models.dev, including Copilot which uses github_token) resolve via whatever npm package the catalog names. No per-provider deliberation; the catalog is the source of truth.
  • Antigravity (absent from models.dev) is the only plugin.

Per-provider "package vs plugin" classification for non-catalog or custom providers lands in docs/specs/llm.md amendments when those providers are actually added — not in this ADR. Single-user pre-alpha means there is no compatibility surface to manage yet.

6. Per-model npm override storage — extend ADR-0026

Stored in the existing ADR-0026 model_overrides table as a new field packageOverride: string (JSON-encoded). Sparse key-value, CRUD from UI, exactly the ADR-0026 pattern. If set for a (provider, modelId), the loader uses the named package instead of the catalog's npm field for that model. No parallel storage mechanism.

7. Removal surfacing on refresh — catalog referenced live; local config stores the minimum; operator-confirmed sync

Refined from the as-accepted text (which over-stated the at-add snapshot). The data-flow model is:

  • Catalog snapshot is referenced live at call time, not snapshotted into local config comprehensively. It's the base layer for lookupModelMeta / resolveModelMeta, with operator DB overrides on top (ADR-0026 unchanged).
  • Local config stores the minimum required for a provider/model to keep working regardless of catalog state: provider row has driver (= npm package name) + credentials + optional overrides; model row has id + display name. The rest (capabilities, pricing, context limits, deprecation) is read live from the catalog when present, falls back to overrides-only when absent.
  • Operator-initiated sync (the only path by which the snapshot changes) pulls a newer snapshot from models.kaged.dev, computes a diff, and surfaces it in the UI for operator confirmation. For each removed entry that matches configured local rows, the UI shows a "keep" checkbox (default checked). Operator can uncheck to delete the local row as part of the apply step.
  • Catalog sync never silently modifies operator config or removes routes. The only writes to local config during sync are the explicit operator-confirmed deletions.

Resilience property: as long as local config has the npm package name and the package is installed in $KAGED_HOME/providers, the provider/model works — even if the catalog snapshot drops the entry entirely. The provider becomes functionally a custom provider; resolveModelMeta falls back to overrides-only; the wire still works because the package loads by name. Cosmetic "no longer in catalog" badge in the UI; no functional impact unless the operator chooses to remove via sync confirmation.

Spec contract: llm.md § Catalog refresh and removal surfacing; operator-facing workflow: local-config.md § Catalog sync workflow.

8. Supply-chain posture for third-party provider plugins — pure operator trust

No allowlist, no integrity pinning, no kaged-curated gate. The operator decides what to install; if they do not want the daemon's catalog, they can add custom providers and the daemon will install and load whatever they ask for. If they never want to update from the package that came with the daemon, that is also fine.

This is consistent with the operator-owned, self-hosted posture: kaged is not the trust authority for the operator's machine. Supply-chain decisions are the operator's to make. A future amendment can add optional integrity pinning (Subresource Integrity hashes in local-config) if the operator community asks for it; v0 ships without.

References


Amendments

2026-06-29 — Antigravity implemented as a native in-daemon module (not a separate plugin)

The Antigravity integration (the reference provider for divergent/custom-auth providers cited throughout this ADR) is implemented as a native module in @kaged/llm/src/antigravity/ — not as a separate plugin repo, not as a dynamically-loaded module, and not as a vendored copy of opencode-antigravity-auth. The wire logic (OAuth, envelope, tool-schema sanitization, thinking config, signature caching, quota protection) is a kaged-native reimplementation informed by the MIT-licensed opencode-antigravity-auth by Jens. It is compiled into the daemon binary and registered in the bundled-driver registry under "antigravity". The createModel(route) factory wraps the bundled @ai-sdk/google driver with a custom fetch that intercepts Google API requests and rewrites them to the Cloud Code proxy. Multi-account rotation and Gemini-CLI quota fallback are dropped (single-account with quota protection is the v0 scope). See docs/specs/llm.md § Antigravity reference plugin (rewritten) for the operative contract.

2026-06-29 — Standard @ai-sdk/* drivers are bundled into the daemon binary; the on-disk provider store is retired (partial reversal of Alternative B)

The technical bet in §3 and Q1 was wrong, and was caught in production. §3 ("Load + resolve") and § Resolutions to open questions Q1 ("The spike — validated") asserted that a --compile'd daemon could await import(absEntryPath) a provider package from $KAGED_HOME/providers/node_modules and have Bun resolve that package's own bare-specifier transitive dependencies (e.g. @ai-sdk/provider-utils) against the on-disk shared node_modules. It cannot. In the shipped containerised daemon (Bun 1.3.14), loading @ai-sdk/openai-compatible by absolute path fails at runtime with Cannot find module '@ai-sdk/provider-utils' from '/data/providers/node_modules/@ai-sdk/openai-compatible/dist/index.js', even though the package and its dependency are both physically present and correct on disk.

This was verified empirically in the running container: a plain bun process running the exact same absolute-path import() resolves the transitive dependency correctly, while the --compile'd binary does not. The root cause is a Bun --compile limitation: a standalone executable does not walk the real-filesystem node_modules to resolve the bare-specifier transitive imports of a module it loaded from an absolute on-disk path (see oven-sh/bun#27058, #14320, #26653). The "dynamic-import passthrough" build option referenced in §3 and § Consequences does not exist as a configuration that fixes this; the Q1 spike evidently exercised bun run, not the compiled artifact, so it did not surface the gap.

Decision (amended). Standard catalog providers are served by @ai-sdk/* packages that are now statically imported into the daemon and bundled by bun build --compile — i.e. Alternative B ("Adopt packages but bundle them into the binary"), previously rejected, is adopted for the standard driver set. Bundling was verified to work: a compiled binary statically importing all the catalog-mapped drivers resolves every transitive dependency at runtime from a node_modules-free working directory. The driver set is the full set named in @kaged/llm's factory map (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/google, @ai-sdk/openai-compatible, @ai-sdk/google-vertex, @ai-sdk/groq, @ai-sdk/xai, @ai-sdk/mistral, @ai-sdk/cerebras, @ai-sdk/cohere, @ai-sdk/togetherai, @ai-sdk/deepinfra, @ai-sdk/perplexity, @openrouter/ai-sdk-provider). Measured cost of bundling all of them: ~1.6 MB on a ~91 MB binary.

What this changes vs. the as-accepted ADR:

  • The operator-local provider store ($KAGED_HOME/providers: package.json, bun.lock, shared node_modules, installed.json), the on-demand bun add install flow (installProvider), the absolute-entry-path resolution, the opaque dynamic import, the image pre-seed, and the bun-required-for-installs constraint (Q2) are all retired. The Docker image no longer copies a /data/providers store; bun is no longer required at runtime for provider installs.
  • resolveModel resolves the package name as before (catalog / per-model npm / packageOverride / provider-level npmPackage) but then looks the package up in a static bundled-driver registry rather than on disk. Middleware wrapping (§3) is unchanged.
  • Custom and baseURL-only providers are unaffected as long as they ride a bundled driver. The reference custom case — a provider whose npmPackage is @ai-sdk/openai-compatible plus a custom baseUrl (e.g. a self-hosted or OpenAI-compatible gateway) — resolves to the bundled @ai-sdk/openai-compatible and works.
  • A provider whose resolved package is not in the bundled set is an error directing the operator to a provider plugin. The "70-provider tail installs on demand" model in §2 is therefore retired in favour of "bundle the standard set; everything else is a plugin." Adding a new stock driver is now a daemon dependency addition + rebuild, not a runtime install.

What is preserved: the catalog (models.kaged.dev) as the source of truth for which providers exist and which package/baseURL/credentials each uses; the LanguageModelV2 integration boundary; the provider-agnostic middleware stack; and the provider-plugin slot for divergent providers (Antigravity remains the reference). ADR-0014's durable call is still honoured.

The docs/specs/llm.md § Resolution algorithm, § Provider store, and § Install flow sections are amended to match; STATUS.md and the cross-referencing specs (agent.md, daemon.md, http-api.md, local-config.md, plugin-host.md, ADR-0013) are updated accordingly. §3, §2, and Q1 above are left in place for institutional memory; this amendment is the operative decision where they conflict.

2026-06-25 — Q7 refined: catalog referenced live, local config stores the minimum, operator-confirmed sync

The as-accepted text for Q7 stated local config "snapshotted into local config at add-time" the catalog metadata that drove an add. Refined: local config stores only the minimum (provider driver/npm package name, model id + display name); the catalog snapshot is referenced live at call time as the base layer for resolveModelMeta, with operator DB overrides on top. When the catalog lacks an entry, only overrides apply. Catalog sync (operator-initiated, never implied) shows added/removed/changed entries with a "keep" checkbox for configured rows that the new snapshot drops; default is keep; the only local-config writes during sync are the explicit operator-confirmed deletions. Resilience property: as long as local config has the npm package name and the package is installed, the provider works regardless of catalog state. See § Resolutions to open questions §7 (rewritten in place) and llm.md § Catalog refresh and removal surfacing.

2026-06-25 — kaged-models §5 deliverable shipped

The kaged-models standalone repo and its publish pipeline shipped alongside this ADR's acceptance — §5 was upgraded from planned to delivered. Concrete endpoints at models.kaged.dev:

  • /api.json — provider catalog (provider entries only)
  • /models.json — model-only metadata (model entries only)
  • /catalog.json — combined (providers + models; the shape the daemon bundles into the image)
  • /manifest.json — provenance ({ source_commit, fetched_at, schema_version })
  • /logos/{provider}.svg (+ /logos/labs/{lab}.svg) — provider/lab logos

Section 5 text rewritten to past tense with the endpoint table; Decision paragraph and § Consequences updated accordingly. The daemon-side consumption (bundling catalog.json + /logos/** into the image at build time) remains part of the implementation phase — the source-of-truth mirror is live; the image-bundling step is downstream of it.

2026-06-27 — Logos moved from daemon image to UI build

Logos are now cloned from the public kaged-dev/kaged-models repository on the models branch at UI build time and stored as static assets under packages/ui/public/logos/ (and logos/labs/). The UI derives the logo path from the provider name (/logos/{provider}.svg) and falls back to the generic provider icon when a logo is absent. The daemon image continues to bundle the catalog.json snapshot, but no longer bundles /logos/**. Runtime catalog updates still use fetch against models.kaged.dev; only the build-time logo source is a git clone. This separates the slowly-changing logo assets from the daemon release cycle and lets the UI refresh logos without a new daemon build. The body text in §5 and §6 has been updated to match.

2026-06-25 — Accepted with all open questions resolved

Promoted from Proposed to Accepted. All eight open questions in the original proposal are resolved; see § Resolutions to open questions for the operative decisions on each. Notable simplifications from the as-proposed text, driven by the single-user pre-alpha posture:

  • No pin map at v0 (Q4). Package versions resolve via latest from the catalog's npm field. The body sections that referenced a kaged-maintained pin map (§1 store layout, §2 install flow, § Consequences) have been updated to remove the pin map commitment; the pin map is now a future amendment.
  • No per-provider OAuth classification in this ADR (Q5). §4 "Other OAuth providers, staged" has been simplified to "defer to models.dev's npm field for all catalog providers; Antigravity is the only plugin." Per-provider dispositions land in docs/specs/llm.md amendments when providers are added.
  • Simplified removal surfacing (Q7). Local config is source of truth once a provider is added (catalog metadata snapshotted at add-time). No diff/warning machinery.
  • Pure operator trust for supply chain (Q8). No allowlist, no integrity pinning, no kaged-curated gate.

The technical bet that was Open Question #1 (opaque dynamic-import of @ai-sdk/* packages from the --compile'd daemon) was validated before acceptance, so the spike is no longer pending. Implementation proceeds via the doc-first → TDD pipeline: docs/specs/llm.md amendment first, then tests, then code.