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.

No file on disk to point --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.

FieldNotes
providers.<name>.api_keyRequired. Supports ${ENV_VAR}
providers.<name>.base_urlOptional 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

FieldDefaultNotes
cache.enabledfalse
cache.ttl_seconds300
cache.backendmemorymemory or redis
cache.redis_urlRequired 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:

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.

If FitGuard can't reach the Redis you've configured, it refuses to start rather than silently falling back to unprotected local tracking. A connection error at startup means Redis isn't reachable, not that anything is broken.

Users and keys

FieldNotes
users.<id>.daily_limit_usdOmit 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
FieldNotes
pricing.<model>.input_per_1k / output_per_1kUSD per 1,000 tokens. Always required
pricing.<model>.providerOnly needed when adding a model not already in the built-in table; correcting an existing entry doesn't need it
pricing.<model>.embeddingSet 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.

FieldNotes
dashboard.session_secretSigns session cookies. Generated once; don't edit
dashboard.users[].usernameDashboard login username (one account, currently)
dashboard.users[].password_hashbcrypt hash, not the password itself
dashboard.session_ttl_hoursHow long a login lasts. Defaults to 168 (7 days)
There is no session revocation list. Sessions are stateless signed cookies, so a leaked one stays valid until it expires — which makes 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.

Esc