AI DevelopmentTutorial

LiteLLM as Your LLM Gateway: Budgets, Fallbacks and a Safe Setup

October 14, 2026
12 min read
LiteLLM gateway with virtual keys, budgets and fallbacks
Share:

Every team that talks to more than one LLM provider ends up in the same mess: API keys pasted into five repos, one shared account where nobody knows who burned the budget, and a provider outage that takes your product down with it. A gateway puts one door in front of all of that.

In this guide I show you how to self-host a LiteLLM proxy: one OpenAI-compatible endpoint, virtual keys per team, budgets and rate limits, fallbacks across providers and spend logs, plus how to run it safely after the March 2026 PyPI incident. Every key and endpoint comes from the official docs (LiteLLM 1.105.1, October 14, 2026); I checked them against the docs, I did not load-test them.

1. The Problem: Provider Keys Everywhere, Visibility Nowhere

Calling providers directly feels simple until the third integration. Each service holds its own copy of the keys, each SDK has its own error format and retry logic, and finance gets one invoice per provider with no breakdown by feature or team. When one provider has a bad hour, every service handles it differently, usually by failing.

The Most Common Mistake

Handing the master key to the apps. The master key is the proxy admin credential: whoever holds it can mint keys, read spend and change budgets. Applications should only ever see scoped virtual keys, one per workload, each with its own model list, budget and rate limit. If one sk- key sits in every .env, you built a shared secret, not a gateway.

A gateway fixes this at the network layer. Clients speak the OpenAI API to one base URL; the proxy maps model names to providers, enforces who may spend what, retries and falls back when a provider fails, and writes every request and its cost to a database. LiteLLM is the open-source option most teams reach for, and the rest of this guide builds it step by step.

2. Minimum Concepts Before Touching Code

Six ideas cover 90% of what you need. Get these straight and the config below reads like plain English.

🔀

model_name vs litellm_params

your alias, their model

model_name is the alias clients request; litellm_params.model is the upstream, like anthropic/claude-haiku-5-5. Swap the provider behind an alias without touching a single client.

🔑

Virtual keys

one key per workload

Keys the proxy issues (sk-...) through /key/generate. Each carries its own allowed models, budget, rate limits and expiry. They need a Postgres database; the master key mints them.

👥

Teams and users

who owns the spend

A team groups keys under one budget. Spend is tracked per key, user and team, so ‘who burned the budget’ finally has an answer.

💸

Budgets and rate limits

USD, TPM and RPM caps

max_budget plus budget_duration sets a USD cap that resets on a schedule; tpm_limit, rpm_limit and max_parallel_requests throttle. Set them on a key, a user or a team.

🛟

Retries vs fallbacks

same model, then another

num_retries retries the same model group; fallbacks moves to a different group when retries run out. Cooldowns pull a failing deployment out of rotation for a while.

🧾

Spend logs

the audit trail

Every request writes a row to the LiteLLM_SpendLogs table: hashed key, user, team, tags, tokens and cost. That table is what you query, export and prune.

The mental model to keep

Clients know one URL and one key. The proxy knows providers, prices and policies. Keep that boundary clean (no provider keys in apps, no policy in app code) and swapping a model becomes a config change, not a deploy.

3. Step 1 — Run the Proxy: Pinned Image + config.yaml

Start with the config. Aliases are the contract with your clients, so name them by role (fast, smart), not by vendor. Provider keys are read from the environment with os.environ/..., never written in the file. Virtual keys, budgets and spend logs need a database, so the proxy also needs DATABASE_URL.

litellm_config.yaml — aliases, master key, log hygiene

# litellm_config.yaml
model_list:
  - model_name: fast            # alias clients request, named by role
    litellm_params:
      model: anthropic/claude-haiku-5-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: fast-backup     # same role, different provider
    litellm_params:
      model: openai/gpt-5-mini  # swap for the IDs your providers list today
      api_key: os.environ/OPENAI_API_KEY
  - model_name: smart
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  disable_error_logs: true
  maximum_spend_logs_retention_period: "30d"
  maximum_spend_logs_retention_interval: "1d"

Now run it. Pin an exact image tag instead of latest (1.105.1 is the latest stable on the releases page as I write; 1.106 release candidates exist, so check when you read this) and bind the port to localhost behind your own TLS reverse proxy. Generate the master key and the salt key once and keep them in a secret manager.

Run it — master key, salt key, pinned image

export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)"
export LITELLM_SALT_KEY="sk-$(openssl rand -hex 32)"  # keep it; never change after adding models
export DATABASE_URL="postgresql://litellm:<password>@<db-host>:5432/litellm"

docker run -d --name litellm \
  -p 127.0.0.1:4000:4000 \
  -v "$(pwd)/litellm_config.yaml:/app/config.yaml:ro" \
  -e LITELLM_MASTER_KEY -e LITELLM_SALT_KEY -e DATABASE_URL \
  -e LITELLM_MODE=PRODUCTION \
  -e ANTHROPIC_API_KEY -e OPENAI_API_KEY \
  ghcr.io/berriai/litellm:v1.105.1 \
  --config /app/config.yaml

Tip: no database, no budgets

Without a database the proxy still serves the OpenAI API, but virtual keys, team budgets and spend tracking do not work. The docs warn that litellm_settings.max_budget is not enforced without a DB: requests keep flowing past the cap and the only signal is a startup warning. Also, LITELLM_SALT_KEY encrypts the credentials you store, so regenerate it and they become unreadable.

4. Step 2 — Teams, Virtual Keys, Budgets and Rate Limits

With the proxy up, use the master key to create structure and then never again from an app. A team carries the shared budget; keys under it carry tighter, per-workload limits. Every field below is documented on the virtual keys and budgets pages.

curl — a team and a scoped key

# 1) team: shared budget + defaults (the response includes a team_id)
curl -s http://localhost:4000/team/new \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "team_alias": "support-bot",
    "max_budget": 50,
    "budget_duration": "30d",
    "rpm_limit": 60,
    "models": ["fast", "fast-backup"]
  }'

# 2) key for ONE workload inside that team (the response includes the sk-... key)
curl -s http://localhost:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "team_id": "<team_id from step 1>",
    "models": ["fast", "fast-backup"],
    "max_budget": 10,
    "budget_duration": "7d",
    "rpm_limit": 30,
    "tpm_limit": 100000,
    "duration": "30d",
    "metadata": {"owner": "support-team"}
  }'

What this buys you

The key can call only fast and fast-backup, spend at most 10 USD per 7-day window, is throttled to 30 requests and 100k tokens per minute, and expires in 30 days. The team caps the whole workload at 50 USD per 30 days. Set the key tighter than the team so one noisy key cannot drain everyone’s budget. Without budget_duration a budget never resets.

Clients then use the plain OpenAI SDK. Point base_url at the gateway, pass the virtual key as api_key and tag requests with metadata.tags so the spend logs can be sliced by feature.

Python — OpenAI SDK against the gateway, with spend tags

from openai import OpenAI

client = OpenAI(
    api_key="sk-<virtual-key>",          # the scoped key, never the master key
    base_url="http://localhost:4000",    # the gateway
)

resp = client.chat.completions.create(
    model="fast",                        # alias from model_list
    messages=[{"role": "user", "content": "Summarise this ticket in one line."}],
    extra_body={"metadata": {"tags": ["feature:ticket-summary"]}},
)
print(resp.choices[0].message.content)

5. Step 3 — Fallbacks Across Providers and Spend Logs

Reliability lives in router_settings. num_retries retries the same model group; if it still fails, fallbacks sends the request to the next group, here a different provider. After allowed_fails failures a deployment goes on cooldown for cooldown_time seconds, so you stop hammering a dead endpoint. The numbers are placeholders: size them against your own traffic.

router_settings — retries, cooldown, cross-provider fallback

router_settings:
  routing_strategy: simple-shuffle   # default, recommended for production
  num_retries: 2                     # per model group, before falling back
  allowed_fails: 3                   # failures per minute before cooldown
  cooldown_time: 30                  # seconds out of rotation
  fallbacks: [{"fast": ["fast-backup"]}]

Two related keys: context_window_fallbacks (it needs enable_pre_call_checks: true) re-routes a prompt that does not fit the model’s context, and content_policy_fallbacks handles provider-side content refusals. You can also send fallbacks per request in the body, or block them for one key with metadata.disable_fallbacks. Then read the money back:

curl — read the spend back

# per-request rows, paginated (page_size max 1000)
curl -s "http://localhost:4000/spend/logs/v2?start_date=2026-10-07%2000:00:00&end_date=2026-10-14%2023:59:59&page=1&page_size=100" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

# spend grouped by team
curl -s "http://localhost:4000/global/spend/report?start_date=2026-10-01&end_date=2026-10-14&group_by=team" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

# one key: spend so far and its limits
curl -s "http://localhost:4000/key/info?key=sk-<virtual-key>" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Fallbacks respect permissions and budgets

By default the docs say fallbacks check the caller’s model access and budget, so a key limited to fast may need fast-backup in its list. Test that in staging. Two more traps: mock_testing_fallbacks is stripped from proxy requests since v1.85.0, so test failover with a real provider error in a non-production environment; and /spend/logs truncates at 10,000 rows, so use /spend/logs/v2 with page and page_size for exports.

6. Run It Safely: What the March 2026 Incident Teaches

On March 24, 2026, litellm 1.82.7 and 1.82.8 were published to PyPI carrying a credential stealer. LiteLLM’s own security update, NetSPI, Snyk and the GitHub issue thread agree on the core: two malicious releases, a payload that gathered environment variables, SSH keys, cloud and Kubernetes credentials and posted them to models.litellm.cloud (not an official domain), and a litellm_init.pth file in 1.82.8 that runs on every Python start, without importing litellm.

LiteLLM reports that the official proxy Docker image and 1.82.6 or earlier were not affected, that the compromise started with a Trivy dependency in its CI pipeline, and that a clean 1.83.0 shipped on March 30 through a rebuilt pipeline. Sources differ on how long the packages were live (about 40 minutes per LiteLLM, roughly three hours per Snyk), so do not lean on the window: if either version touched a machine, assume exposure.

If you ever installed 1.82.7 or 1.82.8

Treat the machine as compromised even if no litellm code ran: the .pth file executes at interpreter start, and Snyk notes that includes pip install itself. Search site-packages for litellm_init.pth, rotate every secret the host could read (provider keys, cloud credentials, SSH keys, database passwords, CI tokens, Kubernetes service accounts) and watch egress for models.litellm.cloud and checkmarx.zone. Pinning back to 1.82.6 does not undo an exposure.

Verify before you run

# A) Container: verify the signature (key pinned to an immutable commit)
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.105.1

# B) pip: exact pin + hashes for the whole dependency tree
#    requirements.in contains: litellm[proxy]==1.105.1
pip-compile --generate-hashes requirements.in -o requirements.txt
pip install --require-hashes -r requirements.txt

# C) Audit: was a bad build ever installed here?
pip show litellm | head -n 2
find / -name litellm_init.pth 2>/dev/null

🧱 Before it runs (supply chain)

  1. 1. Pin an exact version or image digest, never latest, and upgrade on purpose after reading the release notes
  2. 2. Verify the container signature with cosign against the pinned key commit before every deploy
  3. 3. On pip, install from a hash-pinned lockfile with --require-hashes so a swapped artifact fails the install

🔒 While it runs (blast radius)

  1. Egress allowlist: the proxy needs your provider APIs and Postgres, nothing else; deny the rest so a stolen secret has nowhere to go
  2. Least secrets: provider keys and the master key come from a secret manager, and the build step sees none of them
  3. Scoped keys: apps get virtual keys, never the master key; one key per workload so you can revoke one without a fire drill

🚪 What you expose (attack surface)

  1. Admin UI: set DISABLE_ADMIN_UI=True, or create a proxy_admin user, add SSO and turn on disable_env_credential_login so the master key stops working as a UI password
  2. API docs: set NO_DOCS, NO_REDOC and NO_OPENAPI to True in production
  3. Network: keep the gateway private behind TLS and set trusted_proxy_ranges to your ingress CIDRs

7. Common Errors (All of Them Fixable in Minutes)

Almost every gateway incident I have read about falls into one of these buckets. The left column is what sane deployments do; the right column is what pages you at night.

✅ Do this

  • • Pin the proxy version and verify the image signature; upgrade on purpose
  • • One virtual key per workload, with models, max_budget, budget_duration and rpm_limit set
  • • Name aliases by role and keep upstream model IDs in one config file
  • • Add a fallback on a different provider and test it with a real failure in staging
  • • Tag requests with metadata.tags and prune spend logs with a retention period
  • • Run on Python 3.10 or newer, which current releases require

❌ Not this

  • • Giving the master key to apps, CI jobs or teammates
  • • Running without a database and trusting max_budget
  • • Using the user field as identity: end users can set it and shift spend; set user_id on each key instead
  • • Loose version ranges or :latest images in production
  • • Exposing port 4000 and the Admin UI to the internet
  • • Installing on an old Python and assuming you got the latest: pip silently resolves down to the last compatible release

Golden rule

A gateway concentrates every secret and every request you have, which makes it one of the most valuable things on your network. Run it like one: pinned, verified, private, least-privilege, and watched through its own spend logs.

Conclusion

You now have the full loop: a pinned LiteLLM proxy with role-based aliases, a team and scoped virtual keys with budgets and rate limits, cross-provider fallbacks with cooldowns, spend you can query by key, team and tag, and a checklist for running it after the March incident.

Do one thing this week: put a single internal service behind the gateway with its own key, a small budget and a fallback, then read its spend logs after a few days. SSO, Redis and Kubernetes can follow. I have not load-tested this setup, so size workers, pool limits and Redis with the production guide before real traffic.

Tutorial Cheat Sheet

Proxy

  • • pinned version + cosign verify
  • • config.yaml aliases by role
  • • Postgres + master + salt key

Policy

  • • team → key hierarchy
  • • max_budget + budget_duration
  • • rpm/tpm limits + fallbacks

Safety

  • • hash-pinned installs
  • • egress allowlist
  • • Admin UI + API docs off

Sources

Diego Rodriguez

Diego Rodriguez

Senior Full-Stack & AI Engineer

Diego has 10+ years of experience building production-grade AI-powered applications, from LLM orchestration and RAG pipelines to ML-driven risk detection and algorithmic trading systems.

Learn more about Diego →