By Sagar Shankaran, Founder of CallSphere
Design patterns for building a production LLM API gateway — including intelligent rate limiting, semantic caching, provider fallbacks, and request routing for multi-model deployments.
Key takeaways
Standard API gateways handle authentication, rate limiting, and routing for traditional APIs. LLM APIs have additional requirements that standard gateways do not address:
An LLM API gateway sits between your application and LLM providers, handling these concerns in a single layer.
Standard rate limiters count requests. LLM rate limiters need to count tokens, because a single request with a 100K context window costs 100x more than a simple query.
flowchart LR
CLIENT(["Client SDK"])
GW["API Gateway<br/>auth plus rate limit"]
APP["FastAPI app<br/>handlers and DI"]
VAL["Pydantic validation"]
SVC["Service layer<br/>business logic"]
DB[(Database)]
QUEUE[(Background queue)]
OBS[(Tracing)]
CLIENT --> GW --> APP --> VAL --> SVC
SVC --> DB
SVC --> QUEUE
SVC --> OBS
SVC --> CLIENT
style GW fill:#4f46e5,stroke:#4338ca,color:#fff
style APP fill:#f59e0b,stroke:#d97706,color:#1f2937
style DB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
class TokenAwareRateLimiter:
def __init__(self, redis: Redis):
self.redis = redis
async def check_and_consume(
self, tenant_id: str, estimated_tokens: int
) -> bool:
key = f"ratelimit:{tenant_id}:{self.current_window()}"
current = await self.redis.get(key)
if current and int(current) + estimated_tokens > self.get_limit(tenant_id):
return False # Rate limited
pipe = self.redis.pipeline()
pipe.incrby(key, estimated_tokens)
pipe.expire(key, 60) # 1-minute window
await pipe.execute()
return True
def get_limit(self, tenant_id: str) -> int:
# Per-tenant token limits
return self.tenant_limits.get(tenant_id, 100_000) # Default 100K/min
Beyond rate limiting, implement cost budgets that track spending per tenant, team, or project. Alert when spending approaches the budget and hard-stop when it is exceeded.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Cache responses for semantically similar queries to reduce costs and latency.
class SemanticCacheLayer:
def __init__(self, vector_store, ttl_seconds: int = 3600):
self.vector_store = vector_store
self.ttl = ttl_seconds
async def get(self, messages: list[dict], model: str) -> CacheResult | None:
# Create cache key from the last user message + model
cache_query = self.extract_cache_key(messages)
embedding = await self.embed(cache_query)
results = await self.vector_store.search(
embedding, threshold=0.97, filter={"model": model}
)
if results and not self.is_expired(results[0]):
return CacheResult(
response=results[0].metadata["response"],
cache_hit=True
)
return None
async def set(self, messages: list[dict], model: str, response: str):
cache_query = self.extract_cache_key(messages)
embedding = await self.embed(cache_query)
await self.vector_store.insert(
embedding,
metadata={"response": response, "model": model, "timestamp": time.time()}
)
Important: Only cache deterministic, factual queries. Do not cache creative tasks, personalized responses, or time-sensitive queries.
When your primary LLM provider experiences outages or rate limits, automatically fall back to alternatives.
class LLMProviderRouter:
def __init__(self):
self.providers = [
ProviderConfig("anthropic", "claude-sonnet-4", priority=1, weight=0.6),
ProviderConfig("openai", "gpt-4o", priority=1, weight=0.4),
ProviderConfig("anthropic", "claude-haiku-4", priority=2, weight=1.0), # Fallback
]
self.circuit_breakers = {p.name: CircuitBreaker() for p in self.providers}
async def route(self, request: LLMRequest) -> LLMResponse:
# Group by priority, try highest priority first
for priority_group in self.group_by_priority():
available = [
p for p in priority_group
if self.circuit_breakers[p.name].is_closed()
]
if not available:
continue
# Weighted random selection within priority group
provider = self.weighted_select(available)
try:
response = await provider.complete(request)
self.circuit_breakers[provider.name].record_success()
return response
except (RateLimitError, TimeoutError, ServerError) as e:
self.circuit_breakers[provider.name].record_failure()
continue
raise AllProvidersUnavailable()
Normalize requests and responses across providers so your application code does not need provider-specific logic.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
The gateway translates between a unified internal format and each provider's API format:
messages array vs. Anthropic's format)Every request through the gateway should be logged with:
{
"trace_id": "abc-123",
"tenant_id": "tenant-456",
"model_requested": "claude-sonnet-4",
"provider_used": "anthropic",
"input_tokens": 1523,
"output_tokens": 487,
"cost_usd": 0.0061,
"latency_ms": 2340,
"ttft_ms": 890,
"cache_hit": false,
"fallback_used": false
}
Before building your own gateway, evaluate existing options:
For most teams, starting with LiteLLM and adding custom middleware for your specific needs is the fastest path to production.
Sources:

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Rate limits decide UX and reliability for LLM-backed APIs. The 2026 patterns for shaping bursts, queueing, and fair allocation.
Multi-layer cache designs for AI apps — prompt cache, response cache, retrieval cache, embedding cache — and how they compose in 2026.
Three caching layers cut agent cost dramatically in stacked combination. The architecture for stacking them and the gotchas with each one in serious production deployments.
Most voice-agent tool calls hit the same hot data: caller account, upcoming appointments, recent invoices. Pre-fetch on call connect so the LLM never waits. ToolCacheAgent and Asteria show 1.8-3.2x speedups.
An autonomous agent can chain 20 calls from one prompt. Request-per-minute caps cannot stop a thousand-token prompt. Here is token-based rate limiting in 2026.
30-50% of agent utterances are static — greetings, confirmations, holds. Pre-render once, cache at CloudFront edge, and skip TTS on those turns. Save 100-300ms and slash TTS spend.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco