Husk
GitHub

Models

Cost and budgets

How Husk estimates spend before a call, where the maxCostUsd ceiling is enforced, which catalog prices are guesses, and why a local model is genuinely zero.

Every ceiling in Husk is checked before the next model call, using an estimate of what that call will cost. Checking afterwards detects an overspend exactly one call too late, which is one call too many.

The four buckets

A model's price is four numbers, all in USD per million tokens:

FieldMeaningWhen absent
inputPerMTokPrompt tokens0
outputPerMTokCompletion tokens0
cacheReadPerMTokTokens served from a prompt cacheinputPerMTok × 0.1
cacheWritePerMTokTokens written into a prompt cache0

The buckets add rather than overlap: Anthropic reports input_tokens excluding anything served from or written to the cache, so the arithmetic is a straight sum. Providers that do not do prompt caching report zero for the cache buckets and nothing changes.

A cache write defaults to free rather than to a fraction of input. Most providers charge nothing to populate a cache entry, and guessing high there would invent cost that never appears on a bill. Anthropic does charge for it, and the catalog spells out the number per model.

import { breakdown, costOf, formatUsd } from '@husk-ai/models';
 
const usage = { inputTokens: 12_000, outputTokens: 800, cacheReadTokens: 40_000 };
costOf(info, usage);      // 0.0546
breakdown(info, usage);   // { inputUsd, outputUsd, cacheReadUsd, cacheWriteUsd, totalUsd }
formatUsd(0.0546);        // "$0.0546"

Totals are rounded to eight decimal places — sub-cent precision matters when a run makes forty calls. formatUsd prints $0.00 for zero, six significant decimals below a cent, and four decimals above it.

Estimating before the call

There is no tokeniser. estimateTokens is Math.ceil(text.length / 3.6), plus a 4-token framing allowance per message, 16 tokens of preamble for a tool list, and a flat 1,200 tokens for an image. Shipping a 2 MB BPE table to save a budget guard from a 15% error is the wrong trade.

Two numbers come out of the same price table:

  • minimumCostUsd(info, inputTokens) — the prompt alone. You pay for it whatever happens, and the model is free to answer with nothing. This is what a budget guard compares against, because refusing on an optimistic estimate lets through a call that then blows the budget.
  • maximumCostUsd(info, inputTokens, maxOutputTokens) — the prompt plus the model filling its entire output allowance.

Where the ceiling is enforced

There are three ceilings, and they are not the same one.

CeilingDefaultSet inEnforced by
limits.maxCostUsd0.5husk.yamlThe agent's Budget, before every model call
maxCostUsd5~/.husk/config.jsonModelRouter, when candidates are chosen
--max-costfrom the specthe command lineThe agent's Budget

In the agent loop

Budget.check() runs before every model call and refuses on the first ceiling it crosses:

the next call would cost about $0.5231, over the $0.5 ceiling

It also carries the step, token and wall-clock ceilings:

reached the step ceiling of 24
ran out of time after 300s
the next call would reach about 204813 tokens, over the 200000 ceiling

The run ends with stopReason: 'budget', 'step_limit' or 'timeout' — not with an error. Whatever the agent produced up to that point is still in the result.

When the model's price is unknown — a local model, or a router that cannot introspect — the estimate falls back to the most expensive call observed so far in this run. That is $0 on the first call and honest after that.

In the router

ModelRouter filters candidates by minimumCostUsd(candidate, promptTokens) <= limit.

An explicitly named model is never quietly swapped for a cheaper one. If you asked for opus and opus is unaffordable, that is an error, not a downgrade:

error anthropic/claude-opus-5 would cost at least $0.0600 for this prompt, over the $0.05 limit
hint:  Raise maxCostUsd, shorten the prompt (~12000 tokens), or use a free model: `--model free`.

A dynamic alias — auto, free, local — filters instead. If every reachable model is priced out, you get one error rather than a silent downgrade:

error no auto model can serve a ~12000 token prompt for under $0.05
hint:  Raise maxCostUsd, or use `--model ollama/qwen2.5:7b`.

Over the control plane

POST /v1/husks/:name/run clamps the request against the husk file:

maxCostUsd: Math.min(body.maxCostUsd ?? spec.limits.maxCostUsd, spec.limits.maxCostUsd)

A caller can lower the ceiling for one run and cannot raise it above what the husk.yaml declares.

Why local is zero

Ollama and LM Studio models are priced at inputPerMTok: 0, outputPerMTok: 0 and flagged free: true. That is not a rounded-down estimate — no money moves. husk run prints free rather than a dollar figure:

3 steps · 1m07s · 4162 tokens · free

maxCostUsd: 0 in a husk.yaml therefore means "local models only, and fail rather than fall back to anything that bills". It is the strongest cost guarantee Husk offers, and it is one line.

Which prices are real

The catalog is honest about its own confidence. Prices marked estimatedPricing: true are not published figures — they are conservative estimates derived from the previous generation of the same tier, and they deliberately err high so a budget guard refuses early rather than late.

Estimated today: every Anthropic model except Haiku 4.5, gpt-5 and gpt-5-mini, both DeepSeek models, both Mistral models, both Cerebras models, both Together models, and openrouter/deepseek/deepseek-chat.

Published (as far as we know, and only as fresh as the file): the GPT-4.1 family, the GPT-4o family, o3, o4-mini, claude-haiku-4-5, the Gemini 2.5 family, both Groq models.

husk models --json prints the flag, so a script can tell the two apart:

husk models --json | jq -r '.[] | select(.estimatedPricing) | .id'

Reading the real number back

The router fills in usage.costUsd from the catalog whenever the provider did not report one, and warns when the actual cost lands above the ceiling the estimate cleared:

warn  anthropic/claude-sonnet-5 cost $0.6120, over the $0.5000 ceiling; the estimate was low

That warning is the audit trail for a bad estimate. If you see it often on a model, its catalog price is wrong — which is worth a bug report, because the whole guard depends on it.

husk run husk.yaml "" --json | jq '.usage'
{ "inputTokens": 12480, "outputTokens": 806, "costUsd": 0.049 }