Once you're making more than a few thousand LLM calls per day across multiple models and providers, you need a layer that handles routing, failover, rate limits, and cost governance. Doing this ad-hoc in application code doesn't scale — you end up with provider-specific logic scattered across services with no central visibility.
Three tools dominate this space: LiteLLM (self-hosted, open source), OpenRouter (managed SaaS aggregator), and Portkey (observability-focused proxy). They're not interchangeable.
LiteLLM: The Open-Source Proxy
LiteLLM provides a unified API over 100+ model providers. Deploy it as a proxy server and all your services call a single endpoint — LiteLLM handles provider SDKs, auth rotation, retry logic, and routing under the hood.
Docker Compose Setup
# docker-compose.yml
version: "3.8"
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
ports:
- "4000:4000"
volumes:
- ./litellm_config.yaml:/app/config.yaml
command: --config /app/config.yaml --port 4000 --num_workers 8
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- GEMINI_API_KEY=${GEMINI_API_KEY}
redis:
image: redis:alpine
ports:
- "6379:6379"
# litellm_config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY
rpm: 500 # rate limit: requests per minute
tpm: 2000000 # rate limit: tokens per minute
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-5
api_key: os.environ/ANTHROPIC_API_KEY
rpm: 200
tpm: 800000
- model_name: gemini-flash
litellm_params:
model: gemini/gemini-2.5-flash
api_key: os.environ/GEMINI_API_KEY
rpm: 1000
tpm: 4000000
router_settings:
routing_strategy: latency-based-routing
enable_pre_call_checks: true
litellm_settings:
cache: true
cache_params:
type: redis
host: redis
port: 6379
ttl: 600 # 10 minutes
general_settings:
master_key: ${LITELLM_MASTER_KEY}
database_url: ${DATABASE_URL} # PostgreSQL for audit logs
With this config, your services call http://litellm:4000/v1/chat/completions using standard OpenAI SDK syntax. No provider-specific code anywhere else.
Per-Team Budget Controls
LiteLLM's virtual keys let you assign spending limits by team or service:
# Create a virtual key for the "support-bot" team with a $500/month budget
curl -X POST http://litellm:4000/key/generate \
-H "Authorization: Bearer $MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"key_alias": "support-bot-prod",
"team_id": "support",
"max_budget": 500,
"budget_duration": "monthly",
"models": ["gemini-flash", "claude-haiku"],
"tpm_limit": 100000
}'
When the team hits $500, their key stops working until the next billing cycle. No manual intervention, no surprise overruns.
Fallback Chains
# In litellm_config.yaml
router_settings:
fallbacks:
- {"claude-sonnet": ["gpt-4o", "gemini-flash"]}
- {"gpt-4o": ["claude-sonnet", "gemini-flash"]}
context_window_fallbacks:
- {"gpt-4o": ["claude-sonnet"]} # Claude has larger context window
num_retries: 3
request_timeout: 30
If Claude hits a rate limit, requests automatically fall back to GPT-4o. If GPT-4o hits context window limits (the input is too long), fall back to Claude Sonnet which has a larger window. This happens transparently — your application doesn't know which model actually ran.
OpenRouter: Managed Aggregation
OpenRouter is a SaaS layer you call instead of individual provider APIs. No infrastructure to manage. It supports 200+ models across all major providers, handles routing, and adds a small margin (~5–10%) on top of provider costs.
from openai import OpenAI
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key="your-openrouter-key"
)
# Use any model from any provider
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4-5",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={
"HTTP-Referer": "https://yoursite.com",
"X-Title": "Your App Name"
}
)
# Or use OpenRouter's auto-routing
response = client.chat.completions.create(
model="openrouter/auto", # OpenRouter picks model
messages=[{"role": "user", "content": "Summarize this document..."}]
)
OpenRouter's key advantage over LiteLLM is zero ops overhead. No container to run, no config to maintain. The trade-off: less control over routing logic, no private key management, and you're dependent on their uptime.
Use OpenRouter when: you're a small team, you don't want to maintain infrastructure, and the 5–10% markup is worth the operational simplicity.
Use LiteLLM when: you need fine-grained routing control, per-team budget enforcement, private key management, or custom audit logging.
Portkey: Observability-First Proxy
Portkey positions itself as "the AI gateway with observability built in." It's a proxy like LiteLLM but with heavier emphasis on traces, cost analytics, and A/B testing of prompts.
from portkey_ai import Portkey
portkey = Portkey(
api_key="your-portkey-key",
virtual_key="anthropic-virtual-key" # stored in Portkey's vault
)
response = portkey.chat.completions.create(
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": "Analyze this data..."}],
metadata={"user_id": "usr_123", "feature": "report-gen"}
)
Portkey's dashboard surfaces cost per user, cost per feature, latency distributions, and error rates — without you building any logging infrastructure. If your primary pain point is "I don't know where my AI spend is going," Portkey is faster to get value from than self-hosted LiteLLM.
Comparison: Which to Use
| Capability | LiteLLM | OpenRouter | Portkey |
|---|---|---|---|
| Self-hosted | Yes | No | Optional |
| Open source | Yes | No | No |
| Provider coverage | 100+ | 200+ | 100+ |
| Per-team budgets | Yes | Limited | Yes |
| Request routing | Highly configurable | Auto/manual | Configurable |
| Observability/tracing | Basic | Basic | Strong |
| Cost overhead | ~$0 (infra cost) | 5–10% markup | $49+/month |
| Setup time | 2–4 hours | 15 minutes | 30 minutes |
Key Features to Actually Use
Provider failover is the most critical feature in any of these tools. Don't hardcode a single provider — always configure fallbacks. Provider outages happen, rate limits get hit, and having an automatic fallback to a second provider means your application stays up.
Rate limit handling. LiteLLM respects per-model TPM/RPM limits and queues or routes around them automatically. Without this, you'll hit 429 errors under load and need to implement retry logic in every service.
Cost budgets per team. Don't leave this until you need it. Set per-service monthly budgets on day one. A runaway agent loop that would otherwise burn $5,000 in a weekend gets stopped at $100 if you configured the limit.
Audit logging. Every LLM call should be logged with timestamp, model, input/output token counts, cost, and request metadata. Portkey does this out of the box. LiteLLM writes to PostgreSQL. Both are queryable for cost audits.