Resolves provider structs from role names declared in application config.
A role names a purpose (:chat, :vision, :embed, :ocr, ...) and maps it
to a provider module plus the options its new/1 takes. Application code then
names the purpose instead of the vendor, so swapping a hosted model for a
self-hosted container is a config change rather than an edit at every call site.
# config/runtime.exs
config :ex_agent, :roles,
chat: {ExAgent.Providers.Gemini, api_key: System.fetch_env!("GEMINI_API_KEY")},
vision: {ExAgent.Providers.OpenAICompatible,
base_url: System.fetch_env!("MODAL_QWEN_URL"),
model: "Qwen/Qwen3-VL-8B-Instruct",
modalities: [:text, :image]}
ExAgent.provider!(:vision) #=> %ExAgent.Providers.OpenAICompatible{}Role names are arbitrary atoms - the library has no fixed set. A bare module
means {module, []}.
Option values resolved at boot
An option value that is a zero-arity function or an {module, function, args}
tuple is invoked once during build!/0, for credentials that come from a vault
rather than an env var:
chat: {ExAgent.Providers.Gemini, api_key: {MyApp.Vault, :fetch, ["gemini"]}}Because of this, a literal three-element tuple cannot be used as an option value. No provider option currently takes one.
Caching
Roles are resolved once, during application start, and stored in
:persistent_term. Reads are free - no copy, no table lookup - which matters
because ExAgent.provider!/1 sits on every request. Writes are not: each one
triggers a global GC scan across every process in the VM, which is why
build!/0 is the only thing that writes and why per-call overrides
(ExAgent.provider!/2) build a fresh struct instead of caching it.
Call build!/0 yourself if you assemble role config after boot.
Security
A cached provider struct holds a Req client with its API key baked into the
request headers, so credentials live in :persistent_term and will appear in a
VM crash dump. This is the same exposure as holding them in supervisor state,
but it is worth knowing.
Summary
Functions
Resolves every configured role into a provider struct and caches it.
Builds a fresh provider struct for role with overrides merged over its
configured options.
Returns the cached provider struct for role, or :error if it is not configured.
Returns the cached provider struct for role, raising if it is not configured.
Returns the configured role names, in declaration order.
Functions
@spec build!() :: :ok
Resolves every configured role into a provider struct and caches it.
Raises ArgumentError, naming the offending role, if a spec is malformed, its
module is missing, does not export new/1, does not implement
ExAgent.Provider, or its new/1 raises. Failing here means a missing
credential crashes at deploy time rather than returning a 401 on the first
request.
Every role is built before anything is written, so a bad spec cannot leave a half-applied set behind. Rebuilding replaces the previous set entirely.
Builds a fresh provider struct for role with overrides merged over its
configured options.
The result is not cached - see the caching note in the moduledoc. Building
reconstructs the Req client, which is fine per request but not inside a tight
loop. Option values already resolved at boot are reused, so a vault-backed
credential is not fetched again.
Returns the cached provider struct for role, or :error if it is not configured.
Returns the cached provider struct for role, raising if it is not configured.
@spec list() :: [atom()]
Returns the configured role names, in declaration order.