Configuration reference
fitguard init writes this file for you. This is what every field in it means, for when you want to change one by hand.
--config at? Set the environment variable
FITGUARD_CONFIG to the full YAML content below instead — fitguard uses it directly
when it's set, no file needed. This is what makes platforms that build straight from your GitHub repo (Render,
Railway, Heroku) work, since config.yaml is gitignored and never reaches them otherwise.
See the hosted platform deployment guide for a full walkthrough.
port: 8787
data_dir: .
providers:
openai:
api_key: ${OPENAI_API_KEY}
anthropic:
api_key: ${ANTHROPIC_API_KEY}
cache:
enabled: true
backend: memory # or "redis"
ttl_seconds: 300
budget:
backend: local # or "redis", for more than one instance
users:
user_123:
daily_limit_usd: 5
keys:
sk-guard-...: user_123 # what the caller sends, never your real provider key
fallback:
- gpt-4o-mini
dashboard: # written by `fitguard init` / `reset-dashboard-password`
session_secret: ...
users:
- username: admin
password_hash: $2a$10$...
Providers
One entry per provider you're routing through FitGuard. ${ENV_VAR} is expanded when the file loads, so a real key never has to sit in the file itself.
| Field | Notes |
|---|---|
providers.<name>.api_key | Required. Supports ${ENV_VAR} |
providers.<name>.base_url | Optional override, for self-hosted or proxied endpoints |
Recognized names are openai, anthropic, gemini, groq, and together. Anything else is still accepted and treated as an OpenAI-compatible endpoint, useful for a self-hosted model server, but routing only auto-detects those four names from the model field.
To add a provider to an existing config without the destructive overwrite flow of init, use fitguard add-provider.
Cache
| Field | Default | Notes |
|---|---|---|
cache.enabled | false | |
cache.ttl_seconds | 300 | |
cache.backend | memory | memory or redis |
cache.redis_url | — | Required if backend is redis |
Budget backend and Redis
budget.backend: local (the default) tracks spend in memory. It's
correct for exactly one running FitGuard instance. Run more than one behind a load
balancer without changing this, and each instance enforces its own copy of the budget,
so a user's real spend can land at roughly (instance count) × their limit.
budget: backend: redis redis_url: redis://localhost:6379/0 # omit to reuse cache.redis_url
With this set, every instance reserves and settles spend against the same Redis key, atomically, so the budget holds regardless of which instance a request lands on. Get Redis running locally with docker run -d -p 6379:6379 redis:7-alpine or brew install redis.
Failing closed
By default, a key whose user_id has no matching entry under
users: spends without a limit. FitGuard warns about this at every startup, but a warning
is easy to scroll past, and the usual cause is a typo rather than an intention.
budget: fail_closed: true
With this on, that request is refused instead. Two things it deliberately does not change:
- An explicit
daily_limit_usd: 0still means unlimited. That's someone saying the key is unmetered on purpose, which is different from forgetting to configure it. - Single-tenant mode is unaffected. With no
keys:at all, there's no per-user budget to miss.
It also makes a failed spend lookup refuse rather than pass: if FitGuard can't tell whether you're under budget, failing closed means it says no.
Users and keys
| Field | Notes |
|---|---|
users.<id>.daily_limit_usd | Omit the user entirely for an unlimited budget |
keys.<token> | Maps a virtual key to the user_id whose budget applies |
If keys: is empty, FitGuard runs in single-tenant mode: every request is unauthenticated and shares one "default" identity. Fine for a solo local setup, not once more than one caller can reach the port.
Fallback
A flat list of model names, tried in order if the primary model's request errors, rate-limits, or 5xxs. Each one is routed the same way the primary model was, so a fallback to claude-haiku-4-5 after a gpt-4o failure hits your anthropic provider entry, if you've configured one.
Every entry is validated against providers: at load time (and again on any dashboard settings save): a model whose provider isn't configured is rejected outright, rather than silently failing the first time it's actually needed as a fallback. A custom or self-hosted model not in FitGuard's built-in price table still works if named provider/model (e.g. together/my-model), which ties it to a configured provider explicitly.
Pricing overrides
FitGuard's built-in price table (how it works) is a manually maintained snapshot of each provider's published pricing, so it can go stale. Correct a drifted price, or register a model the table doesn't know about yet, with a pricing: block — no release needed:
pricing:
gpt-4o:
input_per_1k: 0.0025
output_per_1k: 0.010
some-brand-new-model:
input_per_1k: 0.001
output_per_1k: 0.003
provider: openai # required only when the model isn't already known
| Field | Notes |
|---|---|
pricing.<model>.input_per_1k / output_per_1k | USD per 1,000 tokens. Always required |
pricing.<model>.provider | Only needed when adding a model not already in the built-in table; correcting an existing entry doesn't need it |
pricing.<model>.embedding | Set true for a new embedding-only model |
Applied once at startup, logged as pricing: N model price override(s) applied from config.
Dashboard login
Separate from keys: above — this gates the human-facing /dashboard page, not API callers. Set by fitguard init or fitguard reset-dashboard-password, never by hand.
| Field | Notes |
|---|---|
dashboard.session_secret | Signs session cookies. Generated once; don't edit |
dashboard.users[].username | Dashboard login username (one account, currently) |
dashboard.users[].password_hash | bcrypt hash, not the password itself |
dashboard.session_ttl_hours | How long a login lasts. Defaults to 168 (7 days) |
session_ttl_hours your exposure window, worth shortening on anything internet-facing. To revoke every session immediately, run fitguard reset-dashboard-password: it rotates session_secret, which invalidates all outstanding cookies at once.
If dashboard.users: is empty, /dashboard has no login at all — anyone who can reach the port can see it. FitGuard warns about this at every startup.