LiteLLM and Model Routing: The Proxy Pattern for Multi-Provider LLM Apps
Every team that moves beyond a single LLM provider hits the same infrastructure problem. You started with OpenAI, then someone wants Claude for long-context tasks, another team spun up a local Ollama instance for privacy-sensitive work, and now your billing is spread across four dashboards, your codebase has four different client libraries, and rotating a compromised API key means touching seventeen files. The proxy pattern solves this. LiteLLM is the most widely adopted open-source implementation of it.
This post covers the full picture: what the proxy pattern actually does, how LiteLLM is architected, how to configure routing and fallbacks, how to run it in production with cost tracking and observability, and an honest comparison against the managed alternatives. If you operate your own AI stack — homelab, startup, or enterprise infra team — this is the pattern you need to understand before you write another provider-specific client.
The Proxy Pattern and Why It Exists
The core idea is simple. You run a service that speaks OpenAI’s API on the inbound side and speaks whatever the target provider requires on the outbound side. Your applications all call the same local endpoint. The gateway handles translation, routing, retry logic, cost accounting, and policy enforcement.
┌─────────────────────────────────────────────────────┐
│ Your Applications │
│ │
│ Python app Node service curl / cline / aider │
└───────────────────────┬─────────────────────────────┘
│ OpenAI-compatible API
│ POST /v1/chat/completions
│ Authorization: Bearer sk-...
▼
┌─────────────────────────────────────────────────────┐
│ LiteLLM Proxy │
│ │
│ Virtual Keys Budget Tracking Guardrails │
│ Load Balancing Fallback Chains Observability │
└─────┬───────────────┬─────────────────┬────────────┘
│ │ │
┌────────▼──┐ ┌───────▼──────┐ ┌─────▼──────────┐
│ OpenAI │ │ Anthropic │ │ Local Ollama │
│ GPT-4o │ │ Claude 3.7 │ │ Qwen3-Coder │
└───────────┘ └──────────────┘ └────────────────┘
The benefits compound as you scale. Every API key lives in one place. Every model call is logged in one system. Cost budgets are enforced at the gateway, not in application code. When a provider has an outage, the fallback triggers automatically rather than surfacing a 500 to your users. Adding a new provider — say, swapping one Azure endpoint for another, or adding a regional Bedrock deployment — happens in a config file with zero application changes.
LiteLLM is not the only way to implement this pattern. But it is the one with the most traction in the open-source ecosystem, the widest provider support (100+ LLMs as of 2025), and the most direct path from a single-file Python script to a full production deployment with Postgres, Redis, and a management UI.
Two Interfaces: SDK vs Proxy
LiteLLM ships in two modes that share the same routing and translation logic but serve different integration points.
The Python SDK (pip install litellm) is a drop-in for openai that adds provider translation. You call litellm.completion() with a model string that encodes the provider prefix, and LiteLLM calls the right API:
|
|
Provider-specific parameters (temperature, top_p, system prompts for providers that use a separate system field, etc.) are normalized by LiteLLM before the request goes out. The response object is always the same shape. This is useful for library code or one-off scripts, but it does not give you centralized key management, cross-team cost tracking, or fallback logic that survives a Python process restart.
The Proxy (litellm --config config.yaml) is an actual HTTP server — FastAPI under the hood — that any client can call. Your application does not need to know about LiteLLM at all. You point your OPENAI_BASE_URL to http://litellm-proxy:4000/v1 and everything works. The proxy is the pattern that scales.
Most serious deployments run the proxy. The SDK is useful for experimentation, testing, and embedding in libraries that need portable multi-provider support without requiring a separately deployed service.
Core Configuration: config.yaml
The proxy is driven entirely by a YAML config file. Understanding this file’s structure is understanding LiteLLM.
|
|
The model_name field is what clients pass to the API. Multiple entries with the same model_name create a pool — requests to gpt-4o above will load-balance between the OpenAI and Azure deployments. This is the core mechanism behind failover and multi-region routing.
Routing Strategies
When multiple deployments share a model_name, LiteLLM picks between them using a routing strategy. The available strategies have meaningfully different trade-offs:
| Strategy | How it picks | Best for |
|---|---|---|
simple-shuffle |
Weighted random by rpm/tpm capacity | Default; balanced across a pool |
least-busy |
Deployment with fewest in-flight requests | CPU-bound local models |
latency-based-routing |
Exponentially weighted moving avg of p50 latency | Mixed cloud + local where latency varies |
usage-based-routing |
Least tokens-per-minute consumed | Near-limit providers, tpm fairness |
cost-based-routing |
Cheapest deployment with remaining budget | Cost-optimized inference |
latency-based-routing is the most useful in practice for mixed-provider setups. It maintains a rolling latency average per deployment and weights traffic toward the faster endpoint. If your Azure instance in East US is faster than West US for your users, traffic drifts there automatically without manual tuning.
For purely cost-sensitive workloads — batch processing, evals, non-interactive pipelines — cost-based-routing combined with provider budget caps creates a natural cost ceiling:
|
|
When OpenAI hits its $50 daily budget, the router stops sending traffic there and routes entirely to Azure until the window resets. No manual intervention required.
Fallback Chains
Fallbacks are distinct from load balancing. Load balancing distributes successful requests across a pool. Fallbacks activate on failure — 429 rate limits, 5xx errors, timeouts, or content policy rejections.
LiteLLM supports three layers of fallback:
|
|
The execution path when a primary fails:
Request → gpt-4o (Azure East US)
│
├─ 429 Rate Limit
│ └─ retry 3x with exponential backoff
│ └─ still failing
│ └─ fallback: claude-sonnet
│ └─ success → return response
│
├─ 413 Context Too Long
│ └─ context_window_fallback: gpt-4-turbo
│
└─ 400 Content Policy
└─ content_policy_fallback: claude-sonnet
The distinction between these three failure types matters. A rate limit is transient — the model is available, just busy. Retrying with backoff is appropriate before escalating. A context window error is structural — the input is too large for this model, and retrying is pointless; you need a different model. A content policy rejection means the provider is refusing the content entirely, and your fallback provider may have different guardrails.
For order-based priority routing (primary + standby, not a parallel pool), use the order field on deployments:
|
|
This is useful for multi-region deployments where you want your primary region to handle traffic and only failover to a secondary when it’s unavailable.
Virtual Keys and Team Budgets
The virtual key system is what makes LiteLLM usable in multi-team environments. Instead of distributing actual provider API keys, you distribute LiteLLM-issued virtual keys. The proxy is the only service that holds real provider credentials.
Virtual keys are created via the admin API or the web UI:
|
|
The response contains a key field starting with sk- — the token your ML team uses in their OPENAI_API_KEY environment variable. It looks identical to an OpenAI key and works the same way from the client’s perspective.
Team budget enforcement works at the proxy level before any provider is called:
Request with virtual key sk-ml-team-xxx
│
├─ Lookup key in Postgres
├─ Check team budget: $487.32 spent of $500.00 allowed
├─ Budget remaining: $12.68
├─ Estimated request cost: $0.18
├─ OK — forward to provider
└─ On response: record spend → $487.50 spent
When a team hits their budget, requests return a 429 with a clear budget-exceeded message rather than a cryptic provider error. You can set soft limits (warn at 80%) and hard limits (block at 100%) independently. Budgets can be reset automatically on a daily, weekly, or monthly cadence.
Teams can also be restricted to specific model pools:
|
|
This is the same mechanism used to enforce that privacy-sensitive data never leaves your network — route a team to local-only models, and the proxy refuses requests to external providers regardless of what model string the client sends.
Observability
LiteLLM’s callback system fires on every request and response. Callbacks are defined in litellm_settings and receive a full StandardLoggingPayload with timing, tokens, cost, model used, and the actual request/response content.
Prometheus is the lowest-friction integration for teams already running a metrics stack:
|
|
Langfuse captures the full trace — inputs, outputs, latency, cost per call — and is the right choice if you want to debug why a particular generation went wrong:
|
|
Every request gets a litellm_call_id that propagates through to Langfuse as the trace ID. You can pass application-level metadata through metadata in the request body and it shows up in Langfuse:
|
|
OpenTelemetry is the path for teams already invested in Jaeger, Tempo, or a commercial APM:
|
|
LiteLLM generates spans for each request including provider roundtrip time, retry attempts, and fallback activations. This makes it possible to see at a glance that your p95 latency spike came from fallback retries to a slow Azure region rather than from application code.
Datadog is supported as a full integration with a custom LiteLLM tile available in the Datadog marketplace, collecting the same Prometheus metrics via the Datadog Agent.
Production Deployment
The minimal production setup requires three components: the LiteLLM proxy, a Postgres database for key/spend persistence, and Redis for distributed rate limiting when running multiple proxy instances.
|
|
For multiple proxy instances behind a load balancer (required for high availability), all instances share the same Postgres and Redis. Redis is what keeps the per-model rpm/tpm counters synchronized across instances — without it, each instance tracks independently and you’ll blow past provider limits.
LiteLLM’s own production documentation recommends at minimum 4 CPU cores and 8 GB RAM for the proxy under meaningful load. The proxy is async (FastAPI + uvicorn workers), so it handles concurrent requests well, but the Postgres connection pool is the typical bottleneck at scale.
For Kubernetes deployments, a Helm chart is available at ghcr.io/berriai/litellm and the standard advice applies: run at least 2 replicas, use a connection pooler (PgBouncer) in front of Postgres if you’re running many replicas, and use a Redis Sentinel or Redis Cluster setup rather than a single Redis instance.
┌─────────────────┐
│ Load Balancer │
│ (nginx/Traefik)│
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────▼───┐ ┌────▼───┐ ┌────▼───┐
│LiteLLM │ │LiteLLM │ │LiteLLM │
│ :4000 │ │ :4000 │ │ :4000 │
└────┬───┘ └────┬───┘ └────┬───┘
└──────────┬──┘─────────────┘
┌────▼────┐
│ Redis │ ← rpm/tpm sync across instances
└─────────┘
┌─────────┐
│Postgres │ ← keys, teams, spend, audit log
└─────────┘
Guardrails
LiteLLM supports content filtering and PII detection as middleware that runs in the proxy before forwarding to providers or after receiving responses. Guardrails integrate with external systems (Presidio, Lakera Guard, Aporia, AWS Bedrock Guardrails) and can also be custom Python callbacks.
A basic PII detection setup using Presidio:
|
|
Guardrails can be applied globally, per-team, or per-key. A team handling regulated data can have mandatory PII masking while an internal tooling team has none, all through the same proxy without any application code changes.
Custom guardrails are Python classes with async_pre_call_hook and async_post_call_success_hook methods:
|
|
Register it in config.yaml:
|
|
Auto Routing and Complexity Classification
LiteLLM introduced auto routing as a way to automatically select a model based on the estimated complexity of the incoming request, rather than requiring callers to specify a model tier. A complexity router uses a fast classifier model to bucket requests into simple/medium/complex and routes each bucket to a different deployment.
|
|
The appeal is cost reduction without application logic: a batch job that generates summaries routes short inputs to gpt-4o-mini at $0.15/1M tokens and only escalates to Claude Opus for genuinely complex reasoning tasks. In practice, the classification is imperfect — complexity classifiers struggle with domain-specific difficulty and tend to over-promote short but semantically dense requests. Treat this as a heuristic optimization, not a reliable quality gate. If output quality matters, explicit model selection from the application is more predictable.
Tag-Based Routing
Tags let you route specific requests to specific deployment pools without requiring callers to know deployment details:
|
|
Callers specify tags in the request body:
|
|
Combined with virtual key restrictions per team, this becomes a data residency enforcement mechanism: keys issued to teams handling PII have tags: ["private"] baked into their configuration, and the proxy refuses any request from those keys that would route to an external provider.
Comparison: LiteLLM vs Managed Alternatives
The LLM gateway space has consolidated around three distinct positions:
| LiteLLM (self-hosted) | Portkey (managed/self-hosted) | OpenRouter (managed) | |
|---|---|---|---|
| Hosting | Self-hosted (Docker/K8s/bare metal) | Managed cloud or self-hosted | Cloud-only |
| Provider count | 100+ | 200+ | 200+ |
| Cost | Server costs ($20-100/mo) + API spend | $49/mo platform + API spend | 5.5% fee on API spend |
| Key management | Full — virtual keys, teams, budgets | Full | Basic |
| Spend tracking | Full — per team, key, model, day | Full + semantic cost allocation | Dashboard only |
| Semantic caching | Basic | Advanced (fuzzy matching) | No |
| Guardrails | Full — Presidio, Lakera, custom | Full + managed policy sets | No |
| Observability | Prometheus, Langfuse, OTEL, Datadog | Proprietary + integrations | Basic |
| Data stays in your infra | Yes | Self-hosted tier only | No |
| Setup complexity | Medium (config.yaml + Docker) | Low (managed) | Minimal |
| Community | Active open-source | Commercial | Commercial |
OpenRouter wins on time-to-working. One API key, access to 200+ models, zero infrastructure. The 5.5% fee is negligible at low spend but becomes meaningful at scale — at $10K/month API spend that’s $550/month in fees. There are also no virtual keys, no team budgets, and no on-premises data residency option.
Portkey is the best option if you want enterprise observability features (semantic caching, advanced tracing, managed guardrail policies) without operating your own infra. The managed tier keeps your data off your servers, which is a disqualifier for compliance-sensitive workloads. The self-hosted Enterprise tier addresses this but pricing is opaque.
LiteLLM is the choice when you need data residency, custom guardrails, integration with existing Prometheus/Langfuse/OTEL infrastructure, or team budget enforcement that your finance team can actually see in a dashboard. The operational cost is real: you maintain the deployment, the Postgres instance, and the Redis cluster. But for organizations that already operate Kubernetes or Docker Swarm stacks, this is a solved problem.
Common Pitfalls
Not using Redis for multi-instance deployments. Without Redis, rpm/tpm counters are per-instance. Three proxy replicas each think they’re allowed 1000 rpm to OpenAI, so your actual combined rate is 3000 rpm — until OpenAI 429s all of them simultaneously.
Forgetting context window fallbacks. Provider rate limits get a lot of attention, but input truncation is equally disruptive. A request that exceeds gpt-4o’s 128K context window returns a 400, not a 429, and the default fallback logic doesn’t catch it unless you explicitly configure context_window_fallbacks. Long document processing pipelines that don’t set this will fail silently on larger inputs.
Using master key in application code. The master key has full admin access to the proxy — it can create and delete other keys, access all logs, and reconfigure the proxy. Application workloads should always use virtual keys with scoped permissions and budgets.
Treating model_name as opaque. Clients pass model_name to the proxy, but some clients also send this string to the provider directly in error reporting or tracing. Using provider-specific names (gpt-4o, claude-3-5-sonnet-20241022) as your model_name means your abstraction leaks into observability data and you lose the ability to silently swap providers without client changes.
Ignoring the warm-up period for latency-based routing. On startup, all deployments have equal latency estimates. Traffic distributes evenly until enough requests accumulate to build accurate estimates. If you’re doing load testing immediately after deployment, the first few minutes of results reflect random distribution, not the steady-state routing behavior.
Not pinning the image tag. litellm:main-stable tracks the release branch and can introduce breaking config changes between updates. For production, pin to a specific version tag (ghcr.io/berriai/litellm:v1.x.y) and upgrade deliberately.
Integrating with Existing Tools
Because the proxy is OpenAI-compatible, any tool that accepts OPENAI_BASE_URL and OPENAI_API_KEY works without modification:
|
|
The proxy also exposes /v1/models which returns all configured model aliases. Tools that enumerate available models will see your aliases rather than provider-specific model IDs, which is typically what you want.
The Honest Trade-off
LiteLLM is genuinely useful infrastructure for any team running more than one LLM provider, and the proxy pattern is the right abstraction for this problem. But it adds a service to operate. That service has a database, a cache, and real upgrade cadence — the project moves fast and config schema changes between versions.
For a solo developer experimenting with multiple providers, the SDK mode or OpenRouter is probably the right starting point. For a team of five shipping a product, the overhead of a self-hosted proxy starts to pay off in reduced operational toil around key management and cost visibility. For a twenty-person team with a dedicated platform function, LiteLLM becomes a foundational piece of infrastructure alongside your CI/CD and logging stack.
The question to answer before deploying it: do you need the things the gateway provides — centralized keys, team budgets, provider-agnostic clients, observability across providers — or do you just need a quick way to call multiple APIs? The first case is a gateway problem. The second case is probably just an environment variable.
Comments