
I tried retraining the NVIDIA LLM Router to match my own persona (Training Edition)
This page has been translated by machine translation. View original
Introduction
Hello, I'm Morishige from the Classmethod Manufacturing Business Technology Department.
After going through the basics and getting NVIDIA LLM Router v3 up and running, I spent a while using the default checkpoint for my own work. The default pool ships with a 9-model general-purpose pool covering Nemotron / GPT-OSS / Qwen / GPT-5 / Opus variants (v1-9models-qwen08b.yaml), and routing decisions work perfectly well with just that.
After tinkering with it for a while, what started to bother me was that this default pool was "slightly off from my own preferences." The latest-generation Opus 4.8 / Sonnet 4.6 / Gemini 3.5 Flash weren't included yet, and the lineup didn't mesh well with the newer models I tend to want to call (DeepSeek V4 / Qwen 3.7 / Kimi K2.6 / GLM 4.7, etc.). The MLP training data was also built with general-purpose use in mind, so it wasn't tuned to my particular usage patterns (which tend to split sharply between heavy design discussions and light brainstorming sessions).
Fortunately, NVIDIA LLM Router v3 comes with official resources to rebuild your own checkpoint from scratch using the three steps: collect → train → evaluate. Rearrange the pool YAML to your liking, convert the questions you typically ask into training data, and you can build routing tailored to you from the ground up. Since the infrastructure was already in place, the starting point for this article was: let me try rebuilding it once, tailored to my own persona.
Specifically, I distilled my persona (the way I typically use Claude Code / Codex, blog posts I'm writing, design discussions around Hermes Agent and NemoClaw, etc.) into 480 training questions, and rebuilt a checkpoint from scratch for a new 9-model pool (including the latest Opus 4.8 / Sonnet 4.6 / Gemini 3.5 Flash). Designing around quality-preserving routing (heavy questions go to Opus, light questions go to cheaper models), I arrived at a configuration that pushes Opus 4.8 adoption to 43.1% while demonstrating cost savings of 98–99% (up to 99.3%) on light and medium-weight questions.
For LLM cost optimization, there are also approaches like task-type-based rule routing, as described in MindStudio's "Run Local AI Models with Claude Code" writeup. The reason I chose NVIDIA LLM Router v3 here was that I wanted to teach the quality × price tradeoff within the same task type (e.g., code generation where deeply-coupled work goes to Opus, but light one-liners go to gpt-oss-120b) to a Qwen3.5-0.8B encoder + PCA + MLP. If you just want to offload auxiliary tasks locally, rule-based routing is perfectly sufficient, so using them by purpose seems practical. This article focuses specifically on the use case of "someone who has been using Claude Code with Opus as the default and wants to move to routing without sacrificing quality."
Pool Redesign Philosophy
Lessons from the Failed 5-Model Configuration
In the first 5-model pool I built (Local + gpt-oss-120b + Kimi K2.7 + GLM 5.2 + Opus 4.6), feeding in 5 use cases resulted in 4 out of 5 non-trivial queries all collapsing into gpt-oss-120b. Looking at the cause, the pricing ladder was lopsided—there was a 14× cost cliff between gpt-oss-120b ($0.05 / $0.21) and the next most expensive option kimi ($0.74 / $3.50). The AUC difference from Opus was only about 5 points, and the MLP learned honestly that "the middle tier is good enough with gpt-oss-120b" when factoring in economics.
This is a subtly impactful trap when building an LLM Router: when the cost difference between adjacent models is too large, the MLP tends to favor the cheaper side. Having continuity with roughly 1.5–3× increments between adjacent models is the basic practice for building quality-preserving routing—this is something I learned through firsthand experience here.
The 9-Model Ladder
The final configuration I arrived at after the redesign is as follows.
| Slot | Model | OpenRouter slug | output $/M | Adjacent multiplier | Role |
|---|---|---|---|---|---|
| 1 | Nemotron 3 Nano 30B-A3B (Local) ⁽*⁾ | openrouter/nvidia/nemotron-3-nano-30b-a3b |
0 ⁽*⁾ | — | Local free anchor, lightweight reasoning |
| 2 | DeepSeek V4-Flash | deepseek/deepseek-v4-flash |
0.18 | — | Lightweight general-purpose (emerging 1 / code / long ctx 1M) |
| 3 | GLM 4.7 Flash | z-ai/glm-4.7-flash |
0.40 | 2.22x | Lightweight (emerging 2 / Chinese accuracy / 202k ctx) |
| 4 | DeepSeek V4-Pro | deepseek/deepseek-v4-pro |
0.87 | 2.18x | Medium-weight reasoning (emerging 3, cost-performance / long ctx 1M) |
| 5 | Qwen 3.7-Plus | qwen/qwen3.7-plus |
1.28 | 1.47x | Medium-weight general-purpose (emerging 4 / latest generation / long ctx 1M) |
| 6 | Kimi K2.6 | moonshotai/kimi-k2.6 |
3.50 | 2.73x | Medium-heavy general-purpose (emerging 5, high versatility) |
| 7 | Gemini 3.5 Flash | google/gemini-3.5-flash |
9.00 | 2.57x | Medium-heavy reasoning + dedicated multimodal path |
| 8 | Claude Sonnet 4.6 | anthropic/claude-sonnet-4.6 |
15.00 | 1.67x | Heavy-weight quality middle, bridge to Opus |
| 9 | Claude Opus 4.8 | anthropic/claude-opus-4.8 |
25.00 | 1.67x | Heaviest, quality anchor |
⁽*⁾ Slot 1 is running on the OpenRouter Nano paid tier ($0.05 / $0.20) to improve reproducibility across the series, but the pool YAML's cost_per_m_*_tokens is kept at 0. If you have an environment where you can run models locally, replacing litellm_model with openai/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 + api_base: http://localhost:8000/v1 will switch Slot 1 to local operation. The MLP label local-nemotron-3-nano stays as-is, and routing decisions remain directly usable without retraining.
Note that slug notation varies by provider (Anthropic / Google / Moonshot / Z.AI use dot format, DeepSeek / NVIDIA use hyphen format)—this is per OpenRouter's specifications. The slugs in this table are taken directly as verified from OpenRouter's /api/v1/models endpoint. In the basics article's default pool table, the hyphen-format Anthropic slug (anthropic/claude-opus-4-6) was listed because that was the notation as written in the default config v1-9models-qwen08b.yaml at the time Part 1 was published.
The key design point is keeping the adjacent multiplier to a maximum of 2.73×. This is roughly a 5× improvement from the old 5-model's 14× jump, aimed at preventing the MLP from "gravitating toward a lower-tier model because of a cost cliff."
※ Values shown inside Mermaid are output unit prices only. Refer to the preceding table for input unit prices.
The Reality of 2026-Generation Benchmarks
Before finalizing the configuration, I wanted to get a clear picture of the capabilities of each of the 9 models—but it turned out that 2026-generation frontier models seem to have dropped the traditional MMLU / HumanEval / MBPP / Aider polyglot from their official benchmarks. Anthropic / DeepMind / Z.AI / Moonshot / Qwen have almost universally removed them from their official disclosures. The metrics appear to have consolidated around 7 axes: GPQA Diamond / AIME / HLE / SWE-bench Verified / MMMU-Pro / LiveCodeBench v6 / Arena Elo.
Here is a reorganized view focusing on GPQA Diamond and Arena Elo for practical verification (as of writing, June 2026; sourced from each company's June 2026 official releases and lmarena.ai).
| Model | GPQA Diamond | SWE-bench Verified | MMMU-Pro | Arena Elo |
|---|---|---|---|---|
| Nemotron 3 Nano 30B-A3B | 71.9 | 38.8 | N/A | N/A |
| DeepSeek V4-Flash | 87.4 | ~74 | (text) | N/A |
| GLM 4.7-Flash | 75.2 | 59.2 | (text) | N/A |
| DeepSeek V4-Pro | 90.1 | 80.6 | (text) | 1467 |
| Qwen 3.7-Plus | 90.3 (vendor) | ~78 | not disclosed | N/A |
| Kimi K2.6 | 90.5 | 80.2 | 79.4 | 1466 |
| Gemini 3.5 Flash | 90.4 (parent) | 78 (internal) | 83.6 | 1480 |
| Claude Sonnet 4.6 | 89.9 | 79.6 | 74.5 | 1467 |
| Claude Opus 4.8 | 93.6 | 88.6 | N/A | 1512 |
※ (vendor) = vendor self-reported values; (parent) = equivalent value from parent model (Gemini 3 Pro family) used as proxy; (internal) = vendor's own internal evaluation.
What surprised me when laying this out was that the top 3 emerging models (V4-Pro / Qwen 3.7-Plus / Kimi K2.6) are virtually tied with Sonnet 4.6 on GPQA / Arena. The price difference is 17.2× (V4-Pro $0.87 vs. Sonnet $15), so there was certainly a case for "removing Sonnet from the pool"—but I kept it in due to the operational convenience of running 9 slots and because I wanted to capture any edge cases where Anthropic's strengths in long-form coherence and safety nuances make a difference. As described later, Sonnet 4.6 ended up holding a 23.1% adoption rate and reliably filling the "medium-heavy quality middle" role.
Gemini 3.5 Flash's Role and Securing the Multimodal Path
Another tricky decision in the pool design was how to handle Gemini 3.5 Flash. While its multimodal capabilities stand out (MMMU-Pro 83.6), the NVIDIA LLM Router v3's Prefill Router is text-only by design (it takes hidden states from the prompt using the Qwen3.5-0.8B encoder), meaning image / audio / video blocks have no effect on routing decisions. With short prompts like "Please analyze the following image," the moment routing selects DeepSeek or Local Nemotron, OpenRouter returns 404 No endpoints found that support input image.
The approach here was to cover Gemini's text-based roles (Google grounding / diagram structuring / Arena Elo 1480 general quality) through training data, while handling the multimodal path by adding a thin shim at the litellm adapter layer to handle it separately. It runs on a static capability lookup for the 9-model pool (image → {gemini-3.5-flash, sonnet-4.6, opus-4.8} / audio · video → {gemini-3.5-flash}). In hands-on testing, a 10×10 PNG / 1-second WAV / 1-second MP4 were all routed to Gemini 3.5 Flash and returned image, audio, and video analysis responses. I've submitted this shim as a proposal upstream as PR #34, so feel free to check that out if you're interested. If you want to build full-fledged multimodal routing based on CLIP (as in NVIDIA AI Blueprint v2's Auto-Router), that's a different story—but for the practical need of "properly routing image / audio / video requests to capable upstreams," this thin shim should cover it adequately.
Collect: Question Data Design
Overall Structure
The training data consists of 480 questions total, broken down as follows.
| Category | Count | Primary models |
|---|---|---|
| Personal persona (curated heavy-leaning 40 + new heavy 60) | 100 | Heavy 60 lean toward Sonnet / Opus |
| Opus-favored questions (heavy signal reinforcement) | 150 | Opus 4.8 dominant |
| Gemini-favored questions (text-only) | 30 | Gemini 3.5 Flash exclusive |
| Lightweight and medium-weight questions (curated light 60 + generated 40) | 100 | Nemotron / V4-Flash / GLM |
| Public datasets (MMLU 30 + HumanEval 15 + GSM8K 15 + DollyJA 40) | 100 | Bias avoidance |
The 150 Opus-favored questions are the core of this configuration, designed with the intent to "heavily embed heavy-weight signals where only Opus can answer correctly, so the MLP doesn't get outweighed by the economic rationale of emerging models." The 30 Gemini-favored questions reinforce the learning of Gemini's text-based distinctiveness (grounding / diagrams / structuring). Sonnet-favored questions were intentionally excluded (since Sonnet ties with the top emerging models on GPQA, making differentiated questions hard to calibrate—so I let the natural distribution handle it). The breakdown ends up as 210 heavy-leaning questions (44%) + 30 Gemini-leaning questions (6%).
Building the 100 Personal Persona Questions
Articulating the patterns of questions you typically send to Claude Code first makes it easier to balance against the Opus-favored questions.
| Subcategory | Count | Content examples |
|---|---|---|
| Existing (article writing / Codex / tailscale / Hermes / NemoClaw, etc.) | 40 | Selected 40 heavy-leaning entries from old curated set |
| Large-scale refactoring plans | 15 | Multi-axis topics like "DDD + async refactor of a 50 kLoC monolith," with longer code context |
| Infrastructure planning | 15 | Topics like "Multi-region Kubernetes + IaC + monitoring design," "DGX Spark + on-prem GPU cluster build" |
| PR reviews | 15 | Real code diffs (50–200 lines) + review angles (security / performance / maintainability) |
| Issue triage | 15 | Topics like "isolating a flaky test with unknown repro conditions," "root cause analysis of a production incident" |
The latter 60 questions (refactoring / infrastructure / PR / issue) are clearly heavy-weight, so they act as strong signals for "heavy = Opus."
Building the 150 Opus-Favored Questions
I organized the scenarios where Opus 4.8 clearly outperforms the top emerging models by characteristic, then structured the 150-question breakdown accordingly.
| Characteristic | Count | Examples |
|---|---|---|
| Philosophy / ethical dilemmas | 30 | Multi-principle collisions like Trolley / Frankfurt / euthanasia |
| Long-form coherence | 25 | Pointing out contradictions between character statements and prior settings in 4,000–5,000 character contexts |
| Multi-step reasoning | 25 | Bayesian updating, causal inference, LP duality |
| Refactoring | 20 | Redesigning a 50 kLoC monolith with DDD, including migration risks and rollback strategies |
| Creative writing / persona imitation | 20 | Maintaining Natsume Soseki's style or a specific character's voice over long outputs |
| Constrained judgment | 15 | Code and designs that must satisfy 10+ constraints simultaneously |
| Cultural sensitivity / honorifics | 15 | Writing polite English disagreement with a superior following Japanese business customs |
Detailed question examples will be placed in nvidia-llm-router-v3-training/data/questions-opus-favor-150.txt on GitHub. One thing I was careful about when writing the questions was "not making them too abstract." Questions like "Please explain ○○" don't produce much differentiation from the top emerging models, but framing them as "Please discuss the X aspect of ○○ in N steps within the constraints of Y and Z, in 600 characters" with explicit constraints tends to bring out Opus's true value.
Building the 30 Gemini-Favored Questions (Text-Only)
This is the part where I wanted to teach the MLP Gemini 3.5 Flash's distinctiveness. Since the multimodal path itself was separated into a different layer via the adapter shim described earlier, I prepared 30 questions focused on text-based signals I wanted the MLP to learn (grounding / diagrams / structuring).
| Category | Count | Content examples |
|---|---|---|
| Requires Google Search grounding | 10 | "Please cite the on-demand pricing for AWS Bedrock Claude Sonnet 4.5 as of June 2026" |
| Mermaid diagram output | 8 | "Create a Mermaid sequenceDiagram of the NVIDIA LLM Router v3 request flow" |
| Multi-level structured markdown | 7 | "Describe the Kubernetes Operator pattern in a 5-chapter structure using both tables and callouts" |
| Citation-based summaries | 5 | "Summarize Anthropic Constitutional AI in 500 characters using [1][2][3] citation format" |
The key is explicitly including grounding-required keywords like "as of June 2026 or later" and "the latest." While the Prefill Router can only make decisions based on the pre-call text, prompts carrying these keywords have characteristics of "text requiring Google Search grounding," which serves as a signal that indirectly nudges the MLP toward selecting Gemini. The actual grounding then runs on the Gemini side once it's called.
Sampling the 100 Public Dataset Questions
For bias avoidance, I sampled 100 questions from a combination of MMLU 30 + HumanEval 15 + GSM8K 15 + DollyJA 40. These were deterministically extracted (with a fixed seed) from a pre-built pool of 300 questions, making the process reproducible.
Running the Training Pipeline
Step 1. Environment Prerequisites and Connectivity Check
The setup is DGX Spark (aarch64, GB10, 128GB UMA) + vLLM Nemotron 3 Nano 30B-A3B NVFP4 (local) + OpenRouter (remaining 8 models). Use probe-models.py to send Reply with the single word 'pong' to each model and confirm HTTP 200 / latency / cost.
# Please adapt the verification scripts from this article to your own working directory
cd workspace/blog/scripts/nvidia-llm-router-v3-training
uv run --with httpx --with pyyaml scripts/probe-models.py \
--config configs/my-pool-9models.yaml
Once all 9/9 show OK, proceed to the next step.
Step 2. Dry-Run (10 Questions × 9 Models)
Before the full collect run, run a dry-run with 90 calls to estimate cost and latency. I hit about 2 thinking-related traps here, but after 3 attempts I arrived at a fully working 9/9 OK configuration using the approach of disabling thinking via extra_body.reasoning.enabled: false with an exception for Gemini 3.5 Flash using max_tokens: 2048. The actual cost came out to $11.27 for the full 480-question run—about 1/4 of the pre-estimate of $45–55.
Dry-Run 3 Attempts and Thinking Trap Details
Running with thinking on initially caused GLM 4.7-Flash / Kimi K2.6 / Gemini 3.5 Flash / DeepSeek V4-Pro to consume all max_tokens=1024 on thinking with no actual response content—a rampant occurrence. A large number of reply_len=0 records came out, which aren't usable as quality signals.
Switching to a unified extra_body.reasoning.enabled: false for all 6 OpenRouter models dramatically changed the latency.
| Model | thinking on | thinking off | Ratio |
|---|---|---|---|
| GLM 4.7-Flash | 29.75s | 5.69s | 5.2x |
| Qwen 3.7-Plus | 34.03s | 2.94s | 11.6x |
| Kimi K2.6 | 13.40s | 3.51s | 3.8x |
But another trap was waiting here. Gemini 3.5 Flash has mandatory reasoning, and passing reasoning: { enabled: false } returns HTTP 400 ("Reasoning is mandatory for this endpoint and cannot be disabled."). The workaround was to leave thinking on for Gemini only, while raising max_tokens: 2048 to reserve tokens for the actual response.
The cost across the 3 attempts was as follows—I honestly didn't expect the max_tokens=1024 + thinking off combination to be this economical, so that was a pleasant discovery.
| Attempt | Cost (90 calls) | 480q estimate |
|---|---|---|
| 1st (thinking on) | $0.265 | $12.70 |
| 2nd (thinking off all models) | $0.148 | $7.12 |
| 3rd (thinking off + Gemini exception) | $0.235 | $11.27 |
Step 3. The judge=vote Trap and Switching to judge=llm
After completing the dry-run, submitting the full collect with judge=vote produced per-model accuracy numbers that made me do a double take: "Local Nemotron 99.8% / all other models 0–0.2%." The cause was in the _judge_vote logic in collect.py: when each model returns a different natural language response, the Counter ends up with all equal counts, and insertion order causes the pool's first entry (Local Nemotron) to always be treated as "correct." This works for MMLU-style option matching, but it's unusable for generative tasks.
Switching to judge=llm + Sonnet 4.6 as judge and re-running collect (7h 15m), a reasonable distribution emerged—Opus at the top, the top 3 emerging models clustered together, and Local / GLM at the bottom—making this usable as a quality signal.
| Model | accuracy (Sonnet judge) | avg tokens |
|---|---|---|
| Opus 4.8 | 69.17% | 738 |
| Sonnet 4.6 ⁽*⁾ | 65.2% | 709 |
| DeepSeek V4-Flash | 63.1% | 724 |
| DeepSeek V4-Pro | 61.0% | 718 |
| Kimi K2.6 | 60.0% | 946 |
| Qwen 3.7-Plus | 56.5% | 741 |
| Gemini 3.5 Flash | 54.6% | 1,719 |
| GLM 4.7-Flash | 39.0% | 742 |
| Local Nemotron | 38.3% | 940 |
⁽*⁾ Sonnet 4.6 also serves as judge, so self-scoring bias applies
Implementation Details of the `judge=vote` Bug and Re-Collect Command
The initial judge=vote results were as follows—a local free model defeating all 8 other top models with 99.8% was clearly a logic-side issue.
| Model | accuracy |
|---|---|
| Local Nemotron | 99.8% |
| DeepSeek V4-Flash | 0.0% |
| GLM 4.7-Flash | 0.2% |
| DeepSeek V4-Pro | 0.2% |
| Qwen 3.7-Plus | 0.2% |
| Kimi K2.6 | 0.0% |
| Gemini 3.5 Flash | 0.0% |
| Claude Sonnet 4.6 | 0.0% |
| Claude Opus 4.8 | 0.0% |
Reading through _judge_vote in collect.py immediately revealed the cause.
def _normalize(text: str) -> str:
return " ".join(re.split(r"\s+", text.strip().lower()))
def _judge_vote(outputs: list[str]) -> str:
normalized = [_normalize(o) for o in outputs]
counts = Counter(normalized)
return counts.most_common(1)[0][0]
When each model returns a different natural language response, all strings remain unique even after normalization. Since Counter respects insertion order for ties, the response of the first model to appear (Local Nemotron, being Slot 1 in the pool YAML) always becomes the majority. Only models whose responses match Local's are judged "correct," and everything else gets 0%.
The re-collect command is as follows (with the judge parallelization patch applied, max_workers=5, targeting a ceiling that doesn't hit OpenRouter's rate limit).
model-router collect \
--config configs/my-pool-9models.yaml \
--questions data/questions-9models.txt \
--judge llm \
--judge-model openrouter/anthropic/claude-sonnet-4.6 \
--output data/collected-my-pool-9models.csv
If you're building your own personalized routing for other use cases, watch out for the same pitfall. It works fine for MMLU-style questions (where A/B/C/D option letters come back as strings), but for generative tasks, using judge=llm is the safe choice.
Step 4. Train (Completes in 5 Minutes)
model-router train \
--config configs/my-pool-9models.yaml \
--data data/collected-my-pool-9models.csv \
--output-dir checkpoints/my-router-9models/
Training runs with accelerate enabled, a 9-dim MLP, and the Qwen/Qwen3.5-0.8B encoder. Stages 1–6 (Load → Extract → PCA → MLP ensemble → Calibrate → Save) completed in 5 minutes and 1 second.
The per-model AUC for the completed checkpoint is as follows.
| Model | AUC (Shared trunk ensemble) |
|---|---|
| Qwen 3.7-Plus | 0.9430 (highest among 9 models) |
| Gemini 3.5 Flash | 0.9250 |
| Claude Sonnet 4.6 | 0.9162 |
| Claude Opus 4.8 | 0.9103 |
| DeepSeek V4-Flash | 0.9065 |
| GLM 4.7-Flash | 0.8937 |
| Local Nemotron | 0.8837 |
| DeepSeek V4-Pro | 0.8757 |
| Kimi K2.6 | 0.8517 |
All models achieved AUC above 0.85, exceeding the target value I had in mind (per-model AUC 0.5–0.9). Compared to the default checkpoint where p_max was 0.07–0.10, it's striking how much tighter the MLP's predictions become just by preparing proper training data.
Step 5. evaluate (Quality Improvement in Numbers)
model-router evaluate \
--config configs/my-pool-9models.yaml \
--checkpoint checkpoints/my-router-9models/prefill_router.pt \
--data data/collected-my-pool-9models.csv \
--output results/eval-my-router-9models.json
| Metric | Value |
|---|---|
| Oracle accuracy | 0.9250 |
| Best single model | 0.6917 (Opus 4.8 alone) |
| Router accuracy (argmax) | 0.7937 (+10.20pp vs Opus alone) |
| Headroom captured | 43.7% |
The quantitatively visible figure here is: "Compared to 69.17% accuracy when Opus alone solved 480 questions, the router achieves 79.37%, an improvement of over 10 percentage points." The router captures 43.7% of the gap between that and the Oracle accuracy (the upper bound when the best model is always chosen) of 92.5%.
Let's also look at the argmax distribution from the evaluation.
| Model | Times Selected | Ratio | Accuracy When Selected |
|---|---|---|---|
| Claude Opus 4.8 | 207 | 43.1% | 64.7% |
| Claude Sonnet 4.6 | 111 | 23.1% | 99.1% |
| Gemini 3.5 Flash | 59 | 12.3% | 98.3% |
| DeepSeek V4-Pro | 33 | 6.9% | 69.7% |
| Qwen 3.7-Plus | 31 | 6.5% | 90.3% |
| DeepSeek V4-Flash | 23 | 4.8% | 82.6% |
| Kimi K2.6 | 16 | 3.3% | 56.3% |
| Local Nemotron | 0 | 0.0% | — |
| GLM 4.7-Flash | 0 | 0.0% | — |
Opus 4.8 leads with 43.1% adoption, followed by Sonnet 4.6 at 23.1% with a remarkable 99.1% accuracy. Gemini 3.5 Flash also comes in at 12.3% with 98.3%. During the design phase, I had said "Sonnet would be selected only occasionally," but when the lid was opened, it turned out to be surprisingly stable as the main player in the mid-to-heavy quality middle tier.
It may seem counterintuitive that Opus's 64.7% is lower than Sonnet's 99.1%, but this is simply because "the difficulty of the sets selected by argmax differs." The 207 questions for which Opus is chosen via argmax are hard question clusters where the MLP has determined that "P(correct) won't be high unless it's Opus," so even Opus faces a higher bar to be judged correct, dropping to 64.7%. Sonnet / Gemini's 99.1% / 98.3% are results of comparatively easier questions being routed to them, as the MLP determined "these can be solved without Opus." Just a note that the previously mentioned 69.17% for Opus alone is aggregated across all 480 questions, so it comes from a different set than the 64.7% here (a 207-question subset).
Conversely, Local Nemotron and GLM 4.7-Flash end up at 0% with argmax. Their accuracy itself is around 38–39%, but with tolerance=0 argmax judgment, they always lose because "other models produce higher P(correct) for the same question." In this configuration where training data is skewed 44% toward heavy models, they have no opportunity to be selected. If tolerance is raised to 0.20, they will properly be picked for lightweight questions.
Behavior of the Persona-Optimized Checkpoint
Routing Demonstration by Tolerance (bench_persona_3tol)
bench_persona_3tol.py is a script that sends 5 representative question types (casual chat / code generation / technical explanation / math proof / philosophy) at 3 tolerance levels (0.05 / 0.10 / 0.20), comparing the routing distribution and cost at each level. I brought up the new checkpoint with model-router serve --port 8204 and ran it.
| Tolerance | Main Models Adopted | Total Cost (5 questions) | Reduction Rate (vs All-Opus) |
|---|---|---|---|
| 0.05 | deepseek-v4-flash dominant | $0.00055 | 99.3% |
| 0.10 | deepseek-v4-flash + glm-4.7-flash | $0.00080 | 99.0% |
| 0.20 | lightweight tier + Local Nemotron + glm | $0.00133 | 98.3% |
| (All-Opus) | Opus 4.6 direct call (bench script fixed) | $0.07664 | baseline |
The (All-Opus) row uses the bench script's comparison baseline model fixed to anthropic/claude-opus-4-6 as-is, so it's the old Opus 4.6 rather than Slot 9 (Opus 4.8) in the new pool. The output price per token is similar, so it's fine as a comparison baseline, but if you want to match the figures precisely, rewrite OPUS_MODEL in the bench script to claude-opus-4.8.
The 5 bench questions are all "lightweight to medium," so Opus not appearing is as expected. Even at tol=0.05, deepseek-v4-flash dominates the lightweight tier — this looks like a gap compared to V4-Flash being selected only 4.8% in the evaluate (480 questions / argmax), but that's because the 5 bench questions are skewed toward the left tail (lightweight side) of the evaluate difficulty distribution. When tolerance is raised to 0.20, Local Nemotron also gets picked up, making lightweight questions virtually free.
Interpreting the Routing Distribution and Sonnet's Role
Let me organize the point that routing behavior looks completely different between evaluate (480 questions / argmax) and bench (5 lightweight-medium questions / with tolerance).
| Evaluation Axis | Environment | Main Models Adopted | Interpretation |
|---|---|---|---|
| evaluate | 480 questions / argmax (tol=0) | Opus 43.1% + Sonnet 23.1% + Gemini 12.3% | Quality-preserving routing |
| bench_persona_3tol | 5 light-medium questions / tol 0.05-0.20 | deepseek-v4-flash + glm-4.7-flash + Local | Cost-reduction routing |
This is the ideal form of the design — routing automatically switches based on question difficulty and tolerance — realized as intended. There is a clear contrast: heavy questions are handled by Opus / Sonnet to preserve quality, while lightweight questions are handled by lightweight models to cut costs.
At the design stage, my stance was "Sonnet ties with the emerging top tier (V4-Pro / Qwen 3.7-Plus / Kimi K2.6) on GPQA, so I won't add differentiation questions in training data — it'll be selected only occasionally," but evaluate showed 23.1% adoption / 99.1% accuracy. Since argmax (tol=0) doesn't factor in cost, "it was chosen because it's cheap" doesn't explain it. The plausible explanations are: (1) using Sonnet 4.6 as the judge introduced self-scoring bias, boosting Sonnet's correct labels and causing the MLP to learn higher P(correct) for Sonnet; or (2) for the 111 selected questions, the MLP's Sonnet predictions genuinely surpassed Opus. A clean separation requires re-collecting with a different judge (GPT-5.4 or Gemini 3.5 Pro), but the fact that Sonnet is functioning stably as a mid-to-heavy quality middle tier doesn't change — the ideal form of "maintaining quality while also reducing costs" is working quantitatively.
How to Increase Local Nemotron's Adoption Rate
The evaluate result showed Local Nemotron at 0% with argmax, and only 2/5 adoption with bench tol=0.20. If you want to "route more brainstorming-type queries to Local," you can tune this in 3 stages: (1) add extra_body.routing.tolerance: 0.20 to the request body to raise tolerance per request (easiest, no restart needed); (2) spin up a separate server on a different port with --models restricted to only the lightweight tier; (3) add 50–100 questions of casual chat / short translation / simple memo organization to the training data and re-collect / re-train. In practice, (1) is the most convenient, and using it per Hermes Agent profile — "tolerance=0.20 for casual chat, 0.05 for code generation via Claude Code" — lets you handle multiple use cases with a single routing service. The topic of integrating with flows running on the Hermes side is planned to be covered in the practical edition.
The Cost Cliff Visible Through Comparison with the Old 5-Model Checkpoint
For reference, I'll also show the routing distribution of the old 5-model pool (Local Nemotron / gpt-oss-120b / Kimi K2.7 Code / GLM 5.2 / Opus 4.6) mentioned at the beginning of the pool redesign. When 5 use cases were submitted, all 4 non-lightweight cases were concentrated on gpt-oss-120b, with p_max 0.79–0.94 showing high confidence, but Opus / Kimi / GLM were never called — a structure where they'd never get a turn.
| Use Case | Model Selected | p_max |
|---|---|---|
| Casual chat (light) | local-nemotron-3-nano | 0.94 |
| Code generation (mid) | gpt-oss-120b | 0.87 |
| Technical explanation (mid) | gpt-oss-120b | 0.83 |
| Math proof (mid-heavy) | gpt-oss-120b | 0.79 |
| Philosophy discussion (heavy) | gpt-oss-120b | 0.81 |
When placed side by side with the new 9-model ladder, the root cause — the 14x jump — is clearly visible even visually.
※ All prices within Mermaid are unified as output prices per token. For input prices, please refer to the respective tables shown earlier.
As a basic practice when building your own routing, being conscious from the start that "the cost multiplier between adjacent models should be within 3x, ideally around 2x" makes it easier to avoid cost cliffs like this.
Summary
Starting from a feeling of "this might be a little different from my preferences" after thoroughly trying out the NVIDIA LLM Router v3 default checkpoint, I went all the way through rebuilding a 9-model pool from scratch and running persona training on 480 questions. The result: with Opus 4.8 adoption at 43.1% preserving quality, while achieving 98–99%+ cost reduction (up to 99.3%) with tolerance 0.05–0.20 for light-to-medium questions, the effectiveness of quality-preserving routing became tangible. The pitfalls encountered along the way (thinking on consuming all max_tokens, the insertion order bug in judge=vote, Gemini's mandatory reasoning, the 14x cost cliff jump) are written down in detail in hopes that they'll serve as reference for others building their own routing for different use cases. In particular, the pool design principle of "cost multiplier between adjacent models within 3x, ideally around 2x" is a basic practice worth being aware of from the start.
To avoid "just building and stopping," observation after putting the checkpoint into production also becomes important. The routing service used in this article has Langfuse callbacks and thin stdout logging of routing decisions, making it possible to retrospectively confirm how many times each model was called, what the costs were, and how the distribution shifts when tolerance is changed. I'm thinking about covering this observability topic more leisurely in a separate article.
Even with "reference implementation only" written on it, v3 is this much fun to play with — the subtle enjoyment of watching it train and smoothly conform to your own persona is genuinely satisfying.
Reference Links
NVIDIA LLM Router
- NVIDIA-AI-Blueprints/llm-router (GitHub) — The repository including the v3 branch covered in this article
- NVIDIA LLM Router で LLM の用途別使い分け環境を構築してみた(基礎編) — Series Part 1, basics of default checkpoint behavior and the OpenAI-compatible endpoint
Upstream PRs Submitted During Verification for This Article
- PR #32: forward all OpenAI-compatible fields to upstream — Proposed fix for the issue where OpenAI-compatible fields such as
toolswere being dropped - PR #33: translate upstream errors to OpenAI-compatible responses — Proposed fix for the issue where upstream 4xx errors were being returned as 500
- PR #34: route image/audio/video requests to capability-matched models — Proposed adapter shim for routing multimodal content to capability-matched models
Related Topics
- OpenRouter — API gateway used as upstream for this article's pool
- Langfuse — Integrated via LiteLLM callback for observability
- lmarena.ai — Source of Arena Elo referenced in the benchmark tables in this article

