Model Selection Is a Budget Decision Wearing a Technical Costume
"Which model should we use" almost always gets answered as if it were purely a quality question. It isn't. Every model choice is a point on a cost-performance curve, and the frontier model that wins your internal eval by a few points can be several times more expensive per call than the option that would have shipped the feature just fine. Nobody notices at ten requests a day. Everybody notices at ten thousand.
- Your OpenAI or Anthropic bill tripled after a feature went from beta to general availability, and finance is asking why
- You're running the frontier model on a classification task that a much cheaper model would nail just as often
- Someone suggested "just use the cheap model for everything" and you're not sure where that quietly breaks
- You need to forecast next quarter's LLM spend for a feature that doesn't exist in production yet
This article covers the frontier-vs-mini trade-off, routing and cascading, batch pricing, and a worked cost estimate you can adapt.
Frontier vs Mini/Flash: What You're Actually Trading
Every major lab now ships two tiers. A frontier tier โ GPT-4o-class, Claude Fable 5 or Opus 5-class, Gemini Pro โ built for the hardest reasoning, the longest context, the most ambiguous instructions. And a smaller, faster tier โ GPT-4o-mini-class, Gemini Flash, Claude Haiku-class โ priced roughly an order of magnitude cheaper per token, with latency to match. The mini tier isn't a worse version of the frontier model in every way that matters. It's tuned for a different job: high-volume, lower-ambiguity tasks where the frontier model's extra reasoning capacity mostly goes unused. Classification, extraction, short rewrites, simple routing decisions โ a mini model handles these at a quality gap that's often smaller than the price gap suggests. Multi-step reasoning, long-document synthesis, anything where getting it subtly wrong is expensive to catch later โ that's where the frontier tier earns its price.
| Frontier tier | Mini / Flash tier | |
|---|---|---|
| Relative cost per token | Highest in the lineup | Roughly an order of magnitude lower |
| Latency | Slower, especially with reasoning enabled | Often sub-second time-to-first-token |
| Best for | Ambiguous, multi-step, high-stakes tasks | High-volume, well-defined, low-ambiguity tasks |
| Context window | Usually largest in the family | Often smaller, sometimes matched |
Model Routing and Cascading
Once you accept that no single model is right for every call in your system, routing follows naturally. Cascading is the specific pattern worth knowing: try the cheap model first, and only escalate to the expensive one when the cheap model's output looks unreliable. The trick is defining "looks unreliable" in a way that's cheap to check โ a confidence score if the model exposes one, a schema validation failure, a length or format check, or a cheap secondary classifier trained specifically to flag "this needs the bigger model." Get that check wrong and you either escalate everything (no savings) or escalate nothing (silent quality loss), so it's worth treating as a real component with its own tests, not an afterthought bolted onto the routing function.
flowchart TD
Req["Incoming request"] --> Cheap["Cheap/fast model
(mini or flash tier)"]
Cheap --> Conf{"Confident?
(schema valid, score high,
task in scope)"}
Conf -->|Yes| Return["Return result"]
Conf -->|No| Expensive["Escalate to frontier model"]
Expensive --> Return
style Cheap fill:#059669,color:#fff,stroke:#047857
style Expensive fill:#dc2626,color:#fff,stroke:#b91c1c
style Return fill:#4f46e5,color:#fff,stroke:#4338ca
async function classifyTicket(ticket: string): Promise<Classification> {
const cheap = await callModel('gpt-4o-mini', buildPrompt(ticket));
// Escalate on structural failure or low self-reported confidence โ
// not on vibes. Define this threshold from labeled data, not intuition.
const needsEscalation =
!isValidSchema(cheap.output) ||
cheap.confidence < 0.7 ||
ticket.length > 4000; // long/ambiguous tickets go straight to the bigger model
if (!needsEscalation) return cheap.output;
const frontier = await callModel('gpt-4o', buildPrompt(ticket));
return frontier.output;
}In practice, a well-tuned cascade routes the large majority of traffic to the cheap model and reserves the expensive one for the tail that actually needs it โ which is exactly the shape of most production workloads: mostly routine, occasionally hard.
Batch API Pricing
Most major providers offer an asynchronous batch endpoint: submit a large set of requests, get results back within a window (often hours rather than seconds), at a meaningful discount off synchronous pricing. This fits offline evaluation runs, bulk classification of a historical dataset, nightly enrichment jobs, anything where nobody is staring at a spinner waiting for the answer. It does not fit user-facing chat, or anything where a human is on the other end expecting a response inside a few seconds. The mistake is assuming batch pricing applies broadly when it actually applies to a narrower slice of your workload than the discount makes tempting โ check which parts of your pipeline can genuinely tolerate the latency before restructuring anything around it.
Prompt Caching as a Cost Lever
If your requests share a large, stable prefix โ a long system prompt, a set of few-shot examples, a document you're asking repeated questions about โ prompt caching (covered in more depth in the context-windows article) discounts the repeated portion on every call after the first. It stacks cleanly with routing and cascading: cache the shared prefix once, then route the variable part of the request through whichever tier makes sense.
Worked Example: Support-Ticket Triage
Say you're triaging 10,000 support tickets a day โ category, priority, routing โ averaging roughly 500 input tokens and 150 output tokens per ticket. These numbers are illustrative, not current pricing; plug in your provider's actual rate when you do this for real.
| Approach | Illustrative $/M tokens (in / out) | Daily tokens | Illustrative daily cost |
|---|---|---|---|
| Frontier model, every ticket | $5 / $15 (illustrative) | 5M in / 1.5M out | ~$47.50 |
| Mini model, every ticket | $0.30 / $1.20 (illustrative) | 5M in / 1.5M out | ~$3.30 |
| Cascade: mini first, ~12% escalate to frontier | blended | same totals | ~$4.90 |
The mini-only run is roughly fourteen times cheaper than frontier-only in this illustration, and the cascade lands close to it while keeping the escalation path for the tickets that are actually ambiguous โ angry customers, multi-issue tickets, anything where a misrouted ticket costs more in support time than the model call ever would. Multiply the daily numbers by 30 for a monthly estimate, and notice how much the answer depends on that escalation rate. It's worth measuring on real traffic before committing to a number in a planning doc.
1. Optimizing cost per token instead of cost per good result
A model that's 70% cheaper but needs a retry on 1 in 5 calls isn't actually cheaper once you count the retries and the downstream cost of the failures that don't get retried. Measure cost against outcomes that passed your quality bar, not raw token spend.
2. Routing on vibes instead of a measured threshold
"The cheap model seems fine for this" is not a routing policy. Build the escalation trigger from labeled examples where you know the right answer, and revisit the threshold when either model gets updated โ providers change model behavior under a fixed name more often than the release notes suggest.
3. Putting batch pricing on the critical path
Batch discounts are real, but the multi-hour turnaround makes it a poor fit for anything a user is waiting on. Keep batch scoped to genuinely asynchronous work, or you'll end up building a synchronous-feeling UI on top of an asynchronous backend and fighting that mismatch forever.
4. Forecasting cost off the sticker price, not the traffic shape
Per-token pricing tells you almost nothing until you multiply it by your actual token distribution, including the long tail of unusually large requests that skew the average. Model the P95 request, not just the median, before you commit a number to a budget.
Key Takeaways
- The mini/flash tier is not a worse frontier model โ it's tuned for a different job. Match the tier to task ambiguity, not to habit.
- Cascading routes the boring majority of traffic cheaply and reserves the expensive model for the tail that actually needs it โ but only if the escalation trigger is measured, not guessed.
- Batch pricing is a discount on patience, not a universal lever.It fits offline and bulk work; it doesn't fit anything with a human waiting.
- Cost estimates are only as good as the traffic distribution behind them. Model your real request mix, including the outliers, before you put a number in front of finance.

