How to Build an AI Model Router That Cuts Your API Costs by 80%+
Every AI application has the same dirty secret: most of the tokens you pay for never needed an expensive model.
Classifying a support ticket does not require Claude Sonnet 5. Extracting a price from an invoice does not need GPT-5.6. Yet teams routinely route 100% of traffic to one premium model, then panic when the invoice arrives — especially now that providers like DeepSeek are raising prices up to 14x and Anthropic's Fable 5 costs $10/M input at retail.
The fix is a model router: a thin layer that sends each request to the cheapest model that can handle it. In this tutorial, you'll build one in ~100 lines of JavaScript, using any OpenAI-compatible API. With the right routing rules, teams routinely cut spend by 80–95% with no visible quality loss.
The Core Idea: Not All Tokens Are Equal
A typical AI product has a workload distribution like this:
- ~70% trivial calls — classification, extraction, formatting, routing, "is this spam?"
- ~25% medium calls — summarization, RAG answers, drafts, translations
- ~5% hard calls — complex reasoning, multi-step agentic work, gnarly code
If you send everything to a frontier model, you're paying frontier prices for the 70% that a small model handles perfectly. Router logic maps each tier to a model priced for it:
| Tier | Example models (Qubax pricing) | Input $/M | Output $/M | Best for |
|---|---|---|---|---|
| Cheap | GLM 5.2 | $0.0075 | $0.0473 | Classification, extraction, formatting |
| Medium | GPT-5.6 Luna | $0.045 | $0.27 | Summaries, RAG, drafts |
| Powerful | DeepSeek V4 Pro | $0.0587 | $0.1173 | Agents, long-context code work |
| Frontier | Claude Sonnet 5 | $0.75 | $3.75 | Hard reasoning, final polish |
Note the spread: Claude Sonnet 5 costs 100x more per input token than GLM 5.2 on Qubax. Getting even half your traffic off the frontier model is the single biggest cost lever you have.
What You'll Build
A Router class that:
- Classifies each request by complexity signals (task type, input length, explicit hints)
- Picks the cheapest capable model
- Calls the API with automatic escalation — if the cheap model's answer looks weak, retry with a stronger one
- Logs per-request cost so you can verify the savings
We'll use the Qubax API (OpenAI-compatible, 300+ models behind one key), but the pattern works with any provider.
Prerequisites
- Node.js 18+
- A Qubax API key
- 15 minutes
mkdir model-router && cd model-router && npm init -y
echo "QUBAX_API_KEY=your-key-here" > .envStep 1: The Model Tiers
Define your ladder declaratively, with live prices so the router can reason about cost:
// models.mjs
export const TIERS = [
{
name: "cheap",
model: "glm-5.2",
inPrice: 0.0075, outPrice: 0.0473, // $/M tokens
},
{
name: "medium",
model: "gpt-5.6-luna",
inPrice: 0.045, outPrice: 0.27,
},
{
name: "powerful",
model: "deepseek-v4-pro",
inPrice: 0.0587, outPrice: 0.1173,
},
{
name: "frontier",
model: "claude-sonnet-5",
inPrice: 0.75, outPrice: 3.75,
},
];
export const tier = (name) =>
TIERS.find((t) => t.name === name);Prices above are current Qubax rates — check qubax.ai/models for live numbers and update as they move. (In August 2026 alone we've seen DeepSeek hike first-party prices up to 14x; aggregator rates move slower, but always verify.)
Step 2: The Complexity Classifier
The router's brain doesn't need to be clever — explicit signals beat ML for v1. Combine three inputs:
// classify.mjs
const CHEAP_TASKS = [
"classify", "extract", "format", "label", "is_", "parse",
"summarize_short", "rewrite", "translate",
];
const HARD_SIGNALS = [
"reason", "plan", "analyze", "debug", "refactor", "architect",
"step", "multi", "agent", "compare", "evaluate", "prove",
];
export function classify({ task, input, hint }) {
// 1. Explicit hint wins — let the caller force a tier
if (hint) return hint;
const t = task.toLowerCase();
const inputTokens = Math.ceil((input || "").length / 4);
// 2. Long context needs a big-context model regardless of task
if (inputTokens > 60_000) return "powerful";
// 3. Task-type keywords
if (HARD_SIGNALS.some((k) => t.includes(k))) return "frontier";
if (CHEAP_TASKS.some((k) => t.includes(k))) {
return inputTokens > 4_000 ? "medium" : "cheap";
}
// 4. Default by size
return inputTokens > 2_000 ? "medium" : "cheap";
}Why length matters: small models degrade faster on long inputs. A 50k-token contract summarization should not run on the cheap tier even though "summarize" is a cheap task.
Step 3: The Router Core
Now the engine — call the model, estimate cost, and escalate on weak answers:
// router.mjs
import { tier } from "./models.mjs";
const API = "https://api.qubax.ai/v1/chat/completions";
export async function callModel(t, messages, maxTokens = 1024) {
const res = await fetch(API, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.QUBAX_API_KEY}`,
},
body: JSON.stringify({
model: t.model,
messages,
max_tokens: maxTokens,
temperature: 0.2,
}),
}).then((r) => r.json());
const usage = res.usage ?? {};
const cost =
(usage.prompt_tokens / 1e6) * t.inPrice +
(usage.completion_tokens / 1e6) * t.outPrice;
return { text: res.choices[0].message.content, usage, cost };
}
// Signals that a weak model failed and we should escalate
function looksWeak(text) {
if (!text || text.length < 20) return true;
if (/as an ai language model/i.test(text)) return true;
if (/(i cannot|i'm unable to|i don't have access)/i.test(text)
&& text.length < 200) return true;
return false;
}
export async function route({ task, input, hint, system }) {
const order = ["cheap", "medium", "powerful", "frontier"];
const start = hint
? order.indexOf(hint)
: order.indexOf(
(await import("./classify.mjs")).classify({ task, input, hint })
);
let spent = 0;
for (let i = start; i < order.length; i++) {
const t = tier(order[i]);
const messages = [
...(system ? [{ role: "system", content: system }] : []),
{ role: "user", content: input },
];
const { text, usage, cost } = await callModel(t, messages);
spent += cost;
const isLast = i === order.length - 1;
if (!looksWeak(text) || isLast) {
return { text, model: t.model, tier: t.name, spent, usage };
}
// else: escalate to the next tier and try again
}
}The looksWeak heuristic is deliberately conservative. It's not judging answer quality — just catching obvious failures (empty, evasive, or truncated responses) so you only pay for escalation when the cheap model genuinely choked.
Step 4: Use It
// index.mjs
import "dotenv/config";
import { route } from "./router.mjs";
// Trivial task → routed to the cheap tier
const spam = await route({
task: "classify",
input: "Ticket: 'my login button 404s on Safari 17'. Return: bug|billing|other",
});
console.log(spam.model, "$" + spam.spent.toFixed(6));
// → glm-5.2 $0.000012
// Hard task → goes straight to frontier
const plan = await route({
task: "plan multi-step refactor of our auth middleware to support SSO",
input: "Current architecture: ... (3,000 words)",
});
console.log(plan.model, "$" + plan.spent.toFixed(6));
// → claude-sonnet-5 $0.0041That first call cost about a sixth of a hundredth of a cent. The same call on Claude Sonnet 5 would cost ~30x more. Multiply by a million requests and you've saved a house.
Step 5: Measure the Savings
Add a tiny logging wrapper and let it run for a week:
const log = [];
export function logCall(r) {
log.push(r);
const total = log.reduce((s, r) => s + r.spent, 0);
const naive = log.reduce(
(s, r) =>
s +
(r.usage.prompt_tokens / 1e6) * 0.75 +
(r.usage.completion_tokens / 1e6) * 3.75, // all-frontier baseline
0
);
console.log(
`requests=${log.length} actual=$${total.toFixed(4)} ` +
`allFrontier=$${naive.toFixed(4)} saved=${(
(1 - total / naive) * 100
).toFixed(1)}%`
);
}Most real workloads land between 80% and 95% savings, with the frontier tier handling under 10% of calls.
Production Tips
- Cache classifications. If task+input-shape repeats, cache the tier decision — don't re-classify identical requests.
- Add a quality circuit breaker. If users downgrade/dislike results from a tier, bump that request type up a tier permanently.
- Use prompt caching for agents. Agent loops re-send the same context every step; on providers that support cache headers this cuts input cost dramatically. DeepSeek's new pricing (cache hits 6x) makes this painful to ignore.
- Set per-tier rate limits so a burst of traffic can't silently all-escalate to frontier.
- Log model+cost per feature. "Search costs $40/day, all of it on the cheap tier" is exactly the visibility finance keeps asking for.
FAQ
What is an AI model router?
A model router sits between your application and your LLM API, sending each request to the model that best balances cost and capability for that specific task — cheap models for simple tasks, frontier models only when needed.
How much can model routing save?
It depends on your traffic mix, but 80–95% savings are common when most requests are simple (classification, extraction, formatting) and only a small fraction need frontier reasoning.
Which models should I use for each tier?
A proven budget stack in 2026: GLM 5.2 ($0.0075/M in) for the cheap tier, GPT-5.6 Luna ($0.045/M in) for medium, DeepSeek V4 Pro ($0.0587/M in) for long-context and agentic work, Claude Sonnet 5 ($0.75/M in) for frontier reasoning. All available on Qubax.
Does model routing hurt quality?
Not for well-classified tasks: on simple tasks, small models match frontier output. The escalation mechanism protects you — if a cheap model's answer looks weak, the router retries on a stronger tier automatically.
Can I use this with providers other than Qubax?
Yes. The router talks to any OpenAI-compatible /v1/chat/completions endpoint. Qubax is convenient because all 300+ models share one key and one schema — so routing is a model-string change, not a new integration.
Is 100 lines of routing code production-ready?
The pattern is, but add retries with exponential backoff, timeouts, per-tier budgets, and observability before shipping. Or evaluate managed routers — Qubax normalizes provider quirks so your router only handles policy, not plumbing.
Ready to stop paying frontier prices for trivial tokens? Get a Qubax API key at [qubax.ai](https://qubax.ai) and route across 300+ models with one integration.