dvergr talks to LLMs through pluggable providers, each resolved from a model registry. To run anything you need (1) an API credential or supported subscription login and (2) a model in the registry pointing at it. Provider credentials are injected by the trusted in-process gateway only after the configured upstream origin has been validated. Credential-free local providers use the same boundary with an explicitly unauthenticated, origin-confined capability.
| Provider key | API type | Default endpoint | Source |
|---|---|---|---|
:anthropic | :anthropic-messages | https://api.anthropic.com/v1 | model/api/anthropic.clj |
:openai | :openai-chat | https://api.openai.com/v1 | model/api/openai.clj |
:fireworks | :openai-chat | https://api.fireworks.ai/inference/v1 | model/api/openai.clj (OpenAI-compatible) |
:claude-code | :claude-code-cli | local claude -p CLI (subscription) | model/api/claude_code.clj |
:codex-subscription | :openai-responses | native ChatGPT Codex Responses transport | model/api/codex_subscription.clj |
:codex-subscription-cli | :codex-cli | isolated codex exec compatibility path | model/api/codex_subscription.clj |
Providers self-register at startup (providers/init-defaults!) only when
their key is available — so a missing key just means that provider is absent,
not an error. If nothing registers, you get a loud boot warning.
Start dvergr with the environment and CLI login you intend to use already in place, then inspect the registry before creating an agent:
(require '[dvergr.model.chat :as chat]
'[dvergr.model.providers :as providers]
'[dvergr.model.registry :as registry])
(providers/ensure-initialized!)
(vec (providers/list-providers))
(providers/default-spec) ; selection for an unpinned agent
(mapv :id (registry/models-for-provider :codex-subscription))
;; Exercise only the model transport, without constructing a room:
(chat/quick-chat "Reply with DVERGR_OK" "codex")
Provider discovery is cached for the life of the process. Restart the REPL after
changing an environment variable or logging a CLI in. An application may call
providers/clear-all! followed by providers/ensure-initialized! during a
controlled reconfiguration, but should not do that while turns are active.
OpenAI documents two distinct local Codex sign-in modes: ChatGPT subscription
access and usage-based API-key access. Dvergr's :codex-subscription providers
require the former.
codex login.codex login status reports a ChatGPT login.providers/ensure-initialized!, and inspect
providers/list-providers as shown above."codex", or pin an agent explicitly:{:agents
{:var {:provider :codex-subscription
:model "codex-subscription"}}}
The explicit native model choices are "codex-subscription-sol",
"codex-subscription-terra", and "codex-subscription-luna" (GPT-5.6), and
"codex-subscription-sol-6.1", "codex-subscription-luna-6" and
"codex-subscription-astra-6" (GPT-6/6.1); their short aliases are
"codex-sol", "codex-terra", "codex-luna", "codex-sol-6.1",
"codex-luna-6" and "codex-astra-6".
The native transport reads $CODEX_HOME/auth.json, falling back to
~/.codex/auth.json. If you have configured
cli_auth_credentials_store = "keyring" or "auto" and no file is present,
dvergr cannot read the keyring directly. You have two options:
:codex-subscription-cli with model "codex-subscription-cli". This
starts a locked-down, ephemeral codex exec for each turn.cli_auth_credentials_store = "file" in ~/.codex/config.toml, then run
codex login again to create the file needed by the native transport.Both providers leave agent state, context, tools, effects, and sandbox semantics in dvergr. The CLI path is compatibility isolation, not delegation of the agent runtime to Codex. Dvergr does not switch paths automatically: replaying a request after streaming or a tool call has begun could duplicate effects.
The native Codex subscription and Claude Code transports accept user message
content as an ordered vector of {:type "text" :text "…"} and
{:type "image_url" :image_url {:url "data:image/png;base64,…"}} parts.
Codex sends native Responses image parts; Claude uses one structured stdin
message with its existing native-tool restrictions unchanged. Inline PNG,
JPEG, WebP and GIF data is accepted, with at most sixteen images and 20 MiB of
encoded image URLs per message. This path never fetches URLs or reads local
image paths. The Codex CLI compatibility transport remains text-only. Image
input does not grant computer-control tools or permissions.
Local Whisper uses DVERGR_WHISPER_PYTHON when configured, otherwise python3
on the server's PATH. Install faster-whisper in that interpreter's environment,
not merely in an interactive shell environment. Trusted host callers can bind
dvergr.audio.stt/*python-executable* for an individual call. The existing
DVERGR_WHISPER_SCRIPT and DVERGR_WHISPER_MODEL settings select the script
and model; no interpreter is installed automatically.
Never copy auth.json into a project or container image. Dvergr injects its
tokens only after validating the exact https://chatgpt.com origin, refreshes
expiring tokens in the host process, and persists rotations back to the auth
file with owner-only permissions where the filesystem supports them.
The native route targets the private endpoint used by the open-source Codex client, not the stable public OpenAI API. Treat it as experimental compatibility machinery and keep the explicit CLI path available across upgrades.
Official setup references: Codex CLI,
authentication, and the
cli_auth_credentials_store configuration.
Keys are read from environment variables (or passed in a config map to the
register-*! fns):
export ANTHROPIC_API_KEY=sk-ant-… # enables :anthropic
export OPENAI_API_KEY=sk-… # enables :openai
export FIREWORKS_API_KEY=fw-… # enables :fireworks
export OPENAI_BASE_URL=https://api.openai.com/v1 # optional
export FIREWORKS_BASE_URL=https://api.fireworks.ai/inference/v1 # optional
OPENAI_API_KEY /
OPENAI_BASE_URL; Fireworks reads FIREWORKS_API_KEY / FIREWORKS_BASE_URL,
and falls back to OPENAI_API_KEY when OPENAI_BASE_URL points at
fireworks.ai (fireworks-key in model/api/openai.clj), the
OpenAI-compatible setup many tools share. In that setup :openai registers
too, with the Fireworks base URL. The Fireworks models are in
resources/models.edn.providers/default-spec picks the
first registered provider in the order Anthropic, Fireworks, OpenAI, Codex
subscription, Codex CLI, Claude Code, with that provider's preferred model:
for Fireworks that is accounts/fireworks/models/minimax-m3
(preferred-default-model in model/providers.clj). The registry's
:defaults in resources/models.edn name minimax-m3 for compaction, and
the alias minimax also resolves to m3.claude --version.
If the CLI exits 0, :claude-code registers and bills against your Claude
subscription (model pricing is 0). Reports still price subscription models
at list price: each one's registry entry names the API model its tokens are
worth (:list-price-of, e.g. claude-code-sonnet → claude-sonnet-5-5), so a
subscription run compares with a paid one.codex login and choose
Sign in with ChatGPT. For the default file-backed login, dvergr refreshes
OAuth credentials inside its trusted host boundary and sends dvergr-owned
history and native tool definitions directly to the ChatGPT Codex Responses
endpoint. The token is not placed in provider request maps or exposed to SCI.
A keyring-only login registers :codex-subscription-cli as a compatibility
path. Subscription model pricing is recorded as 0, while normal plan limits
still apply.The subscription endpoint is an upstream Codex compatibility surface rather
than the public OpenAI API. Keep :codex-subscription-cli available when
upgrading Codex/dvergr so an upstream wire or authentication change has an
immediate fallback.
Pin an agent to the native subscription model with:
:agents {:var {:provider :codex-subscription
:model "codex-subscription"}}
The optional aliases codex-sol, codex-terra, codex-luna,
codex-sol-6.1, codex-luna-6 and codex-astra-6 select an explicit
subscription model. To force the compatibility path, use provider and
model :codex-subscription-cli / "codex-subscription-cli".
| You have | Setup | Provider / bundled model |
|---|---|---|
| Anthropic API account | ANTHROPIC_API_KEY | :anthropic / "claude-sonnet-4-6" |
| Claude Code subscription | Logged-in claude on PATH | :claude-code / "claude-code-sonnet" |
| ChatGPT Codex subscription | codex login with ChatGPT | :codex-subscription / "codex-subscription" |
| Keyring-only Codex login | Logged-in codex on PATH | :codex-subscription-cli / "codex-subscription-cli" |
| Fireworks account | FIREWORKS_API_KEY | :fireworks / "accounts/fireworks/models/minimax-m3" |
| OpenAI API account | OPENAI_API_KEY, plus an OpenAI registry entry | :openai / the registered model id |
The built-in registry carries Anthropic, Claude Code, Codex subscription, five
native OpenAI Chat models, and the curated Fireworks registry in
resources/models.edn. OPENAI_API_KEY can therefore run one of the built-in
OpenAI models immediately. (registry/refresh-from-models-dev! #{:openai}) is
an opt-in runtime overlay: it can add current models and replace its mapped
limits/rates in this process, but does not rewrite a resource or source file.
Declare a model in resources/models.edn when it is a dvergr-maintained
configuration rather than a live overlay.
For a custom or local OpenAI-compatible endpoint, register both its credentials and at least one model. A credential-free local endpoint still receives an origin-confined capability, so it cannot accidentally acquire another provider's authorization header:
(require '[dvergr.model.gateway :as gateway])
(providers/register-openai-compatible!
:ollama
{:base-url "http://localhost:11434/v1"
:credentials (gateway/unauthenticated-credentials
#{"http://localhost:11434"})})
(registry/register-model!
{:id "qwen3:8b"
:name "Qwen 3 8B (local)"
:provider :ollama
:api-type :openai-chat
:capabilities #{:tools :streaming :system-prompt}
:context 32768
:max-output 8192
:pricing {:input 0 :output 0}
:quirks {}})
(chat/quick-chat "Reply with LOCAL_OK" "qwen3:8b")
Provider keys are not in
config.local.edn— they come from the env. Other secrets (Telegram, GitHub, …) live in config; seedoc/configuration.md.
resources/models.ednEdit this file to add/change models without touching source. It's loaded at
startup and is also re-loadable via (registry/load-models-resource!). Shape:
{:models
{"accounts/fireworks/models/minimax-m3" ; map key = the model :id
{:name "MiniMax M3"
:provider :fireworks ; must match a provider key
:api-type :openai-chat
:capabilities #{:tools :streaming} ; :tools :vision :thinking :streaming
; :system-prompt :cache-control :json-mode
:context 512000 ; context window (tokens)
:max-output 16384
:pricing {:input 0.30 :cache-read 0.06 :output 1.20} ; $/MTok
:quirks {}}} ; e.g. :default-top-p, :tool-id-in-every-chunk?
:aliases {"minimax" "accounts/fireworks/models/minimax-m3"} ; short names
:defaults {:primary-model "accounts/fireworks/models/minimax-m3"
:compaction-model "accounts/fireworks/models/minimax-m3"
:summary-model "accounts/fireworks/models/gpt-oss-20b"}}
To add a model: add an entry under :models keyed by its provider-side id,
set :provider to a registered provider, list :capabilities, and (optionally)
an alias. EDN entries merge over the built-in default-models (current Claude
Opus/Sonnet/Haiku plus Claude-Code and Codex-subscription models, defined in
model/registry.clj).
(registry/refresh-from-models-dev! #{:anthropic …}) is an opt-in, network call
that overlays live pricing/context from https://models.dev into process state
only. It is not a build or release generator and does not persist a downloaded
snapshot. Built-in OpenAI rate fields are a separately documented dated
snapshot in model/registry.clj; resources/models.edn is a distinct curated
registry.
Vision models carry :vision in :capabilities. Fireworks Kimi K3 and
GLM 5.3 Flash accept image input. dvergr.media.vision (image describe/extract)
selects per-call :model, then DVERGR_VISION_MODEL, then
:defaults {:vision-model "…"} in models.edn, in that order. The bundled default
is accounts/fireworks/models/kimi-k3. It works with FIREWORKS_API_KEY, or
OPENAI_API_KEY plus OPENAI_BASE_URL=https://api.fireworks.ai/inference/v1.
Explicit configuration overrides the default. For example, gpt-5.4-mini needs
an OpenAI provider with OPENAI_API_KEY and an OpenAI endpoint. Missing providers
and known provider/endpoint mismatches fail before HTTP with an actionable error
naming :vision-model and DVERGR_VISION_MODEL. Unknown custom proxy URLs must
serve the selected model; they cannot be validated locally. LLM_CALL_MODEL
defaults to Fireworks accounts/fireworks/models/minimax-m3.
Retired ids remain in the registry with :retired true and :successor.
Alias resolution follows successors and emits one warning naming the retired
id and the resolved id per resolution. Kimi K2.6 and K2.5 resolve to image-capable
Kimi K3, GLM 5.2 to GLM 5.3, and MiniMax M2.7 to MiniMax M3. A provider's 404
"Model not found" error names the resolved model and its source config key
(:model for direct calls). vision/describe throws on failed or empty
descriptions so screen intake can report a failure instead of silently dropping
a frame. Provider errors carry :status (HTTP status), :message (the provider's
message), and :model in ex-data, including after retry exhaustion.
Some Fireworks models leak their tool-call envelope into content (Kimi, GLM);
their :quirks (:kimi-tool-id-format?, :glm-tool-format?) turn on the
recovery/scrub in the OpenAI adapter, so you rarely touch them by hand.
Per request (model/chat.clj), resolution is:
:model opt is required (an id or an alias). Aliases resolve first via
registry/resolve-alias.registry/get-model! — throws if absent.(:provider opts) if given, else (:provider model-def) from the
registry entry, then providers/get-provider! (throws if that provider isn't
registered, i.e. no key).:tools model throws.
:thinking and quirk defaults (like Kimi's top_p) are applied automatically.Agents carry :provider / :model in their config (config.local.edn :agents,
materialised into Datahike actor rows). Of the registry's :defaults,
:compaction-model (an agent's compaction) and :summary-model (room
summaries in orchestration/stats.clj) are read via registry/get-default;
nothing reads :primary-model: an unpinned agent's model comes from
providers/default-spec (above).
:agents {:var {:provider :fireworks
:model "accounts/fireworks/models/minimax-m3"
:tags #{:secretary}}}
Can you improve this documentation? These fine people already did:
Christian Weilbach & Daniel JompheEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |