Example configs¶
Copy and adapt. They load and validate as shipped.
quickstart.toml¶
The smallest useful config: one provider, one model. Used by the README quick start.
# The smallest useful config: one provider, one model. Clients ask for `fast`.
# Used by the README quick start:
# export GROQ_API_KEY=... # the only key it needs
# docker run -d -p 127.0.0.1:8080:8080 -e GROQ_API_KEY \
# -v "$PWD/quickstart.toml:/etc/humpyard/config.toml:ro" ghcr.io/gschaetz/humpyard:<version> (the tag in the README)
#
# `0.0.0.0` is for inside a container, where the default loopback address is unreachable from the
# host; the docker command above publishes the port on 127.0.0.1 only, so the gateway is not exposed
# to your network. Running the binary directly on a shared network? Use listen = "127.0.0.1:8080".
# There are no [keys] here, so anyone who can reach the port can spend your provider key: keep it local
# or add virtual keys (docs/budgets.md) before exposing it.
listen = "0.0.0.0:8080"
[providers.groq]
base_url = "https://api.groq.com/openai/v1"
api_key_env = "GROQ_API_KEY"
[targets.fast]
endpoints = [{ provider = "groq", model = "openai/gpt-oss-120b" }]
config.toml¶
The general example: providers, targets, routes, keys, budgets, health.
listen = "127.0.0.1:8080"
# shutdown_grace_secs = 30 # how long to let in-flight requests finish after SIGTERM/Ctrl-C (0-3600)
# decision_retention_days = 90 # keep routing decisions this long in the ledger database (0 = forever)
# reload_poll_secs = 15 # also re-read this file every 15 s and reload if it changed (0 = off;
# # SIGHUP always reloads). Use under Kubernetes, where nothing can signal.
# Providers: OpenAI-compatible endpoints. Keys come from environment variables only.
[providers.groq]
base_url = "https://api.groq.com/openai/v1"
api_key_env = "GROQ_API_KEY"
timeout_secs = 120
max_retries = 1 # extra attempts on this provider before failing over
# headers = { "x-app" = "humpyard" } # optional static headers on every call
[providers.openrouter]
base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"
# Targets: a named model served by an ordered list of endpoints (failover order).
[targets.fast]
endpoints = [
{ provider = "groq", model = "openai/gpt-oss-120b" },
{ provider = "openrouter", model = "meta-llama/llama-3.3-70b-instruct" },
]
[targets.smart]
endpoints = [{ provider = "openrouter", model = "anthropic/claude-sonnet-4.5" }]
# Routes: what clients request as `model`. Types: passthrough, random, stage_router, llm_classifier.
[routes.auto]
type = "stage_router"
efficient = ["fast"]
capable = ["smart"]
mode = "efficient_first"
confidence_threshold = 0.5
# Selectors choose the route from request facts, first match wins; without a match the requested
# model is the route. A rule is skipped when the key may not use its route (allowed_routes).
# [[select]]
# when = { profile = "deep" } # client header x-humpyard-profile: deep
# route = "smart-only"
# [[select]]
# when = { header = { "x-client" = "openclaw" }, subagent = true }
# route = "agent"
# [[select]]
# when = { prompt_tokens = { min = 30000 } } # also: tools = true, images = true
# route = "long-context"
# Any route can limit which failures move on to the next target (default: all of them):
# fallback_on = ["overflow"] # classes: overflow, rate_limit, timeout, server_error, connection, forbidden
# An escalation route additionally needs a judge target:
# [routes.agent]
# type = "llm_classifier"
# mode = "escalation" # or "capability"
# efficient = ["fast"]
# capable = ["smart"]
# judge = ["fast"]
# Endpoint health (all optional; these are the defaults). After `failure_threshold` consecutive
# failures an endpoint is skipped for `cooldown_secs` (doubling each time, up to
# `max_cooldown_secs`), then probed with one request. 0 turns it off.
# [health]
# failure_threshold = 3
# cooldown_secs = 30
# max_cooldown_secs = 300
auto-routing.toml¶
One auto model whose rules pick the pool by intent. See the recipe.
# Auto routing: clients always ask for the model `auto`; selector rules pick the pool by intent.
# See docs/routing.md ("Auto routing recipe"). Check with:
# humpyard check-config examples/auto-routing.toml (needs the three *_KEY variables set)
# and try a decision without sending anything:
# curl localhost:8080/v1/route/explain -H "Authorization: Bearer $KEY" \
# -d '{"model":"auto","headers":{"x-humpyard-profile":"private"}}'
listen = "127.0.0.1:8080"
[ledger]
path = "humpyard.db"
[providers.local] # a model server on your own machine or network
base_url = "http://127.0.0.1:11434/v1"
api_key_env = "LOCAL_KEY"
[providers.free] # a free or cheap aggregator
base_url = "https://free.example/v1"
api_key_env = "FREE_KEY"
[providers.paid]
base_url = "https://paid.example/v1"
api_key_env = "PAID_KEY"
# quota_source = "opencode-go" # optional: read this provider's usage windows (see docs/routing.md)
# price_source = "models-dev:openrouter" # optional: `humpyard prices check` compares prices with it
# ---- pools: one target per kind of work ----
[targets.local-model]
class = "private" # a label; a route that sets allowed_classes enforces it ("Strict routes")
endpoints = [{ provider = "local", model = "local-coder" }]
[targets.free-fast]
class = "public"
endpoints = [{ provider = "free", model = "fast-small" }]
[targets.paid-fast]
class = "paid"
endpoints = [{ provider = "paid", model = "fast-small" }]
[targets.paid-capable]
class = "paid"
endpoints = [{ provider = "paid", model = "capable-large" }]
[targets.paid-long]
class = "paid"
endpoints = [{ provider = "paid", model = "long-context" }]
[targets.paid-vision]
class = "paid"
endpoints = [{ provider = "paid", model = "vision" }]
# ---- routes: what the rules can choose. Keep a key's `allowed_routes` in step with these. ----
[routes.cheap] # everyday work: free first, paid when the free tier fails
type = "passthrough"
targets = ["free-fast", "paid-fast"]
[routes.capable] # tool-using turns and anything needing a stronger model
type = "passthrough"
targets = ["paid-capable"]
[routes.long-context]
type = "passthrough"
targets = ["paid-long"]
[routes.vision]
type = "passthrough"
targets = ["paid-vision"]
[routes.private-pool] # stays on your own hardware, enforced: see "Strict routes" in docs/routing.md
type = "passthrough"
targets = ["local-model"]
allowed_classes = ["private"] # any target of another class is removed before routing, so a
# future edit that adds a public fallback here fails to load
# `auto` is itself a safe route: it is where a request lands when every rule is skipped (a key may
# not use any rule's route) or none matches.
[routes.auto]
type = "passthrough"
targets = ["free-fast", "paid-fast"]
# ---- the auto rules: first match wins, and EVERY rule is scoped to model = "auto" ----
# (Selectors see every request. Without the model condition a client asking for `capable`
# directly would be re-routed too.)
[[select]] # 1. privacy first: a client says "private" and nothing else wins
name = "private-profile"
when = { model = "auto", profile = "private" }
route = "private-pool"
[[select]]
name = "private-tag"
when = { model = "auto", tag = { privacy = "private" } }
route = "private-pool"
[[select]] # 2. images need a vision model
name = "images"
when = { model = "auto", images = true }
route = "vision"
[[select]] # 3. very long prompts need a long-context model
name = "long-prompt"
when = { model = "auto", prompt_tokens = { min = 30000 } }
route = "long-context"
[[select]] # 4. sub-agents do narrow work: keep it cheap
name = "subagent"
when = { model = "auto", subagent = true }
route = "cheap"
[[select]] # 5. tool-using turns get the stronger model
name = "tool-turns"
when = { model = "auto", tools = true }
route = "capable"
[[select]] # 6. catch-all for `auto`: everyday chat
name = "everyday"
when = { model = "auto" }
route = "cheap"
budgets.toml¶
Virtual keys with daily and monthly limits.
# Keys, prices, ledger and budgets. Mint a key with: humpyard keygen alice
listen = "127.0.0.1:8080"
# Where every upstream call is recorded. Required when any key has a limit.
[ledger]
path = "humpyard.db"
# Past this fraction of any limit a key is "restricted": targets whose output price is above the
# ceiling are skipped (tiers degrade, e.g. capable -> efficient).
[budget]
restricted_at = 0.8
restricted_max_output_price = 5.0 # USD per million output tokens; omit for no price ceiling
[keys.alice]
sha256 = "sha256:0000000000000000000000000000000000000000000000000000000000000001"
allowed_routes = ["auto"] # omit to allow everything
daily_usd = 5.0
monthly_usd = 50.0
over_budget = "block" # HTTP 402 at the limit
[keys.ci]
sha256 = "sha256:0000000000000000000000000000000000000000000000000000000000000002"
daily_tokens = 2000000
over_budget = "free_only" # keep going on zero-priced targets only
[providers.openrouter]
base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"
# Every endpoint needs a price when any key has a USD budget (0 is fine for free models).
[targets.fast]
endpoints = [{ provider = "openrouter", model = "meta-llama/llama-3.3-70b-instruct", price = { input = 0.1, output = 0.3 } }]
[targets.smart]
endpoints = [{ provider = "openrouter", model = "anthropic/claude-sonnet-4.5", price = { input = 3.0, output = 15.0, cached_input = 0.3, tiers = [{ above_tokens = 200000, input = 6.0, output = 22.5, cached_input = 0.6 }] }}]
[targets.free]
endpoints = [{ provider = "openrouter", model = "meta-llama/llama-3.3-70b-instruct:free", price = { input = 0.0, output = 0.0 } }]
[routes.auto]
type = "stage_router"
efficient = ["fast"]
capable = ["smart"]