Docs/Gateway
Gateway

Set up LiteLLM as your AI gateway

Updated August 26, 2026 · 9 min read

A raw vLLM endpoint has no concept of who is calling it. Everyone shares one URL, nobody has a key of their own, and you cannot answer "which team burned the GPU on Tuesday". LiteLLM sits in front and gives you per-user keys, budgets, and one address that can route to your Spark or to a cloud model without the caller knowing which.

On this page
  1. What it actually buys you
  2. Pin the version first
  3. Install with Docker Compose
  4. Write config.yaml
  5. Issue virtual keys
  6. Budgets and rate limits
  7. Fallbacks and hybrid routing
  8. Point your clients at it

What it actually buys you

LiteLLM is a proxy that speaks the OpenAI API on the front and roughly a hundred provider APIs on the back. On a private setup the interesting parts are narrower than the marketing suggests, and there are four of them.

If you have one model, one user and no compliance requirements, skip this page. The gateway earns its keep at the point where a second team starts calling your endpoint.

Pin the version first

Do not install LiteLLM from an unpinned tag. On 24 March 2026 an attacker published malicious 1.82.7 and 1.82.8 packages to PyPI after stealing a publish token from the project's CI pipeline. Per PyPI's incident report, the packages were live for about 40 minutes and shipped a .pth file that ran a credential stealer whenever the Python interpreter started, with no import litellm required. The project's own security update covers the response; 1.83.0 was the first clean release afterwards.

Nothing about that is unique to LiteLLM, and it is not a reason to avoid the project. It is a reason to pin an exact version or image digest here, and to treat any gateway that holds every provider credential you own as a high-value target.

Install with Docker Compose

Virtual keys, budgets and spend tracking all need Postgres, so the database is not optional once you want the features that justify running this. Use the litellm-database image, which includes the migration step.

# /workspace/litellm/docker-compose.yml
services:
  litellm:
    # pin a real version, not :latest
    image: docker.litellm.ai/berriai/litellm-database:v1.83.0
    restart: unless-stopped
    ports:
      - "127.0.0.1:4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    environment:
      DATABASE_URL: postgresql://litellm:${PG_PASSWORD}@db:5432/litellm
      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
      LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
    command: --config /app/config.yaml
    depends_on:
      - db

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${PG_PASSWORD}
      POSTGRES_DB: litellm
    volumes:
      - ./pgdata:/var/lib/postgresql/data

Generate the secrets into a .env beside it, and never commit that file:

cat > .env <<EOF
PG_PASSWORD=$(openssl rand -hex 16)
LITELLM_MASTER_KEY=sk-$(openssl rand -hex 24)
LITELLM_SALT_KEY=$(openssl rand -hex 24)
EOF
chmod 600 .env

The salt key encrypts stored provider credentials in the database. Changing it later makes every saved credential unreadable, so set it once and back it up with the same care as the credentials themselves.

Note the port binding: 127.0.0.1:4000:4000, not 4000:4000. The gateway holds every key you own, and there is no reason for it to listen on a public interface. Reach it over an SSH tunnel, or put a reverse proxy with TLS in front of it.

Write config.yaml

This is where your own models get names. The openai/ prefix is what tells LiteLLM to treat the backend as OpenAI-compatible, which is exactly what vLLM is:

# /workspace/litellm/config.yaml
model_list:
  # the model on your Spark
  - model_name: spark-qwen
    litellm_params:
      model: openai/qwen3-coder-30b
      api_base: http://localhost:8000/v1
      api_key: "none"

  # a second Spark, same alias: LiteLLM load-balances between them
  - model_name: spark-qwen
    litellm_params:
      model: openai/qwen3-coder-30b
      api_base: http://spark-2.internal:8000/v1
      api_key: "none"

  # an escape hatch for when the fleet is busy
  - model_name: cloud-backup
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY

litellm_settings:
  drop_params: true
  num_retries: 2

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

Two aliases pointing at the same model_name is not a mistake. LiteLLM treats entries sharing a name as a pool and distributes requests across them, which is how a second Spark gets added with no client changes at all.

drop_params: true silently discards parameters the backend does not understand rather than returning a 400. That is what you want with mixed backends, since a client sending an OpenAI-specific field should not break against a local model. It also means a typo in a parameter name fails quietly, so turn it off while debugging.

Bring it up and check it loaded what you expect:

docker compose up -d
docker compose logs -f litellm

curl http://localhost:4000/v1/models \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Issue virtual keys

The master key is an admin credential. It should live in your password manager and never in an application config. What applications get is a virtual key, minted against the master key:

curl http://localhost:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "models": ["spark-qwen"],
    "metadata": {"team": "support-bot"},
    "max_budget": 50,
    "budget_duration": "30d",
    "rpm_limit": 60
  }'

The response contains a sk- key scoped to exactly what you listed. This one can call spark-qwen and nothing else, so a compromised support bot cannot start spending against your cloud provider. Revoke it with /key/delete and no other caller notices.

Give every service its own key. The cost is one API call, and the benefit shows up the first time you need to answer what a key was used for, or turn one off in a hurry.

Budgets and rate limits

On your own hardware the meaningful limit is not dollars, it is contention: one enthusiastic batch job can make the shared chat interface unusable for everyone else. Rate limits are the useful control, and they apply per key.

rpm_limit and tpm_limit cap requests and tokens per minute. Set rpm_limit low on anything automated and leave interactive users generous, which keeps a runaway agent loop from starving the humans. max_budget with budget_duration matters mostly for keys that can reach a paid provider, where it is a genuine spend cap rather than a fairness knob.

Spend per key, per team and per model is queryable from the /spend/report endpoint, and the admin UI at http://localhost:4000/ui renders the same data if you would rather click than curl.

Fallbacks and hybrid routing

The honest reason most private deployments end up with a cloud key configured: a single Spark is one box, and one box reboots. Fallbacks make that a latency event rather than an outage.

litellm_settings:
  fallbacks:
    - spark-qwen: ["cloud-backup"]
  # separate list for "prompt was too long for the local model"
  context_window_fallbacks:
    - spark-qwen: ["cloud-backup"]
  num_retries: 2

Be deliberate about this one. A fallback to a cloud provider means that under exactly the conditions you did not plan for, prompts you intended to keep on your own hardware leave the building. If the whole point of the Spark is that data does not go to a third party, set the fallback to a second local instance and let requests fail when both are down. That is a policy decision, not a configuration detail, and it is worth writing down next to the config.

Point your clients at it

Everything that speaks OpenAI works unchanged, with the base URL swapped and a virtual key in place of an OpenAI one:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:4000/v1",
    api_key="sk-your-virtual-key",
)

resp = client.chat.completions.create(
    model="spark-qwen",
    messages=[{"role": "user", "content": "Summarise this ticket."}],
)

For Open WebUI, set OPENAI_API_BASE_URLS to http://localhost:4000/v1 and OPENAI_API_KEYS to a virtual key. It will list whatever that key is allowed to see, so the model picker becomes a per-key view of your fleet without any change on the Open WebUI side.

NextWhich models fit in 128 GB Back toAll docs

One gateway, one Spark, no per-token bill.

Deploy a dedicated DGX Spark in EU-Central. $50 free on your first $100 top-up.

Deploy a Spark