By default zoder picks the best free model for a task and sends every model
in the fallback chain to the single default_provider endpoint. That is fine
when one provider serves the whole free pool, but it cannot express "use my
subscription model first, then fall back to a different provider's free
models." This document covers the three config knobs that make that work.
Each [[providers]] entry may declare a serves list of model-id prefixes
it serves. A routed model id is sent to the provider with the longest
matching prefix (most specific wins; config order breaks exact-length ties).
A model that no provider claims falls back to default_provider — so this is
purely additive and never changes behavior for existing single-provider setups.
# A flat-rate subscription model on its own endpoint.
[[providers]]
id = "minimax"
# Optional for the common unique mapping custom.minimax -> minimax.
# Set it when more than one Zeroclaw profile has the alias "minimax":
# engine_provider_ref = "custom.minimax"
base_url = "https://api.minimax.io/v1"
kind = "openai-chat"
auth = { type = "env", var = "MINIMAX_API_KEY" }
paid = false # flat-rate subscription => $0 marginal => treated as free
billing = "free"
serves = ["MiniMax-"] # MiniMax-M3, MiniMax-Text-01, ... -> this provider
# A free open-weight endpoint (any OpenAI-compatible host): open-weight
# model families only (NOT the metered azure/aws/oci/gcp/google catalog).
[[providers]]
id = "openweight"
base_url = "https://your-openai-compatible-host.example/v1"
kind = "openai-chat"
auth = { type = "env", var = "OPENWEIGHT_API_KEY" }
paid = false
billing = "free"
serves = ["deepseek-ai/", "meta/llama-", "mistralai/", "qwen/"]engine_provider_ref is the exact Zeroclaw providers.models profile used
for agentic execution. It is separate from zoder's provider id, so existing
short ids remain valid. During configuration load zoder auto-derives an absent
mapping from an exact reference or a unique alias match, using explicit
endpoint/kind metadata to disambiguate. If multiple profiles remain possible,
loading fails with a clear request to set engine_provider_ref; dispatch
never chooses one arbitrarily.
The mapping compares Zeroclaw's effective transport and endpoint, not only
the literal kind and uri fields. Zoder preserves the canonical
implementation selected by kind, then resolves omitted or ignored values
through that factory's rules and the typed provider family (including regional
selectors such as MiniMax endpoint = "cn" and computed fields such as Azure
resource plus deployment). Native Groq and OpenRouter, for example, ignore
a configured URI and always use their fixed endpoints; a different
implementation selected from an OpenRouter-family profile can still consume
its explicit URI. An OpenAI profile with requires_openai_auth = true is
projected as the distinct openai-codex implementation and, without an
explicit URI, resolves to the ChatGPT subscription Codex Responses endpoint
rather than api.openai.com; zoder's configured Codex base is normalized with
the same /responses suffix before comparison. A family whose effective
identity cannot be resolved is rejected instead of treating omission as a
wildcard.
With the above, a single zoder exec fallback chain can run MiniMax-M3 on
api.minimax.io and then meta/llama-3.3-70b-instruct on the open-weight host
— each link hits the provider that actually serves it. The policy gate runs
per link: a fallback that resolves to a paid / metered provider is skipped
fail-closed (use --allow-paid to permit it), and the spend ledger records the
provider that actually served the winning model.
Prefixes should be delimiter-bounded (deepseek-ai/, meta/llama-,
MiniMax-) so a short prefix like meta cannot also capture metamath/.... An
empty prefix is rejected by config validation (it would capture every model).
A vendor overlay's [profile] may pin a primary_model. The router always
tries it first, then ranks the rest of the free pool (by capability ×
latency × live health, the usual rank_key) as the fallback chain behind it.
The pin is honored even if the model is not itself in the free pool (e.g. a
subscription model), so an empty free pool is not an error in this path.
# config.minimax.toml — pin MiniMax first; the free open-weight models fall back.
[profile]
name = "minimax"
primary_model = "MiniMax-M3"primary_model is independent of [profile].default — an overlay can pin the
primary model without owning the default_provider. If several overlays set it,
the default-claiming overlay wins, otherwise the alphabetically-last one. It
applies to zoder exec (oneshot + agentic) and zoder route.
zoder refresh reconciles the model corpus against live /models catalogs. In
addition to the default provider, it now queries every provider that declares
serves and is billed free, filters that provider's returned ids to its own
serves allowlist, and folds the survivors into the routing pool as free,
routable chat candidates.
For the free open-weight provider above this means the open-weight models
(deepseek-ai/* | meta/llama-* | mistralai/* | qwen/*) become routable, while
the host's azure/ aws/ oci/ gcp/ google-prefixed (metered/hosted) catalog
entries are dropped because they do not match the serves allowlist. Safety
rails:
- A model the corpus already classifies as paid (or with nonzero per-token economics) is never silently flipped to free.
- A newly-ingested, unbenched model gets a neutral capability prior so it is selectable as a fallback until the corpus builder benches it; a real benchmark always overrides the prior.
- Reconciliation runs against the union of every provider's served ids, so a
free provider's models are not retired by the default provider's narrower list;
a model that genuinely leaves all served catalogs is retired (and its
freeflag cleared).
Run it after wiring a new free provider:
zoder refresh # adds the free open-weight models; prints how many were promoted
zoder route "..." # confirm MiniMax-M3 leads, the free models fall back