# AI Provider Plugins
Deft's AI layer follows an everything-is-a-plugin model: the provider
harness itself (`deft/stdext/ai`) knows nothing about any specific LLM
vendor. Every provider — DeepSeek, Anthropic, OpenAI, Ollama, Z.AI, or a
third-party one — is a separate package that the harness discovers and
loads at runtime.
## How discovery works
1. A package declares the tag in its manifest:
%Package{
owner "deft" name "ai-deepseek" version "0.1.0"
tags @{ "ai" "ai-provider" }
main "src/ai-deepseek.dft" }
2. On first AI call, `deft/stdext/ai/plugins` scans every configured
package root (`pkg/list`), evals each `package.dft`, and picks the
packages tagged `"ai-provider"`.
3. Each hit is mounted (`ai-<name>`) and runtime-imported. Its entry
module declares `implements AiProviderExtension`; the VM validates the
exports against the contract before the provider joins the registry. A
missing contract field fails the load loudly.
4. `ai/refresh` rescans so a provider installed mid-session is picked up
(already-loaded modules are reused).
Dev checkouts shadow installed copies with the same package name, and
duplicate provider keys resolve first-package-wins. The generic host is
`deft/stdext/plugin` — future plugin types (tools, skills, loops, …)
reuse it with their own tag and contract type.
## The contract: `AiProviderExtension`
Declared in `deft/stdext/types`, the module exports:
| Field | Type | Purpose |
|---|---|---|
| `key` | keyword | Registry key (`:deepseek`) |
| `label` | string | Menu display name |
| `version` | string | Plugin version |
| `base-url` | string | Default API base URL |
| `env-var` | string? | Env var holding the API key (`nil` = none) |
| `default-model` | string | Model used when cfg has none |
| `extensions` | map? | Default cfg fields merged by `build-cfg` (e.g. thinking mode) |
| `Provider` | any | The `AiProvider` protocol instance |
## The protocol: `AiProvider`
Defined in `deft/stdext/ai`, the methods split into three groups:
- Request shaping — `models`, `url`, `headers`, `body`,
`messages-with-user`, `tool-format`
- Response parsing — `extract-content`, `extract-reasoning`,
`extract-usage`, `has-tool-use`, `extract-tool-calls`,
`build-tool-results`, `finish-reason`, `scavenge-tool-calls`
- Policy/capabilities — `context-window`, `cost`, `compact-message`,
and the optional `balance` / `refresh-models` capabilities (stubs return
`nil`; consumers `await` them, which passes plain values through)
`deft/stdext/ai` drives the tool-use loop, `ai/budget` derives token
limits from `context-window`, and `ai/config` builds cfg maps from the
descriptor. Consumers — the chat app, the editor's AI extension, the help
browser — import only `deft/stdext/ai/registry` and pass a cfg.
## Ergonomic API
import "deft/stdext/ai/registry" as ai [ai/list-providers] # @{ :deepseek :anthropic ... } [ai/provider-label :deepseek] # "DeepSeek" [ai/provider-models :ollama] # @{ "llama3.2" ... } [ai/tool-format :anthropic] # :anthropic set cfg [ai/default-cfg] # :deepseek if installed, else first set cfg [ai/build-cfg :openai "gpt-4o-mini" :temperature 0.2] set reply [await [ai/ask $cfg "Explain this function."]]
## Writing a provider
Copy `packages/ai-deepseek` (or `ai-ollama` for a local server), rename
the package directory, then:
1. Update `package.dft`: `name`, `main`, `exports`, keep the
`"ai-provider"` tag.
2. In the entry module, `implements AiProviderExtension`, export the
contract fields, and `defimpl YourType ai/AiProvider { ... }`.
3. Add `context-window` rows for your models, a `tool-format`, and
`cost`/`balance`/`refresh-models` when applicable.
4. Drop the package under a prelude `:pkgs` root (or install it) — the
harness finds it on the next scan.
No edit to `deft/stdext/ai` is ever required to add a provider.