Husk
GitHub

Models

The model router

One surface over eleven providers — how an alias becomes a model, what happens when that model says no, and which code path your command actually takes.

Husk talks to eleven model providers through one interface. ModelRouter in @husk-ai/models answers four questions and nothing else:

  1. What is reachable right now?detect()
  2. Which model does this alias mean?resolve()
  3. Can we afford it? — the budget check
  4. What do we do when the provider says no? — the fallback chain
husk models

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.

AliasResolves to
opusanthropic/claude-opus-5
sonnetanthropic/claude-sonnet-5
haikuanthropic/claude-haiku-4-5-20251001
gptopenai/gpt-4.1
geminigoogle/gemini-2.5-pro
flashgoogle/gemini-2.5-flash
gemmaollama/gemma3
llamaollama/llama3.2
qwenollama/qwen2.5-coder
localstrategy: first available Ollama or LM Studio model
freestrategy: best free model that is actually reachable
autostrategy: best available, preferring quality then cost

resolveAlias tries five things, in this order, and stops at the first that answers:

  1. A user override from modelAliases in ~/.husk/config.json.
  2. A dynamic alias — local, free, auto.
  3. A canonical alias from the table above.
  4. An explicit provider/model id. Split at the first slash, so together/meta-llama/Llama-3.3-70B-Instruct-Turbo is one model, not two.
  5. 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

~/.husk/config.json
{
  "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:

StrategyPoolSort
autoevery reachable modelquality descending, then input price ascending
freeevery reachable modelfree models first, then input price, then quality
localonly ollama and lmstudiofree 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 warning event 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.