Skip to content

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"]