← Back to list

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%

Manjunath Hanmantgad · 2026-05-18 06:45 · 0 claps · 10.0 min read
#model-routing #ai-production #agentic-ai #api-gateway
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents

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 messages with role/content. Anthropic separates system from messages. Google uses contents with parts.

  • Response format: Different field names for finish_reason, usage, content.

  • Error codes: OpenAI returns 429 for rate limits. Anthropic returns 529 for overload. Google returns 429 but 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:

  1. Embed the incoming query using text-embedding-3-small (~$0.00002 per query).

  2. Search Redis (with the redis-vss module) for cached responses where cosine similarity > 0.95.

  3. 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: true header 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:

  1. Log 1,000 requests with a rough task-type tag. Even a keyword heuristic gives you the distribution.

  2. 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.

  3. 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.

  4. Add semantic caching only after you have the routing baseline. The cache compounds savings but adds its own failure modes.

  5. 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