●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Intelligent Model Routing with Claude API — Auto-Selecting Sonnet 4.6 and Haiku 4.5 for Optimal Cost and Quality
Build an intelligent routing layer that automatically selects between Claude Sonnet 4.6 and Haiku 4.5 based on request complexity. Covers classifier design, circuit breakers, fallback chains, and cost monitoring for production deployments.
I opened the billing dashboard one morning and saw a number 1.8x higher than the previous month. Traffic was up ten percent. The only thing that had really grown was the share of requests landing on Sonnet 4.6.
The cause was easy to find. Out of a vague fear that quality would slip, I had pinned everything to the larger model — short classifications, boilerplate summaries, all of it.
So I overcorrected and pushed everything onto Haiku 4.5. Now the small number of requests that genuinely needed reasoning came back weaker, and they failed the pre-publish quality checks more often. Add the reruns back in and the savings evaporated.
You can't solve this dilemma by picking one model. The only way out is to classify request complexity upfront and route each call to the cheapest model that still produces acceptable quality — what I'll call intelligent model routing throughout this article.
What follows is the pattern I've been running for over six months across two very different workloads I maintain as an indie developer: a content operations backend, and a customer-support bot for iOS and Android apps. Classifier design, circuit breakers, and — the part almost nobody writes about — how to derive your thresholds from data instead of guessing at them.
The Cost-Quality Tradeoff You're Ignoring
Let's start with the numbers, because they motivate everything that follows.
As of Q1 2026, Claude Sonnet 4.6 costs roughly 3x as much as Haiku 4.5 per 1M input tokens and 15x as much per 1M output tokens. In a system serving diverse workloads, that difference compounds fast.
A content moderation task—checking if a user comment violates policy—doesn't need Sonnet's extended reasoning. Haiku handles it reliably and responds in 100ms vs. 300ms for Sonnet. You're paying 15x the cost for 3x slower latency.
A creative copywriting task for email marketing? That does need Sonnet. The output quality difference is material. Users notice.
The problem isn't that Sonnet is expensive. It's that you're routing both tasks to the same model. In a mature system with thousands of daily requests, that wastes millions of tokens annually.
Intelligent routing flips the equation. Instead of "use the best model for everything," you ask: "What's the minimal model that solves this specific task reliably?" The answers vary wildly by use case.
How to Classify Request Complexity
Before you can route, you need to know what you're routing. That means classifying incoming requests as either "simple" (Haiku-suitable) or "complex" (Sonnet-necessary).
The classification problem is harder than it sounds. A request that looks simple (three words) might need deep reasoning if it contains jargon or philosophical nuance. A long request might be simple—just verbose.
I've found that a three-layer classifier works well in practice:
Layer 1: Keyword-based routing — The fastest. Certain task types almost always route to one model. "Summarize this" or "extract the key points" rarely needs Sonnet. "Brainstorm campaign ideas" or "debug this code" usually does. A simple regex or string-matching layer catches maybe 40-50% of requests with near-zero cost.
Layer 2: Token count and structural analysis — For requests that don't match known patterns, look at token count (longer ≠ harder, but extreme length correlates with complexity) and structural signals like nested queries or conditional logic. This catches another 30-40%.
Layer 3: Embedding-based classification — For the remaining 10-20% of ambiguous requests, compute an embedding and compare to examples of "simple" and "complex" tasks from your production logs. This is accurate but slower and costs tokens, so use it sparingly.
Most teams I've worked with find that layers 1 and 2 alone handle 80-90% of production traffic correctly. Layer 3 is a nice-to-have for high-value requests where a mismatch is costly.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A two-tier routing pattern that pins the classifier itself to Haiku, cutting roughly $1,500/month in classification overhead to a quarter.
✦Real production numbers: 35→50% Sonnet ratio alerting, dedicated half-open health checks, prompt-cache hit rate recovering from 12% → 64%.
✦A shadow-evaluation harness for deriving routing thresholds from data, plus eight operational gotchas not in Anthropic's docs.
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Notice the structure: layer 1 catches obvious cases fast. Layer 2 handles the ambiguous middle. Layer 3 is optional—use it only when the cost of a mismatch (sending a simple request to Sonnet) outweighs the embedding cost.
Resilient Routing with Circuit Breakers and Fallback Chains
A router that always picks the "right" model is useless if that model is unavailable. Production systems need fallback strategies.
I recommend a circuit breaker pattern: monitor success rates for each model, and if one starts failing, automatically route traffic to the other. This protects against partial outages and allows graceful degradation.
Model used: claude-haiku-4.5
Response: 4
Cost: $0.000012
This pattern gives you automatic failover without manual intervention. If Sonnet starts timing out or returning errors, the circuit breaker trips and Haiku handles the load. When Sonnet recovers, the half-open state allows gradual re-entry.
Monitoring Costs in Real Time
Routing saves money only if you know where the savings are. Real-time cost monitoring is essential.
I recommend logging every API call with: (1) selected model, (2) estimated vs. actual cost, (3) latency, (4) whether classification was correct (measured by manual spot-checks or user feedback), and (5) error rates by model.
A 3-5% misclassification rate is typical and acceptable—the cost savings from routing simple tasks to Haiku outweigh the occasional wasted Sonnet call. If your rate exceeds 10%, your classifier needs tuning.
Common Mistakes That Will Burn Your Budget
I've seen these patterns repeatedly in production systems. Avoid them.
Mistake 1: Over-relying on keyword matching
❌ Before: You keyword-match on "summarize" and route to Haiku every time.
User: "Summarize this research paper on quantum computing and explain the
philosophical implications of wave-particle duality."
This needs Sonnet's reasoning, but keyword matching sends it to Haiku. Result: generic summary, user unhappy, you blame the router.
✅ After: Add a second signal. If the request contains jargon (detected via embedding or a domain-specific dictionary), bump complexity to "complex" even if keywords suggest "simple."
❌ Before: You build fallback logic and route Sonnet→Haiku on failure. But Sonnet request costs tokens even when it fails. You're paying for two model calls.
✅ After: Use circuit breakers before attempting the call, not after. This prevents wasted API calls in the first place.
// Wrong: Call, check status, then retryconst try1 = await callModel(sonnetModel, input); // Costs moneyif (try1.error) { const try2 = await callModel(haikuModel, input); // Costs more money}// Right: Check breaker, choose model, call onceconst selectedModel = router.selectModel();const result = await callModel(selectedModel, input); // One call total
Mistake 3: Treating all output tokens equally
❌ Before: You route a request to Haiku for classification, expecting a one-word response. But the user includes max_tokens: 4096, so if Haiku decides to generate a 3,000-word essay, you pay for all of it at Haiku rates (which are cheap per token but still add up).
✅ After: Set max_tokens dynamically based on task type. Summarization? 500 tokens. Code generation? 2000. Classification? 50.
That's 70% cost reduction. On an annual basis, that's $20,000+ saved.
Your actual savings depend on your workload distribution, but the order of magnitude is typical. Most teams see 40-70% improvements.
Monitoring and Observability
Beyond cost metrics, you need visibility into the health of your routing system.
I recommend logging these signals:
Classification accuracy — Spot-check 5-10% of calls manually. Did the router pick the right model? Log mismatches.
Model performance divergence — If Haiku starts returning lower-quality results for tasks it was handling well, alert. Something changed (model version, task distribution, user expectations).
Circuit breaker trips — Every time a breaker opens, log it. This is a leading indicator of outages or performance regressions.
Latency by model — Haiku should be consistently 30-50% faster than Sonnet for equivalent tasks. If that ratio changes, investigate.
Cost per task — Group calls by task type and compare actual vs. estimated cost. Use this to tune max_tokens limits.
// Observability helperclass RouterObservability { private observations: Map<string, number[]> = new Map(); recordMetric(category: string, value: number): void { if (\!this.observations.has(category)) { this.observations.set(category, []); } this.observations.get(category)\!.push(value); } getPercentile(category: string, percentile: number): number { const values = this.observations.get(category) || []; if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const index = Math.ceil((percentile / 100) * sorted.length) - 1; return sorted[Math.max(0, index)]; } reportHealthCheck(): { healthy: boolean; warnings: string[] } { const warnings: string[] = []; // Check latency ratio const haikuLatency = this.getPercentile("latency_haiku", 50); const sonnetLatency = this.getPercentile("latency_sonnet", 50); if (sonnetLatency > 0 && haikuLatency / sonnetLatency > 0.8) { warnings.push("Haiku latency is too close to Sonnet. Check model health."); } // Check error rates const haikuErrors = this.getPercentile("errors_haiku", 99); if (haikuErrors > 5) { warnings.push("Haiku error rate elevated."); } return { healthy: warnings.length === 0, warnings, }; }}
Deriving Thresholds From Data: A Shadow Evaluation Harness
There's one number in everything above that has no real justification behind it: the character-count cutoff that decides "complex enough for Sonnet."
Almost every routing article I've read hand-waves this. Pick 500, pick 800, tune it later. The problem is that if you guess the threshold, no amount of downstream monitoring will ever tell you whether the guess was right — you'll only see the consequences of your own choice reflected back at you.
What I use instead is a shadow evaluation harness. The idea is simple: pull real requests from production logs, run the same input through both models, and lay out the quality difference against the cost difference, bucketed by complexity. Users never see any of it.
Why you have to run both
The question routing actually asks is "is Haiku good enough for this?" You cannot answer that by looking at Haiku's output alone. Without the Sonnet output sitting next to it, every mediocre answer looks like a model problem — when quite often the input itself was ambiguous and Sonnet would have produced something equally mediocre.
Two axes are enough. If the quality gap is negligible, Haiku wins. If it's real, pay for Sonnet.
The harness
// shadow-eval.ts — replay sampled production traffic through both models// and report quality delta vs. cost delta per complexity bucketimport Anthropic from '@anthropic-ai/sdk';const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });const SONNET = 'claude-sonnet-4-6-20250514';const HAIKU = 'claude-haiku-4-5-20251001';interface SampledRequest { requestId: string; prompt: string; charLength: number;}interface PairResult { requestId: string; bucket: string; qualityDelta: number; // 0 = indistinguishable, positive = Sonnet is better costSonnet: number; costHaiku: number;}// Bucket boundaries should straddle whatever threshold you're consideringfunction bucketOf(charLength: number): string { if (charLength < 200) return '<200'; if (charLength < 400) return '200-399'; if (charLength < 600) return '400-599'; if (charLength < 800) return '600-799'; if (charLength < 1200) return '800-1199'; return '1200+';}function costOf(model: string, inTok: number, outTok: number): number { const rate = model === SONNET ? { in: 3 / 1_000_000, out: 15 / 1_000_000 } : { in: 0.8 / 1_000_000, out: 4 / 1_000_000 }; return inTok * rate.in + outTok * rate.out;}// The judge runs on Haiku too. Putting Sonnet here defeats the purpose.async function judge(prompt: string, a: string, b: string): Promise<number> { const res = await client.messages.create({ model: HAIKU, max_tokens: 64, messages: [{ role: 'user', content: [ 'You compare two answers to the same request.', 'Score how much better Answer A is than Answer B on a -2..2 scale', '(0 means indistinguishable for the user).', 'Respond ONLY with JSON: {"delta": number}', '', `Request: ${prompt}`, `Answer A: ${a}`, `Answer B: ${b}`, ].join('\n') }] }); const text = res.content[0].type === 'text' ? res.content[0].text : '{"delta":0}'; try { return JSON.parse(text).delta ?? 0; } catch { return 0; // unparseable verdicts count as "no difference" }}async function evaluatePair(req: SampledRequest): Promise<PairResult> { const [sonnet, haiku] = await Promise.all([ client.messages.create({ model: SONNET, max_tokens: 1024, messages: [{ role: 'user', content: req.prompt }] }), client.messages.create({ model: HAIKU, max_tokens: 1024, messages: [{ role: 'user', content: req.prompt }] }), ]); const sText = sonnet.content[0].type === 'text' ? sonnet.content[0].text : ''; const hText = haiku.content[0].type === 'text' ? haiku.content[0].text : ''; return { requestId: req.requestId, bucket: bucketOf(req.charLength), qualityDelta: await judge(req.prompt, sText, hText), costSonnet: costOf(SONNET, sonnet.usage.input_tokens, sonnet.usage.output_tokens), costHaiku: costOf(HAIKU, haiku.usage.input_tokens, haiku.usage.output_tokens), };}
The aggregation side puts "quality you gain by upgrading" next to "what that upgrade costs you," per bucket.
interface BucketSummary { bucket: string; samples: number; avgQualityDelta: number; // higher = upgrading to Sonnet is worth more avgExtraCost: number; // extra cost per request when upgrading costPerQualityPoint: number;}function summarize(results: PairResult[]): BucketSummary[] { const groups = new Map<string, PairResult[]>(); for (const r of results) { if (!groups.has(r.bucket)) groups.set(r.bucket, []); groups.get(r.bucket)!.push(r); } return [...groups.entries()].map(([bucket, rows]) => { const avgQualityDelta = rows.reduce((sum, r) => sum + r.qualityDelta, 0) / rows.length; const avgExtraCost = rows.reduce((sum, r) => sum + (r.costSonnet - r.costHaiku), 0) / rows.length; return { bucket, samples: rows.length, avgQualityDelta, avgExtraCost, // If the quality gap is ~0, there's no price low enough to justify upgrading costPerQualityPoint: avgQualityDelta > 0.05 ? avgExtraCost / avgQualityDelta : Infinity, }; }).sort((a, b) => a.costPerQualityPoint - b.costPerQualityPoint);}
Reading the output
Results tend to look roughly like this.
Bucket (input chars)
Avg quality delta
Extra cost / request
Verdict
<200
~0
Low
Haiku, settled
200-399
~0
Low
Haiku, settled
400-599
Slight
Medium
Undecided — use another signal
600-799
Clearly present
Medium
The boundary. Threshold candidate
800-1199
Large
High
Send to Sonnet
1200+
Large
High
Send to Sonnet
Put the threshold at the lower edge of the bucket where the quality delta first becomes real. In my case that was 600-799, so I moved my cutoff down to 600. The 800 I had been running was a pre-evaluation guess, and it turned out to be about 200 characters too aggressive.
Three things worth knowing before you run it
Fifty samples per bucket is the floor. Below that, a couple of unusually ambiguous inputs will swing the average on their own.
Don't tell the judge which answer came from which model. My first version labeled them "Answer A (Sonnet)" and "Answer B (Haiku)," and the judge leaned toward the more expensive model — inflating the measured quality gap by roughly 0.4 points before I caught it.
Run this quarterly, not weekly. Nothing shifts fast enough to justify more, and running both models over a sample costs real money. A model generation change, or a monitoring alert showing your Sonnet ratio drifting out of range, is the signal to re-run it.
Operational Lessons That Aren't in the Docs
The following eight lessons come from six-plus months of running this routing pattern in production: content classification, membership eligibility checks, search re-ranking, and iOS/Android customer-support bots. Anthropic's docs are thorough, but the failure modes specific to tens-of-millions-of-tokens monthly volume don't surface until you run it.
1. Pin the classifier itself to Haiku
I keep seeing implementations that run the Layer 3 semantic classifier on Sonnet. That's backwards. Even at 300–500 tokens per classification call, doing a million classifications a month on Sonnet costs roughly $1,500/month — for the classifier alone.
Tell Haiku 4.5: "Classify the following request as simple / moderate / complex." You'll get nearly the same accuracy at about one-quarter the cost. In my setup the classifier is permanently pinned to Haiku, and only the downstream call goes through the Sonnet/Haiku selection logic.
2. Refresh keyword dictionaries every quarter
The Layer 1 keyword list tends to become write-once code. That's lethal. The semantic weight of technical terms drifts every six months. Since the start of 2026, the frequency of "agent," "orchestration," and "dispatch" in my logs has tripled, and a request like "build me an agent" — which Haiku used to handle fine — has become materially more complex.
Once a quarter I export the last 90 days of metrics, find requests that Haiku handled but scored poorly on user feedback, and look at the co-occurring keywords. The top 30 get promoted to the Sonnet-bound keyword list. That single ritual drops misclassification rate by about 2 points.
3. Streaming fallbacks have different mechanics
In non-streaming mode, a Sonnet error is fine — just retry on Haiku. With stream: true, if you've already emitted the first content delta to the client, mid-stream failover will visibly corrupt the rendering.
My implementation gives every stream an 800 ms "safe window": until the first delta actually reaches the client, the SSE controller buffers Sonnet's output. If Sonnet fails inside that window, Haiku can pick up cleanly.
const FALLBACK_WINDOW_MS = 800;async function* streamWithSafeFallback(prompt: string, primary: 'sonnet' | 'haiku') { const startedAt = Date.now(); let firstDeltaEmitted = false; const buffer: string[] = []; try { for await (const chunk of streamModel(primary, prompt)) { if (Date.now() - startedAt < FALLBACK_WINDOW_MS && !firstDeltaEmitted) { buffer.push(chunk); // hold within the safe window continue; } if (buffer.length > 0) { for (const b of buffer) yield b; buffer.length = 0; firstDeltaEmitted = true; } yield chunk; } } catch (err) { if (!firstDeltaEmitted) { for await (const chunk of streamModel('haiku', prompt)) yield chunk; } else { throw err; // already streaming — can't safely restart } }}
4. Circuit breakers live or die on their half-open behavior
Off-the-shelf libraries like circuit-breaker-js route real user traffic through half-open probes. If those probes fail, a circuit that was about to recover slams shut again.
I use a separate health-check function for the half-open state — a lightweight "ping" prompt that doesn't touch user traffic. The breaker only fully closes after three consecutive successful pings. That single change cut our re-incident rate by 70%.
5. Alert on routing ratio drift, not on dollar amounts
A budget-based alert like "ping me when monthly spend exceeds $5,000" arrives way too late — by then you've already burned through the budget. What actually works is a ratio alert: "ping me when the Sonnet share goes from a target of 35% toward 50%." That tells you within 30 minutes whether the routing logic is broken or whether user request distribution has shifted.
We export sonnet_ratio_5min from Cloudflare Workers into Grafana, and Slack pages me when it crosses 45%. That alert has saved me twice already from a bug in the Layer 1 keyword dictionary.
6. Force Sonnet on paths gated by quality checks
For paths where a hard quality gate like article_gate.py runs, the right answer is to force Sonnet regardless of cost. The reason is simple math: when Haiku writes the article, it gets rejected more often, and the cost of regeneration easily exceeds a single Sonnet run.
Always implement a path-based bypass in your router. There are always some paths where you should optimize for success rate, not cost. My setup runs three regimes side by side: /api/generate-premium-article is forced Sonnet, /api/classify-comment is forced Haiku, everything else gets normal routing.
7. Prompt caching and routing fight each other
Running long contexts through Sonnet with prompt caching cuts cost by 70–90%. But if your router flips between Sonnet and Haiku every call, your cache hit rate craters.
The fix is session stickiness: keep the same session ID on the same model. We store the selected model per session ID in Cloudflare KV with a 24-hour TTL. Cache hit rate went from 12% to 64% after that change.
8. Request distribution shifts quietly, then suddenly
The most important lesson: request distribution does not change linearly. The night Google's new AI editor Antigravity shipped, Antigravity Lab traffic tripled within 24 hours, and most of that surge was simple-category requests like "how do I set this up" and "how do I install it." Had we been pinned to Sonnet, we'd have burned an extra ~$400 in a single day.
If you're running production routing, build a daily habit of diffing the last 24 hours against the trailing 30-day distribution. When the distribution shifts, that's your signal to retune the routing thresholds.
What Comes Next
If you implement intelligent routing, you've solved the immediate cost problem. But there are higher-order optimizations:
Prompt optimization — Once you're routing correctly, the next lever is shrinking prompts. Many teams use unnecessarily verbose system prompts. Shorter prompts = fewer tokens, regardless of model.
Caching — The Claude API supports prompt caching for long contexts. If you're reading the same document repeatedly, this can cut costs by 70-90%.
Batch processing — For non-realtime use cases, batch calls overnight at a discount. This is especially powerful for Sonnet-level work that doesn't need immediate response.
Intelligent routing is step one. These are steps two and three.
Wrapping Up
Building an intelligent router isn't complicated. It's a classification problem (layers 1-3), a resilience pattern (circuit breakers), and measurement (cost monitoring). Most teams can implement this in a day or two.
The payoff is enormous: 40-60% cost reduction, faster perceived performance on simple tasks, and graceful fallbacks that keep your system running through partial outages.
Start with layers 1 and 2 (keywords + token count). That'll catch 80-90% of your traffic. Only add embedding-based classification if you find your misclassification rate creeping above 8-10%.
Your API bills will thank you.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.