Serving Systems intermediate 9 min read 6 flashcards

LLM Gateways and Routing

Why every serious LLM deployment ends up behind a gateway, and how to choose between LiteLLM, Portkey, OpenRouter, and rolling your own.

You start with one provider key in an environment variable. Six months later you have four providers, fifteen models, three internal teams sharing the bill, two compliance reviews open, and an outage every time Anthropic's us-east region degrades. The LLM gateway is the piece of infrastructure that turns "we have an API key" into "we have a managed LLM platform." Either you build one or you run one of the open options.

What a gateway does that direct SDK calls do not

Capability Why you need it Direct SDK
Unified API across providers OpenAI, Anthropic, Groq, Gemini, Bedrock, Vertex all look the same to your app Per-provider SDK each
Rate-limit handling Provider 429s become routed retries instead of user-facing errors Manual try/except per call site
Failover and fallback Anthropic down -> Bedrock Claude -> OpenAI; transparent to the caller None
Cost attribution Per-team, per-user, per-feature spend, with budgets Read provider dashboard, manually allocate
Policy-based routing "Cheap model first, escalate on low-confidence" Hand-written if statements
Per-tenant quotas Free vs paid user limits enforced at the edge Implement in your app, get it wrong
Audit log Every prompt and response, indexed, exportable None; you wire your own
Centralised observability One trace UI for every LLM call, not one per provider Per-provider dashboards

The single biggest argument for a gateway: when you have N apps and M models, you have N*M wiring problems. A gateway makes it N + M.

The open options

LiteLLM (BerriAI). Python SDK + standalone proxy server. Unifies 100+ providers behind the OpenAI-compatible Chat Completions API. The proxy supports virtual keys per team/user, spend tracking, budgets, fallbacks, retries, and a usable admin UI. MIT-licensed, the de-facto default for self-hosted gateways. Easiest path: run the proxy in a container, point all your apps at it, give each team a virtual key.

Portkey. Hosted (with a self-host option) gateway with strong observability and prompt-management UX. Routes through 250+ models, ships caching, guardrails, and a prompt library. Picks itself when the team wants a dashboard rather than a config file.

OpenRouter. A unified billing layer in front of every model. Not really self-hosted - it is a marketplace. One API key, you can call Claude, GPT, Llama, Mistral, anything. The pitch is operational simplicity at the cost of putting your traffic through a third party. Strong for prototyping, agent development, and teams that want one invoice instead of twelve.

Cloudflare AI Gateway. Edge-deployed gateway with built-in caching and analytics. Picks itself if you are already on Cloudflare and want zero infra.

Roll your own. Sometimes the right answer at scale. A 500-line FastAPI service can do 80% of what LiteLLM does, with exactly the policies you want and no compatibility tax. Defensible if you have one team writing this and three teams using it; indefensible if every team builds their own.

Policy-based routing

The interesting policies are not "always use GPT-4." They are:

# Cheap-first with confidence escalation
def route(prompt, user_tier):
    response = call("groq/llama-3.1-70b", prompt)
    if response.logprob_confidence < 0.7 or user_tier == "pro":
        response = call("anthropic/claude-sonnet-4-5", prompt)
    return response

# Fallback on rate limit / provider error
def call_with_fallback(prompt):
    chain = [
        "anthropic/claude-sonnet-4-5",      # primary
        "bedrock/anthropic.claude-sonnet",  # same model, different cloud
        "openai/gpt-4.1",                   # different vendor, similar quality
    ]
    for model in chain:
        try:
            return call(model, prompt, timeout=30)
        except (RateLimitError, ProviderError, TimeoutError):
            continue
    raise AllProvidersFailed()

# Tenant-aware routing
def route_for_tenant(prompt, tenant):
    if tenant.requires_data_residency == "EU":
        return call("bedrock-eu-west/claude-sonnet", prompt)
    if tenant.compliance_tier == "HIPAA":
        return call("vertex-private/gemini-pro", prompt)
    return call("anthropic/claude-sonnet-4-5", prompt)

LiteLLM expresses these as router config; Portkey expresses them as policies in the UI. Either way, the policy is declarative, per-request observable, and changeable without a deploy.

Per-tenant quotas

Once multiple tenants share an upstream key, you need quotas at the gateway, not in the app. Otherwise one tenant's runaway loop consumes the rate limit and every other tenant gets 429s. Standard pattern:

Limit type Granularity Window Enforcement
Requests per minute Per tenant key 60 s sliding Soft - return 429 with Retry-After
Input + output tokens per day Per tenant 24 h UTC reset Hard - block until reset
Spend per month Per tenant Calendar month Soft - alert at 80%, hard at 100%
Concurrent in-flight requests Per tenant n/a Hard - prevents one tenant from saturating workers

LiteLLM ships all of these out of the box. Roll-your-own usually starts with Redis counters and a Lua script for atomic increment-and-check.

The observability story

The minimum useful telemetry per request:

  • request_id, tenant_id, user_id, feature (your app dimension)
  • model_requested, model_actually_used (after fallback)
  • prompt_tokens, cached_tokens, completion_tokens
  • cost_usd (computed from your price table, not the provider's bill)
  • ttft_ms, total_latency_ms
  • finish_reason, error_class (if any)
  • prompt_hash and response_hash (for dedup, never the raw text in metrics)

Then ship this to whichever observability stack you use - LiteLLM and Portkey both export to Langfuse, Datadog, Prometheus, OpenTelemetry. Raw prompts and responses go to a separate audit log (S3 with object-lock, typically), not into your metrics pipeline.

When it falls down

  • Gateway becomes a SPOF. You centralised all LLM traffic through one service. Run it in HA, keep the config out of the request path, and have a documented "bypass the gateway" runbook for outages.
  • Latency tax. A gateway adds one network hop and some bookkeeping. Budget 5-15 ms for in-region, 50+ ms cross-region. Acceptable for chat; sometimes not for tight agent loops.
  • Fallback hides regressions. If GPT-4 silently falls back to Claude on every error, your eval set is no longer evaluating GPT-4. Alert on fallback rate, do not just hide it.
  • OpenRouter's data policy. Routing through a third-party gateway means a third party has your prompts. Read the data-retention terms and decide whether that fits your compliance posture.

Further reading

Check yourself

6 flashcards for this concept

Click a card to reveal the answer.

Drill the whole track