Husk talks to eleven model providers through one interface. ModelRouter in
@husk-ai/models answers four questions and nothing else:
- What is reachable right now? —
detect() - Which model does this alias mean? —
resolve() - Can we afford it? — the budget check
- What do we do when the provider says no? — the fallback chain
A provider is only listed once it is reachable and reports at least one model. Providers has the environment variable and the sign-up path for each one.
Aliases
Nine aliases resolve to a fixed model id. Three resolve to a strategy, because their honest answer depends on what is running on this machine right now.
| Alias | Resolves to |
|---|---|
opus | anthropic/claude-opus-5 |
sonnet | anthropic/claude-sonnet-5 |
haiku | anthropic/claude-haiku-4-5-20251001 |
gpt | openai/gpt-4.1 |
gemini | google/gemini-2.5-pro |
flash | google/gemini-2.5-flash |
gemma | ollama/gemma3 |
llama | ollama/llama3.2 |
qwen | ollama/qwen2.5-coder |
local | strategy: first available Ollama or LM Studio model |
free | strategy: best free model that is actually reachable |
auto | strategy: best available, preferring quality then cost |
resolveAlias tries five things, in this order, and stops at the first that answers:
- A user override from
modelAliasesin~/.husk/config.json. - A dynamic alias —
local,free,auto. - A canonical alias from the table above.
- An explicit
provider/modelid. Split at the first slash, sotogether/meta-llama/Llama-3.3-70B-Instruct-Turbois one model, not two. - A bare model name, but only when exactly one provider in the catalog offers it.
Anything else throws E_MODEL_UNAVAILABLE and lists the aliases it does know.
Pinning your own
{
"modelAliases": {
"fast": "groq/llama-3.3-70b-versatile",
"sonnet": "anthropic/claude-sonnet-5"
}
}An override may point at another alias. The resolver follows up to eight hops and refuses a cycle rather than hanging:
error Model alias "a" loops back on itself
hint: Fix the modelAliases entry for "a" in ~/.husk/config.json.What auto, free and local actually pick
Every reachable model is pooled, filtered by affordability, then sorted:
| Strategy | Pool | Sort |
|---|---|---|
auto | every reachable model | quality descending, then input price ascending |
free | every reachable model | free models first, then input price, then quality |
local | only ollama and lmstudio | free first, then input price, then quality |
"Quality" is a maintainer's judgement number in the catalog, 0–100, used only to break
ties. It is not a benchmark score. A model nobody has catalogued — a fresh ollama pull
— scores 40 if it is free and 50 if it is not.
Detection
detect() probes every registered provider in parallel and caches the answer for
30 seconds. Each probe is capped at 3 seconds; a provider that hangs is reported as
unavailable with the reason rather than stalling the report. Nothing here throws — the
entire purpose of the call is to render husk doctor.
Providers with a key requirement answer instantly from the environment. Keyless local servers — Ollama, LM Studio — are probed over the network with their own 2-second timeout.
Fallback
RouterRequest.fallbacks and a husk's fallbackModels build an ordered candidate list:
the requested model first, then each declared fallback, then whatever the strategy
ordering turns up. Each candidate is tried in turn.
Two rules shape the behaviour:
- A downgrade is never silent. The caller gets a
warningevent naming what failed and what replaced it. A run that quietly finished on an 8B local model when it asked for Opus is worse than a run that failed. - A 400 is never retried, anywhere. The request is malformed; shopping a malformed request around six providers wastes six round trips.
Within one candidate, the router retries up to 3 times total with exponential backoff
and full jitter, 400 ms base and an 8 s ceiling. It does not retry an abort, a
malformed-request error, E_NO_CREDENTIALS, or E_BUDGET_EXCEEDED.
On a stream, a candidate can only be abandoned before it has produced content. Once
a token has reached the caller there is nothing honest to do with a mid-stream failure
except report it, so that case ends in an error event rather than a silent restart on
a different model.
Cost
Every response comes back with a costUsd. When the provider reports one, Husk uses it;
when it does not, Husk computes it from the catalog price and the reported token counts.
A local model is genuinely $0.00, not a rounded-down estimate.
Cost and budgets covers the estimate, the maxCostUsd ceiling, and which
prices in the catalog are published figures rather than conservative guesses.
Where to go next
- Providers — every provider, its environment variable, and how to get a key. Four have a real free tier.
- Local models — Ollama and LM Studio end to end, and an honest account of what a 7B model can and cannot do as an agent.
- Cost and budgets — estimation, the ceiling, and why local is zero.