tokencnnI Built a Model Router That Picks the Right LLM for Every Call — Here's the Python Two...
Two months ago my codebase had exactly one LLM client, hardcoded to one model. First I ran everything on the cheapest model, because the bill looked great. Then a user showed me a reply that was confidently wrong, and I did what everyone does: I switched everything to GPT-4o. The bill went up 28x and the quality on the easy 90% of traffic didn't improve. The mistake wasn't picking the wrong model — it was picking one model.
The fix turned out to be a router: a few hundred lines of Python that classify each request, send it to the cheapest model that's good enough, validate the output, and fall back to a stronger model when validation fails. This is the step-by-step version, with the real cost numbers from my traffic.
Every request to an LLM API is not the same task. In my pipeline they split roughly like this:
If you send all four to the cheap model, the 5% hard ones come back mediocre and you ship bad output. If you send all four to the premium model, you pay $10.00 per 1M input tokens to classify a ticket. Routing is the middle path: a small classifier decides the task, a lookup table decides the model.
Prices per 1M tokens, as of this writing:
| Model | Input $/1M | Output $/1M |
|---|---|---|
| DeepSeek V4 Flash | $0.35 | $1.10 |
| Qwen3-235B-A22B | $1.60 | $6.40 |
| GLM-5-130B | $1.20 | $4.80 |
| GPT-4o | $10.00 | $30.00 |
My routing table came from running the same prompts across all four models (the evals are a story for another post). What I settled on:
| Task | Model | Why |
|---|---|---|
| classify / extract | deepseek-v4-flash |
Fastest TTFT (0.7s p50), cheap enough to fire on every request |
| summarize | glm-5-130b |
Solid all-rounder, good long-form coherence |
| translate | qwen3-235b-a22b |
Best Chinese↔English output in the roster |
| reason | gpt-4o |
The safety net — only the hard 5% goes here |
Nothing clever here — a dict of routes and a function that calls the OpenAI-compatible SDK. All four models sit behind one endpoint, so switching models is just a string:
import openai
client = openai.OpenAI(
api_key="sk-...",
base_url="https://api.tokencnn.com/v1", # one endpoint, every model
)
ROUTES = {
"classify": {"model": "deepseek-v4-flash", "max_tokens": 150},
"extract": {"model": "deepseek-v4-flash", "max_tokens": 300},
"summarize": {"model": "glm-5-130b", "max_tokens": 800},
"translate": {"model": "qwen3-235b-a22b", "max_tokens": 500},
"reason": {"model": "gpt-4o", "max_tokens": 1500},
}
def call_model(model, messages, max_tokens):
resp = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
)
return resp.choices[0].message.content
def route(task, messages):
spec = ROUTES[task]
return call_model(spec["model"], messages, spec["max_tokens"])
The classifier is just another cheap call with temperature=0 and a tight label vocabulary. The important bit: fall through to the expensive route when unsure — a misclassified hard request sent to a weak model is worse than an easy request sent to GPT-4o:
CLASSIFIER_LABELS = "classify, extract, summarize, translate, reason"
def pick_task(text):
resp = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content":
f"Label this request with exactly one of: {CLASSIFIER_LABELS}.\n\n{text}"}],
max_tokens=10,
temperature=0,
)
label = resp.choices[0].message.content.strip().lower()
return label if label in ROUTES else "reason" # fail safe, not fast
This is where routing earns its keep. Cheap models fail differently, not less often: GLM-5-130B occasionally pauses mid-stream, DeepSeek V4 Flash sometimes returns malformed JSON on gnarly schemas, and both can hallucinate a tool call that doesn't fit the schema. So I never trust the first pass — I validate, and retry with the next tier up on failure:
import json
def route_with_fallback(task, messages, validate):
spec = ROUTES[task]
for model in [spec["model"], "gpt-4o"]: # escalate to premium
text = call_model(model, messages, spec["max_tokens"])
try:
validate(text) # schema check, exact-match, hidden test
return model, text
except (json.JSONDecodeError, ValueError):
continue
raise RuntimeError(f"validation failed on both models for task={task}")
The fallback fires on the 2–4% of requests that fail validation, so the GPT-4o calls stay rare — but the option is what makes the cheap model usable on the long tail. Retrying a sub-millicent call beats paying $30.00 per 1M output tokens on everything.
Here are the real numbers from 10,000 requests/day, ~1,680 input and ~190 output tokens per request on average. First, what you'd pay if every request went to a single model:
STEPS = 10_000 # requests per day
IN_PER_STEP = 1_680 # avg input tokens per request
OUT_PER_STEP = 190 # avg output tokens per request
FINAL_OUT = 0
def daily_cost(p_in, p_out):
total_in = STEPS * IN_PER_STEP
total_out = STEPS * OUT_PER_STEP + FINAL_OUT
return (total_in * p_in + total_out * p_out) / 1_000_000
for name, p_in, p_out in [
("DeepSeek V4 Flash", 0.35, 1.10),
("Qwen3-235B-A22B", 1.60, 6.40),
("GLM-5-130B", 1.20, 4.80),
("GPT-4o", 10.00, 30.00),
]:
print(f"{name:18} ${daily_cost(p_in, p_out):.2f} / day")
DeepSeek V4 Flash $7.97 / day
Qwen3-235B-A22B $39.04 / day
GLM-5-130B $29.28 / day
GPT-4o $225.00 / day
Now the same traffic through the router — same token volume, only the paying model changes, so every number below is reproducible from the constants above:
| Task | Share | Requests | Model | Daily cost |
|---|---|---|---|---|
| classify / extract | 70% | 7,000 | deepseek-v4-flash |
$5.58 |
| summarize | 15% | 1,500 | glm-5-130b |
$4.39 |
| translate | 10% | 1,000 | qwen3-235b-a22b |
$3.90 |
| reason (hard escalations) | 5% | 500 | gpt-4o |
$11.25 |
| Total routed | 10,000 | $25.13 |
So the routed bill is 89% cheaper than running everything on GPT-4o ($225.00 → $25.13), while keeping GPT-4o quality on the 5% that needs it. Two honest flipsides: it's 3.2x the all-DeepSeek floor — routing isn't about being cheapest, it's the cheapest bill that meets your quality bar — and the 5% routed to GPT-4o is 45% of the total bill. That's the premium you're actually paying for, and it's exactly where you want it.
ROUTES dict is config, not a law — I re-check the mix monthly, and an eval harness catches regressions when a provider swaps a model underneath.The annoying part wasn't the code — DeepSeek, Qwen, and GLM each have their own console, billing, and rate limits, so a router meant maintaining four accounts. I route everything through tokencnn.com: a single OpenAI-compatible endpoint where deepseek-v4-flash, qwen3-235b-a22b, and glm-5-130b sit behind one API key, so the router above works unchanged and switching a route is a one-line edit. Sign up with just an email — no China phone number, no WeChat — and the $1 free credit covers roughly a week of routed traffic:
curl https://api.tokencnn.com/v1/chat/completions \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [
{"role": "system", "content": "Classify this ticket."},
{"role": "user", "content": "Label this request: ..."}
]
}'
The best model isn't one model — it's a policy. Classify each request, route to the cheapest model that's good enough, validate the output, and escalate only when validation fails. On my traffic that's an 89% cut vs all-premium without giving up GPT-4o where it matters, at the cost of ~0.7s of classifier latency and one honest constraint: you have to be able to validate output, or the fallback is just theater.
What does your routing table look like? I'd genuinely like to steal your task→model mapping — especially the task where you learned the hard way that the cheap model wasn't good enough.