The goal: every team that calls an LLM through your LiteLLM proxy gets its own key. That key only reaches the models the team is approved for, stops when it hits its monthly budget, and cannot flood a shared upstream deployment. When a team asks "why are we getting 400s?" or finance asks "who spent what?", you can answer from the proxy instead of guessing.
This post covers the setup and the day-to-day admin calls. It assumes the proxy is already running and routing to at least one upstream model.
Prerequisites
Virtual keys, budgets and spend tracking need a database. Without one the proxy answers key and budget calls with No connected db. and budgets are not enforced.
| Requirement | Where it goes |
|---|---|
| PostgreSQL database | DATABASE_URL env var or general_settings.database_url |
Master key starting with sk- | LITELLM_MASTER_KEY env var or general_settings.master_key |
| Model list | model_list in config.yaml |
A minimal config.yaml:
model_list:
- model_name: chat-standard
litellm_params:
model: azure/gpt-4.1-mini-prod
api_base: os.environ/AZURE_API_BASE
api_key: os.environ/AZURE_API_KEY
- model_name: chat-premium
litellm_params:
model: azure/gpt-4.1-prod
api_base: os.environ/AZURE_API_BASE
api_key: os.environ/AZURE_API_KEY
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
The model_name values are what teams put in their requests and in key allow-lists. Keep them stable and generic (chat-standard), so you can change the upstream deployment without touching every team's key.
Warning
The master key can create, read and delete every key. Store it in a secret store, never hand it to an application, and rotate it if it appears in a log or a chat message.
Set two shell variables for the examples below:
PROXY=https://llm-gw.internal.example.com
MK=$LITELLM_MASTER_KEY
Create a team with a budget
Put the budget and limits on the team, not on each key. A key that belongs to a team is checked against the team budget (and team member budgets), not the key owner's personal budget. The details of this changed in newer releases, so check the notes for your version.
curl -sS "$PROXY/team/new" \
-H "Authorization: Bearer $MK" -H "Content-Type: application/json" \
-d '{
"team_alias": "service-desk-bot",
"models": ["chat-standard"],
"max_budget": 500,
"budget_duration": "30d",
"tpm_limit": 200000,
"rpm_limit": 300
}'
max_budgetis in the currency of your model cost map, usually USD. Spend is calculated by the proxy from token counts and its price table, so it is an estimate and not your invoice.budget_durationresets spend on a schedule. Values like30s,30m,30hand30dare accepted. If you leave it out, the budget never resets.tpm_limitandrpm_limitare enforced by the proxy. If you run several proxy replicas, configure Redis so the counters are shared. Otherwise each replica enforces the limit on its own.
Save the team_id from the response.
Issue a key for the team
curl -sS "$PROXY/key/generate" \
-H "Authorization: Bearer $MK" -H "Content-Type: application/json" \
-d '{
"team_id": "<team_id>",
"key_alias": "service-desk-bot-prod",
"models": ["chat-standard"],
"metadata": {"owner": "servicedesk-platform", "env": "prod", "ticket": "CHG-4812"}
}'
The response contains the key (sk-...). It is only shown once, so put it straight into the team's secret store. Always set key_alias and metadata. When a key shows up in an error message months later, these fields are how you trace it back to an owner.
Test the key with an OpenAI-compatible call:
curl -sS "$PROXY/v1/chat/completions" \
-H "Authorization: Bearer sk-team-key" -H "Content-Type: application/json" \
-d '{"model":"chat-standard","messages":[{"role":"user","content":"Reply with OK"}],"max_tokens":5}'
The same key used with "model":"chat-premium" should fail, because that model is not on the key's or the team's list.
Day-to-day checks
Spend and limits for a key or team
curl -sS "$PROXY/key/info?key=sk-team-key" -H "Authorization: Bearer $MK"
curl -sS "$PROXY/team/info?team_id=<team_id>" -H "Authorization: Bearer $MK"
Compare spend with max_budget and look at budget_reset_at. A team near its limit in week one usually has a batch job or a retry loop. Ask them for the call pattern before you raise the budget.
Raise a budget or change models
curl -sS "$PROXY/team/update" \
-H "Authorization: Bearer $MK" -H "Content-Type: application/json" \
-d '{"team_id": "<team_id>", "max_budget": 750, "models": ["chat-standard", "chat-premium"]}'
Record the change ticket in the team's metadata so the reason is kept next to the setting.
Block a key without deleting it
# Stop the key immediately (reversible)
curl -sS -X POST "$PROXY/key/block" \
-H "Authorization: Bearer $MK" -H "Content-Type: application/json" \
-d '{"key": "sk-team-key"}'
# Restore it
curl -sS -X POST "$PROXY/key/unblock" \
-H "Authorization: Bearer $MK" -H "Content-Type: application/json" \
-d '{"key": "sk-team-key"}'
Blocking keeps the spend history and settings. Use it for a leaked key or a runaway client while you investigate. Delete the key only after a replacement is in place.
Caution
POST /key/deleteremoves the key at once and every client using it starts failing. Block first, confirm in the logs that traffic has moved to the new key, then delete.
Reading budget and limit errors
| What the client sees | Cause | Fix |
|---|---|---|
type: budget_exceeded, message names the virtual key or key_alias | Key or team spend reached max_budget | Check /team/info. Wait for the reset, or raise the budget with an approval |
ExceededBudget: Crossed spend within team | A team member's own budget inside the team is used up | Raise that member's budget or move the workload to its own key |
| 429 with a rate limit message | Key or team tpm_limit/rpm_limit reached | Spread the load, or raise the limit if upstream quota allows it |
| 401, key not valid or blocked | Key blocked, deleted, mistyped or expired | /key/info; check blocked and expires |
| Error saying the key can't access the model | Model not on the key's or team's models list | Add it with /team/update or /key/update |
No connected db. | Proxy started without a database | Set DATABASE_URL and restart |
Exact message text and status codes (400 vs 429) for budget errors differ between releases. Alert on the type field rather than on the full message.
Prevent it next time
- Create teams and keys from a script or pipeline, not by hand, and always include
key_alias, owner and ticket metadata. - Give every key an expiry (
durationon/key/generate, for example"90d") and a rotation reminder. - Set team TPM limits below the upstream deployment's quota, so one team cannot use up capacity that others share.
- Run Redis with multiple proxy replicas so rate limits and budgets are counted across the whole fleet.
- Compare proxy spend with the provider's billing once a month. Large gaps usually mean the price table is out of date or traffic is bypassing the gateway.
- Back up the proxy's PostgreSQL database. It holds every key, team and spend record. If you lose it, every team needs new keys.