Route easy → cheap, hard → premium.
Two patterns: classifier-first picks a model up front, confidence-based escalates only when needed. Both cut spend by 80%+ vs naive routing to a single premium model.
Why bother
Most requests are easy
Production traffic distributions are wildly skewed. Across most chatbot + support traffic, ~60% of requests are factual lookup or simple formatting — a budget-tier model handles them perfectly. Routing those to claude-sonnet-4-6 is paying ~15× more than necessary.
The trick: a tiny model to decide which model to use. Or: try the cheap one first and escalate on low confidence.
Pattern 1 — Classifier first
Pick the model up front
A 7B model classifies the question difficulty. The classifier adds ~$0.000_05 per request — negligible compared to a sonnet call at $0.012.
import os
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.anoman.io/v1", api_key=os.environ["ANOMAN_API_KEY"])
# Tiny, cheap classifier. ~$0.000_05 per call.
CLASSIFIER_MODEL = "qwen-2.5-7b"
# Heavy model for hard requests.
HEAVY_MODEL = "claude-sonnet-4-6"
# Cheap model for easy requests.
LIGHT_MODEL = "gpt-4o-mini"
CLASSIFIER_SYSTEM = """
Classify the difficulty of the user's question as one of:
easy — Factual lookup, simple instruction, short formatting task
medium — Multi-step reasoning, document summarization, code generation
hard — Algorithm design, math proof, multi-turn planning, long context
Respond with ONLY the single word. No explanation.
""".strip()
def classify(question: str) -> str:
response = client.chat.completions.create(
model=CLASSIFIER_MODEL,
messages=[
{"role": "system", "content": CLASSIFIER_SYSTEM},
{"role": "user", "content": question},
],
max_tokens=10,
temperature=0,
)
return response.choices[0].message.content.strip().lower()
def ask(question: str) -> str:
difficulty = classify(question)
model = {
"easy": LIGHT_MODEL,
"medium": LIGHT_MODEL,
"hard": HEAVY_MODEL,
}.get(difficulty, HEAVY_MODEL) # safe default: escalate
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": question}],
# Track which path took for monitoring
metadata={"routed_via": "classifier", "difficulty": difficulty},
)
return response.choices[0].message.content
print(ask("Capital of Indonesia?")) # easy → cheap
print(ask("Prove that the halting problem is undecidable.")) # hard → premiumPattern 2 — Confidence escalation
Try cheap, escalate on bail
Instructs the cheap model to respond with ESCALATE when uncertain. The cheap model gets a chance to handle every request; we only pay premium when it explicitly bails. ~5% escalation rate is typical for well-prompted setups.
# Try the cheap model first. Only escalate when the cheap model says
# "I'm not sure" or doesn't follow the output format.
CHEAP = "gpt-4o-mini"
HEAVY = "claude-sonnet-4-6"
ASK_SYSTEM = """
Answer the user's question. If you're not confident, respond with
exactly: ESCALATE
Otherwise, give the answer.
""".strip()
def ask_with_escalation(question: str) -> tuple[str, str]:
"""Returns (answer, model_used)."""
response = client.chat.completions.create(
model=CHEAP,
messages=[
{"role": "system", "content": ASK_SYSTEM},
{"role": "user", "content": question},
],
max_tokens=300,
temperature=0,
)
answer = response.choices[0].message.content.strip()
if answer.startswith("ESCALATE") or answer == "":
# Cheap model bailed. Escalate.
response = client.chat.completions.create(
model=HEAVY,
messages=[{"role": "user", "content": question}],
)
return response.choices[0].message.content, HEAVY
return answer, CHEAPPattern 3 — Hybrid
Classifier + escalation
Use the classifier as a fast gate to route obvious hard requests directly to premium (no double-call overhead). For everything else, the cheap-with-escalation path applies. Best of both.
# Combine both: the classifier picks a candidate model, the candidate
# can still escalate on low confidence. Catches mis-classifications.
def smart_ask(question: str) -> dict:
difficulty = classify(question)
if difficulty == "hard":
# Skip cheap layer entirely.
return {"answer": ask_heavy(question), "model": HEAVY, "path": "direct"}
answer, model_used = ask_with_escalation(question)
return {
"answer": answer,
"model": model_used,
"path": "escalated" if model_used == HEAVY else "cheap",
}Economics
Back-of-envelope
Assume 10,000 requests/day:
- 60% easy (factual)
- 30% medium (summarization, simple code)
- 10% hard (multi-step reasoning)
Naive routing — everything to claude-sonnet-4-6:
- Avg cost: ~$0.012 per request (~500 in + ~300 out tokens)
- Daily: $120
- Monthly: ~$3,600
Classifier-first routing:
- Classifier: 10K × $0.00005 = $0.50
- Easy → cheap (6K × $0.0005) = $3
- Med → cheap (3K × $0.0008) = $2.40
- Hard → heavy (1K × $0.012) = $12
- Daily: $17.90
- Monthly: ~$540
- Savings: 85% vs naive
Confidence escalation — assume 5% escalation rate from cheap:
- Cheap call: 10K × $0.0008 = $8
- Heavy retry: 500 × $0.012 = $6
- Daily: $14
- Monthly: ~$420
- Savings: 88% vs naive
Production tips
- Track metadata — pass
metadata: {difficulty, path}on every call so the dashboard Usage page shows the routing distribution. Helps you tune the classifier prompt. - Monitor escalation rate — if it’s >15%, your classifier is mis-routing too often. Tune the prompt or move to the hybrid pattern.
- Latency-conscious paths — the classifier adds ~200 ms. For latency-sensitive UX, prefer confidence escalation (single call cheap path, escalate only when needed).
- Cache the classifier — same question gets same classification. Add
x-anoman-cache: semanticto the classifier call so repeated questions skip even the cheap classifier hop. - A/B test the threshold — split 5% of traffic to "always use premium" as a quality control. Compare end-user thumbs-up rate vs the routed cohort.
Track your routing distribution in the dashboard.
By-model breakdown with cost per route.