# 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.