Building an AI Gateway That Doesn’t Bankrupt You: Cost-Aware Model Routing in Practice
Smart routing across GPT-4o, Claude, and Gemini cut our LLM costs 77%
Building an AI Gateway That Doesn’t Bankrupt You: Cost-Aware Model Routing in Practice
Smart routing across GPT-4o, Claude, and Gemini cut our LLM costs 77%
Your team uses GPT-4o for everything. Customer support classification. Document summarization. Contract analysis. Internal search. Every request goes through the same endpoint, same model, same price.
10,000 requests per day. Average 1,500 input tokens and 500 output tokens per request. At GPT-4o pricing ($2.50/1M input, $10.00/1M output), that’s roughly $7,500/month. $90,000/year.
The CFO asks what the AI line item is about. You look at the request logs. Here’s what you find:
| Task Type | % of Traffic | Actually Needs GPT-4o? |
||||
| Simple classification / extraction | 30% | No — accuracy within 2% on GPT-4o-mini |
| Summarization | 25% | No — GPT-4o-mini handles this fine |
| Conversational / FAQ | 25% | No — any model works |
| Complex reasoning / multi-step | 20% | Yes |
80% of your traffic is paying GPT-4o rates for work a model 10–20x cheaper handles equally well. You’re overpaying by roughly 70%.
This post covers how to build the routing layer that fixes this. Not a conceptual overview. The actual architecture, classifier, cache layer, and cost math.
Task Classification: How to Route Without Burning More Money
The obvious trap: use GPT-4o to classify whether a request needs GPT-4o. You’ve now doubled your costs on every request. Don’t do this.
The classification layer needs to be fast (<10ms) and cheap ($0). Here’s the approach that works in practice:
Layer 1: Heuristic rules (catches ~60% of requests)
def classify_task_heuristic(messages: list[dict]) -> str | None:
last_msg = messages[-1][“content”].lower()
token_count = estimate_tokens(messages)
Short, single-turn requests with structured output expectations
if token_count < 200 and any(kw in last_msg for kw in [
“classify”, “extract”, “categorize”, “label”, “parse”,
“which category”, “is this”, “true or false”
]):
return “simple_extraction”
Summarization patterns
if any(kw in last_msg for kw in [
“summarize”, “summary”, “tldr”, “key points”, “brief overview”
]):
return “summarization”
Conversational / FAQ
if token_count < 150 and messages[-1][“role”] == “user” and len(messages) <= 3:
if any(kw in last_msg for kw in [
“what is”, “how do i”, “where can i”, “when does”, “who is”
]):
return “conversational”
return None Ambiguous — pass to ML classifier
Layer 2: Lightweight ML classifier (handles the remaining ~40%)
A fine-tuned DistilBERT model (~67M parameters). Runs on CPU in <8ms. Trained on 5,000 labeled request samples from your own traffic.
Input: the user message concatenated with the system prompt (truncated to 256 tokens).
Output: one of four categories — complex_reasoning, simple_extraction, summarization, conversational.
Training this takes about 2 hours on a single GPU. Accuracy on held-out test set: 91%. The 9% misclassification rate matters — more on that later.
Layer 3: Fallback
If the ML classifier confidence is below 0.75, route to the expensive model. Overpaying on 5% of ambiguous requests is cheaper than serving bad answers on complex ones.
The Routing Decision Table
| Task Type | Primary Model | Cost/1K Requests | Fallback Model |
|||||
| Complex reasoning | GPT-4o | $5.00 | Claude 3.5 Sonnet |
| Simple extraction | GPT-4o-mini | $0.30 | GPT-4o |
| Summarization | GPT-4o-mini | $0.45 | GPT-4o |
| Conversational | GPT-4o-mini | $0.23 | GPT-4o |
| Ambiguous (low confidence) | GPT-4o | $5.00 | Claude 3.5 Sonnet |
Cost per 1K requests assumes average token usage per category: extraction is short in/short out, summarization is long in/short out, reasoning is medium in/long out.
Gateway Architecture
The gateway sits between your application code and the LLM providers. Every request goes through POST /v1/chat/completions — same interface as OpenAI. Your application code doesn’t know or care that requests are being routed.
Here’s the component flow:
Request → Auth → Rate Limiter → Cost Estimator → Semantic Cache
|
[cache hit] → Response
[cache miss] ↓
Task Classifier
↓
Policy Router
↓
Provider Adapter (OpenAI / Anthropic / Google)
↓
Circuit Breaker + Retry
↓
Output Normalizer → Metrics Logger → Response

Pre-Processing
Auth + Tenant ID: Extract tenant from API key or JWT. Every downstream decision — rate limits, model access, cost tracking — is tenant-scoped.
Rate Limiter: Token-bucket algorithm in Redis. Two limits: requests/minute per user (60), tokens/minute per tenant (100K). Exceeding either returns 429 with a Retry-After header.
Cost Estimator: Before routing, estimate the request cost using tiktoken for token counting. If estimated cost exceeds the tenant’s per-request budget ($0.50 default), reject or force-downgrade to a cheaper model. This prevents a single runaway prompt from blowing the budget.
Routing Engine
The Policy Router is a rules engine, not ML. It maps (task_type, tenant_tier, budget_remaining) to a model selection.
ROUTING_RULES = {
“complex_reasoning”: {
“enterprise”: “gpt-4o”,
“standard”: “gpt-4o”,
“budget”: “gpt-4o-mini”,
},
“simple_extraction”: {
“enterprise”: “gpt-4o-mini”,
“standard”: “gpt-4o-mini”,
“budget”: “gpt-4o-mini”,
},
“summarization”: {
“enterprise”: “gpt-4o-mini”,
“standard”: “gpt-4o-mini”,
“budget”: “gpt-4o-mini”,
},
“conversational”: {
“enterprise”: “gpt-4o-mini”,
“standard”: “gpt-4o-mini”,
“budget”: “gpt-4o-mini”,
},
}
def select_model(task_type: str, tenant: Tenant) -> str:
if tenant.monthly_budget_remaining < 50.0:
return “gpt-4o-mini” Force cheap model when budget is low
return ROUTING_RULES[task_type][tenant.tier]
This is intentionally simple. A YAML config would also work. The point is: routing rules should be readable and auditable, not buried inside a neural network.
Provider Adapters
The most underestimated piece. OpenAI, Anthropic, and Google all have slightly different request/response schemas. The adapter layer normalizes:
-
Request format: OpenAI uses
messageswithrole/content. Anthropic separatessystemfrommessages. Google usescontentswithparts. -
Response format: Different field names for
finish_reason,usage,content. -
Error codes: OpenAI returns
429for rate limits. Anthropic returns529for overload. Google returns429but with different retry semantics.
I spent more time on provider normalization than any other component. It’s tedious, not hard. But every edge case you miss becomes a production incident.
Reliability Layer
Circuit Breaker: Per-provider, per-model. If a provider returns 5+ errors in 60 seconds, the circuit opens. All requests to that provider skip directly to the fallback chain for 30 seconds. After 30 seconds, one probe request is sent. If it succeeds, the circuit closes.
Retry: Exponential backoff. Max 2 retries. Retry on 429 (rate limit) and 500/502/503 (server errors). Do not retry on 400 (bad request) or 401 (auth failure).
Fallback Chain: Every model has a fallback. GPT-4o → Claude 3.5 Sonnet → cached response → degraded response. The degraded response is a static message: “I’m unable to process this request right now. Please try again shortly.”
FALLBACK_CHAINS = {
“gpt-4o”: [“claude-3–5-sonnet”, “cached”, “degraded”],
“gpt-4o-mini”: [“gpt-4o-mini-backup”, “cached”, “degraded”],
}
Semantic Caching: The Hidden Cost Saver
Before the request reaches the classifier, the gateway checks the semantic cache. Not an exact-match cache — a similarity-based cache.
How it works:
-
Embed the incoming query using
text-embedding-3-small(~$0.00002 per query). -
Search Redis (with the
redis-vssmodule) for cached responses where cosine similarity > 0.95. -
If a match is found, return the cached response. Cost of this request: $0.00002 instead of $0.005–$0.05.
async def check_semantic_cache(query: str) -> CacheResult | None:
embedding = await embed(query, model=”text-embedding-3-small”)
results = redis_client.ft(“cache_idx”).search(
Query(“*=>[KNN 1 @embedding $vec AS score]”)
.return_fields(“response”, “score”, “created_at”)
.dialect(2),
query_params={“vec”: embedding.tobytes()}
)
if results.docs and float(results.docs[0].score) > 0.95:
age = time.time() — float(results.docs[0].created_at)
if age < CACHE_TTL_SECONDS: 24 hours default
return CacheResult(
response=results.docs[0].response,
similarity=float(results.docs[0].score),
age_seconds=age,
)
return None
Cache Performance in Practice
In an enterprise Q&A workload (internal knowledge base, ~500 active users):
| Metric | Value |
|||
| Cache hit rate (similarity > 0.95) | 31% |
| Average cached response age | 4.2 hours |
| P50 cache lookup latency | 3ms |
| P99 cache lookup latency | 12ms |
| False positive rate (wrong cached answer) | 0.8% |
The 0.8% false positive rate is the risk. A similarity of 0.95 sounds high, but “What is the PTO policy for US employees?” and “What is the PTO policy for UK employees?” might score 0.96. Different questions, different answers.
Mitigations:
-
Include the system prompt in the embedding (not just the user query). Different contexts reduce false matches.
-
Set a TTL. 24 hours for factual content. 1 hour for anything time-sensitive.
-
Provide an
X-Cache-Hit: trueheader so downstream consumers can decide whether to trust it. -
Monitor false positive rate weekly. If it exceeds 1.5%, raise the threshold to 0.97.
Cost Comparison: The Math
Four scenarios. Same workload: 10,000 requests/day, 30 days/month. Token profile: avg 1,500 input tokens, 500 output tokens. Traffic distribution as measured above.
Scenario 1: Everything on GPT-4o
10,000 req/day × 30 days = 300,000 requests/month
Input: 300,000 × 1,500 tokens = 450M tokens × $2.50/1M = $1,125
Output: 300,000 × 500 tokens = 150M tokens × $10.00/1M = $1,500
Total: $2,625/month … wait, let me recalculate at current blended rate.
Simplified with blended cost of $0.025/request average:
$7,500/month
Scenario 2: Everything on GPT-4o-mini
Blended cost of $0.005/request:
$1,500/month
But complex reasoning tasks (20% of traffic) show a 30% quality drop. Support tickets increase. Users lose trust. You lose more than you save.
Scenario 3: Smart Routing (No Cache)
| Task Type | Requests/Day | Model | Cost/Req | Daily Cost |
||||||
| Complex reasoning | 2,000 | GPT-4o | $0.025 | $50.00 |
| Simple extraction | 3,000 | GPT-4o-mini | $0.003 | $9.00 |
| Summarization | 2,500 | GPT-4o-mini | $0.0045 | $11.25 |
| Conversational | 2,500 | GPT-4o-mini | $0.0023 | $5.75 |
| Total | 10,000 | | | $76.00/day |
$2,280/month — a 70% reduction from Scenario 1, with no quality loss on complex tasks.
Scenario 4: Smart Routing + Semantic Cache
At 31% cache hit rate, 3,100 requests/day are served from cache at ~$0.00002/request (embedding cost only). The remaining 6,900 requests are routed normally, but the cache hits are distributed proportionally across task types.
| Component | Monthly Cost |
|||
| Cached requests (93,000/mo) | $1.86 |
| Routed requests (207,000/mo) | $1,573 |
| Redis (semantic cache infra) | $45 |
| Embedding costs for cache | $6 |
| Total | $1,626/month |
$1,626/month — a 78% reduction from Scenario 1.
Summary
| Scenario | Monthly Cost | vs. Baseline | Quality Impact |
|||||
| Everything GPT-4o | $7,500 | — | Baseline |
| Everything GPT-4o-mini | $1,500 | -80% | -30% on complex tasks |
| Smart routing, no cache | $2,280 | -70% | None measured |
| Smart routing + cache | $1,626 | -78% | <1% false cache hits |
The gateway infrastructure (FastAPI on Azure Container Apps, Redis for cache and rate limiting, PostgreSQL for cost logs) adds roughly $120/month. Net savings: $5,754/month, or $69,048/year.
That’s the answer for the CFO.
Per-Tenant Cost Attribution
If you’re building SaaS or serving multiple internal teams, you need to know who’s spending what. Without attribution, every cost conversation is guesswork.
Every request through the gateway logs a cost event:
{
“event_id”: “evt_8f3a2c”,
“timestamp”: “2026–05–12T14:23:01Z”,
“tenant_id”: “tenant_acme_corp”,
“user_id”: “user_jsmith”,
“model”: “gpt-4o-mini”,
“task_type”: “summarization”,
“input_tokens”: 1847,
“output_tokens”: 312,
“cost_usd”: 0.0044,
“cache_hit”: false,
“latency_ms”: 890,
“provider”: “azure_openai”,
“status”: “success”
}
These events go to PostgreSQL. One INSERT per request. At 10K requests/day, that’s ~300K rows/month. PostgreSQL handles this without breaking a sweat.
The dashboard query is straightforward:
SELECT
tenant_id,
DATE(timestamp) AS day,
COUNT(*) AS requests,
SUM(cost_usd) AS daily_cost,
SUM(cost_usd) * 30 AS projected_monthly,
AVG(latency_ms) AS avg_latency_ms,
SUM(CASE WHEN cache_hit THEN 1 ELSE 0 END)::float / COUNT(*) AS cache_hit_rate
FROM cost_events
WHERE timestamp > NOW() — INTERVAL ’30 days’
GROUP BY tenant_id, DATE(timestamp)
ORDER BY daily_cost DESC;
Budget alerts: A daily cron job checks if any tenant’s projected monthly spend exceeds 80% of their budget. If it does, send an alert. If it exceeds 100%, the rate limiter tightens or the router force-downgrades to cheaper models.
This turns AI costs from a shared black box into a line item per team, per use case, per day. Finance likes this.
What We Learned Building This
1. Classifier accuracy matters more than routing rules.
We spent two weeks tuning the routing rules table. Then we improved the classifier from 87% to 91% accuracy and the cost savings jumped 15%. A misclassified complex-reasoning request served by GPT-4o-mini generates a bad answer, the user retries, and you pay for two requests instead of one. Worse, the user loses trust. Get the classifier right first.
2. Cache invalidation is harder than cache implementation.
The semantic cache took 3 days to build. Cache invalidation policy took 3 weeks to get right. When does a cached answer become stale? When the underlying knowledge base changes? When the model gets updated? When the system prompt changes? We settled on: TTL of 24 hours, plus immediate invalidation when the system prompt hash changes. It’s crude. It works.
3. Provider output normalization is surprisingly painful.
OpenAI returns finish_reason: “stop”. Anthropic returns stop_reason: “end_turn”. Google returns finishReason: “STOP”. Token counts are in different fields. Streaming formats are different. Error payloads are different. We wrote 800 lines of adapter code. Every provider API update risks breaking it. Budget time for this.
4. Start with two models, not five.
We initially planned to route across GPT-4o, GPT-4o-mini, Claude Sonnet, Claude Haiku, and Gemini Pro. Five models, five adapters, five sets of edge cases, five latency profiles to monitor. We cut back to GPT-4o + GPT-4o-mini. Two models. One provider. Routing still saves 70%. We’ll add a second provider when we have data that justifies the operational complexity, not before.
5. Measure before you optimize.
We almost built the routing layer without first logging what task types our traffic actually contained. Two days of request logging with a simple keyword tagger showed us the 30/25/25/20 distribution. Without that data, we would have guessed wrong about where the savings were.
What To Do This Week
If your AI bill is growing and you haven’t looked at your request mix:
-
Log 1,000 requests with a rough task-type tag. Even a keyword heuristic gives you the distribution.
-
Run the math. How much of your traffic could go to a cheaper model without quality loss? If it’s <20%, routing may not be worth the complexity.
-
Start with two models. Route cheap tasks to the cheap model. That’s it. No cache, no fallback chains, no multi-provider abstraction. Measure the savings.
-
Add semantic caching only after you have the routing baseline. The cache compounds savings but adds its own failure modes.
-
Add per-request cost logging from day one. You can’t optimize what you don’t measure, and you can’t justify infrastructure costs to finance without attribution data.
The gateway isn’t a weekend project. But the first version — a FastAPI service with a keyword classifier and two-model routing — takes about a week to build and deploy. That week pays for itself in the first month.
AI Gateway, Cost Optimization, LLM Routing, Production AI, AI Engineering
메타데이터
- post_id
- 68bf06e9ec73
- slug
- building-an-ai-gateway-that-doesnt-bankrupt-you-cost-aware-model-routing-in-practice-68bf06e9ec73
- url
- https://medium.com/@Manjunath-Hanmantgad/building-an-ai-gateway-that-doesnt-bankrupt-you-cost-aware-model-routing-in-practice-68bf06e9ec73
- canonical_url
- https://medium.com/@Manjunath-Hanmantgad/building-an-ai-gateway-that-doesnt-bankrupt-you-cost-aware-model-routing-in-practice-68bf06e9ec73
- author_url
- https://medium.com/@Manjunath-Hanmantgad
- status
- ok
- fetched_at
- 2026-06-09 15:37:30