NVIDIA LLM Router の後継 NeMo Switchyard を試してみた

NVIDIA LLM Router の後継 NeMo Switchyard を試してみた

NVIDIA LLM Router の後継にあたる Python ライブラリ「NeMo Switchyard」がアルファリリースされました。実際に動かして試してみました。
2026.07.03

はじめに

こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。

DGX Spark で NVIDIA LLM Router を動かす記事を 2 本公開したばかりなのですが、その直後に「LLM Router の後継にあたる Python ライブラリが出た」と教えてもらいました。NeMo Switchyard、v0.1.0 のリリースは日本時間の 2026 年 7 月 1 日です。

記事の続きを準備していた身としては少し複雑な気持ちですが、実際に触ってみると「LLM Router で苦労して自作した機能が最初から入っている」ことが次々に分かってきました。この記事では、手元の Mac と DGX Spark で Switchyard を動かし、LLM Router 検証で踏んだ罠が本当に解消されているのかを確かめていきます。

https://dev.classmethod.jp/articles/dgx-spark-nvidia-llm-router-v3/

https://dev.classmethod.jp/articles/dgx-spark-nvidia-llm-router-v3-training/

NeMo Switchyard とは何者か

GitHub の NVIDIA-NeMo organization で公開された、LLM トラフィックを振り分ける routing proxy です。pip install nemo-switchyard で入る Python パッケージで、中身は maturin でビルドされた Rust コアに Python がかぶさる二層構成。ライセンスは Apache 2.0、バージョンは 0.1.0 で、開発ステータスには Alpha と明示されています。

https://github.com/NVIDIA-NeMo/Switchyard

ドキュメントサイトも公開されています。

https://nvidia-nemo.github.io/Switchyard/

LLM Router を触ったことがある方向けに、性格の違いを表にしてみます。

観点 LLM Router NeMo Switchyard
配布形態 Docker Compose(Blueprint、fork 前提) pip install
ルーティング判定 訓練済み分類器(要 GPU、要訓練データ) LLM classifier または tool 実行履歴のヒューリスティクス
対応 API OpenAI Chat Completions のみ OpenAI Chat / Anthropic Messages / OpenAI Responses を変換
Claude Code 接続 CCR などの変換プロキシを別途用意 switchyard launch claude の 1 コマンド
GPU router の推論に必要 不要

大きな違いは「訓練済み router がいない」ことと「プロトコル変換を内蔵した」ことの 2 点かなと思います。LLM Router は Qwen の embedding を PCA と MLP に通す独自分類器を訓練する設計でした。Switchyard はそこを LLM への問い合わせや、エージェントの tool 実行履歴から取れるシグナルに置き換えています。GPU が要らなくなったので、Mac でもそのまま動きます。

routing は 4 方式から選ぶ

ドキュメントに載っている routing は 4 方式です。

方式 tier の決め方 向いている場面
passthrough 固定 1 モデル 別名を安定させたいだけのとき
random-routing 指定確率で strong / weak を振り分け A/B テスト、コスト実験
llm-routing classifier 用の LLM がリクエスト内容を分類 内容に応じた振り分け
cascade tool 実行結果のシグナルで判定し、迷ったときだけ classifier に相談 コーディングエージェントの長い作業

llm-routing は、直近 4 ターンの会話を要約して classifier モデルに渡し、simple / medium / complex / reasoning の 4 カテゴリに分類させてから weak / strong の tier にマップします。判定には tool calling を使い、確信度が閾値を下回ったり分類に失敗したりしたときはデフォルト tier に落ちる fail-open 設計です。分類ポリシーは general、coding_agent、openclaw の 3 種類が組み込まれていて、コーディング向けと常駐アシスタント向けがあらかじめ用意されているのが面白いところですね。

cascade はさらに凝っていて、エラーの深刻度やテストの合否、ファイル編集の回数といった tool 実行結果のシグナルを 3 層で判定します。まず明確な状況(クリティカルなエラーは strong、テスト全通過の仕上げ作業は weak)を即決し、次に重み付きスコアで判断、確信が持てないときだけ LLM classifier に相談する、という段階構成です。ユーザーが触るダイヤルは confidence_threshold の 1 つだけで、推奨値 0.5 は SWE-Bench Pro で較正したと書かれています。

インストールから serve まで

Python 3.12 以降が条件です。今回は uv で環境を作りました。

uv init switchyard-handson && cd switchyard-handson
uv add "nemo-switchyard[server,cli]"

wheel は Linux の x86_64 / aarch64 に加えて macOS の arm64 も用意されているので、Apple Silicon の Mac にそのまま入ります。

設定は endpoints(プロバイダ接続)、targets(upstream のモデル)、profiles(クライアントに見せる routing ポリシー)の 3 層構造です。LLM Router 記事で使っていたモデル pool と比較しやすいよう、同じモデルを OpenRouter 経由で並べました。

profiles.yaml
endpoints:
  openrouter:
    base_url: https://openrouter.ai/api/v1
    api_key: ${OPENROUTER_API_KEY}

targets:
  strong:
    endpoint: openrouter
    model: anthropic/claude-sonnet-4.6
    format: openai
  weak:
    endpoint: openrouter
    model: nvidia/nemotron-3-nano-30b-a3b
    format: openai
  classifier:
    endpoint: openrouter
    model: google/gemini-3.5-flash
    format: openai

profiles:
  fast:
    type: passthrough
    target: weak
  smart:
    type: llm-routing
    profile_name: coding_agent
    strong: strong
    weak: weak
    classifier: classifier

つまずいたのは 2 点だけでした。profile の type 名はハイフン区切りの random-routing で、クイックスタートの例に出てくる random_routing(アンダースコア)は旧形式の route bundle 用の別体系です。また llm-routing の classifier には target の ID を文字列で渡します。どちらもエラーメッセージが「expected one of strong, weak, ...」「expected a string」と期待する形を具体的に教えてくれるので、すぐに直せました。

この 2 形式の命名差はクイックスタートに注記がなかったので、ドキュメントへの追記を upstream に PR として送っています。

https://github.com/NVIDIA-NeMo/Switchyard/pull/19

起動は 1 コマンドです。

uv run switchyard serve --config profiles.yaml --port 4000

これで OpenAI Chat Completions、Anthropic Messages、OpenAI Responses の 3 つの API が同じポートに生えます。クライアント側の形式と upstream 側の形式が違っても、内部の中間表現を介して相互変換してくれます。

/v1/models が 3 種類の ID を返す

serve を立てて最初に GET /v1/models を見ると、Switchyard の設計思想が一番よく分かります。返ってくるモデル一覧に、種類の違う ID が同居しています。

{"id": "smart", "display_name": "llm-routing", ...}
{"id": "strong", "display_name": "anthropic/claude-sonnet-4.6", ...}
{"id": "anthropic/claude-sonnet-4.6", "display_name": "target strong", ...}

profile の ID(smart)を model に指定すると routing が働き、target の ID(strong)や upstream のモデル名を指定すると routing をスキップしてそのモデルに固定されます。つまり「振り分けてほしいとき」と「このモデルと決め打ちしたいとき」を、クライアントは model 名の使い分けだけで選べる設計です。

これを見て、思わず遠い目になりました。LLM Router 連載の実践編として準備していた検証で、一番の壁がまさにここだったからです。

LLM Router の「model 名無視」は解消されたのか

LLM Router には、リクエスト body の model を見ないという仕様がありました。クライアントが model: claude-opus-4-8 と明示しても auto routing に上書きされてしまうため、Claude Code のようにタスク種別ごとにモデル名を出し分けるクライアントとは相性が悪く、検証では fork にモデル名バイパスのパッチを当てて回避していました。

Switchyard に同じ検証を投げてみます。同一プロンプト「Say OK only.」を、指定する model 名だけ変えて 5 通り送りました。

指定した model 名 種別 実際に応答したモデル
smart profile Nemotron 3 Nano(routing が「簡単」と判定)
strong target Claude Sonnet 4.6 に固定
weak target Nemotron 3 Nano に固定
anthropic/claude-sonnet-4.6 upstream 名 Claude Sonnet 4.6 に固定
nvidia/nemotron-3-nano-30b-a3b upstream 名 Nemotron 3 Nano に固定

routing してほしいときは routing され、固定したいときは固定される。LLM Router では fork のコードに手を入れてようやく実現した挙動が、最初から公式にサポートされています。この一点だけでも、乗り換える価値はあるかなと思っています。

switchyard launch claude で Claude Code を 1 コマンド接続

Claude Code は Anthropic API 専用のエージェントなので、OpenAI 互換の routing proxy と繋ぐには従来 CCR(Claude Code Router)のような変換プロキシを挟む必要がありました。Switchyard は Anthropic Messages の変換を内蔵しているため、これが 1 コマンドになります。

switchyard launch claude

空きポートに proxy が立ち、ANTHROPIC_BASE_URL などを差し替えた Claude Code がそのまま起動します。デフォルトの構成は Claude Opus 4.7(strong)、Kimi K2.6(weak)、Gemini 3.5 Flash(classifier)という検証済みトリオで、画面下部のステータスフッターに tier ごとのリクエスト数とトークン数がリアルタイム表示されます。簡単な指示と重い相談を投げ分けると、フッターの数字で振り分けが見えるのはなかなか爽快ですよ。

switchyard launch claude で起動した Claude Code。下部のステータスフッターに llm-classifier route 全体と tier(kimi-k2.6)のリクエスト数・トークン数が表示されている

セッション内で「今はどんなモデルで動作している?」と尋ねると、route の ID である switchyard-deterministic-... が返ってきました。Claude Code 本体は自分が proxy 越しの routing 構成で動いていることを知らないまま、裏では Kimi K2.6 が応答している。この透過っぷりが launcher の肝です。

疎通の確認には smoke テストが用意されています。credential の解決から proxy 起動、Claude Code の実応答までを 8 段階で自動チェックしてくれました。

[1/8] Resolving credentials...        OK
[2/8] Reaching backend...             OK (GET /models 200, 289ms)
[3/8] Probing /v1/messages support... OK (native passthrough)
[4/8] Starting proxy...               OK (127.0.0.1:51068)
[5/8] Locating claude binary...       OK
[6/8] Round-tripping chat completion... OK (reply='ok')
[7/8] Spawning claude with proxy env...  OK (10463ms)
[8/8] Tearing down proxy...           OK
verify claude: PASS (model=moonshotai/kimi-k2.6, 14322ms)

複数の routing 構成を切り替えたい場合は、route bundle という YAML を渡します。登録した route は Claude Code の /model ピッカーに並ぶので、「普段は classifier 任せ、難所だけ Sonnet 固定」といった切り替えが会話の途中でできます。ここで 1 つ注意があって、Claude Code のピッカーは ID が claude か anthropic で始まるモデルしか表示しません。Switchyard 側が claude- prefix の別名を自動生成してくれますが、route 名を最初から claude-smart のように付けておくと picker の表示と一致して迷いません。

Claude Code の /model ピッカー。switchyard の route に加えて、claude- prefix 付きの backend カタログが並び、末尾に +336 models と表示されている

実際にピッカーを開いてみると、登録した route の下に backend カタログのモデルが claude- prefix 付きでずらりと並んでいました。手元では 340 個超え。route として定義していないモデルにも、その場の気分で直接切り替えられるということですね。

なお route bundle は旧形式扱いで、起動時に deprecated の警告が出ます。かといって新形式の profile config を launcher に渡すオプションはまだ無いので、ピッカーで複数 route を切り替えたい場合は現状これが唯一の手段です。bundle 経由の起動は listen まで 1 分近くかかることがあり、自分は最初ハングしたと勘違いしました。このあたりは 0.1.0 らしい過渡期の隙間ですね。

/effort は今度こそ効くのか

LLM Router 検証で踏んだもう 1 つの罠は、実は CCR 側にありました。Claude Code は /effort コマンドで思考の深さを 5 段階に調整できますが、その値は body の output_config.effort に入っていて、thinking フィールドは effort レベルに関係なく常に {type: "adaptive"} が付きます。CCR の think 判定は thinking の存在しか見ないため、/effort low の軽いリクエストまで高価なモデルに直送されてしまう。回避には判定ロジックを差し替えるカスタム router を書く必要がありました。

Switchyard で同じ状況を再現してみます。thinking: {type: "adaptive"} を付けたまま、output_config.effort を 5 段階に変えた Anthropic Messages リクエストを llm-routing の profile に投げました。

effort 応答したモデル
low Nemotron 3 Nano
medium Nemotron 3 Nano
high Nemotron 3 Nano
xhigh Nemotron 3 Nano
max Nemotron 3 Nano

プロンプトは全部「Say OK only.」なので、内容で判定すればすべて weak 行きが正解です。thinking フィールドが付いていても routing は暴発しませんでした。Switchyard の変換層はリクエストを一度中間表現に落とす作りで、output_config.effort はそこで第一級のフィールドとして扱われています。「thinking があるから重い処理」という短絡的な判定をする場所が構造的に存在しない、と言えそうです。

DGX Spark 1 台で完全ローカル routing を組む

aarch64 の wheel が出ているので、DGX Spark でも試してみました。venv を作って nemo-switchyard[server] を入れるだけで、GB10 の aarch64 環境でもそのまま import が通ります。

せっかくなので、外部 API を一切使わない完全ローカル構成を組んでみました。ollama に入れた 2 つのモデルを tier に見立てます。

targets:
  strong:
    endpoint: ollama # http://localhost:11434/v1
    model: qwen3.6:35b
  weak:
    endpoint: ollama
    model: qwen3:1.7b

llm-routing の profile に「2+2 は?」と聞くと 1.7B が即答し、「停止性問題が決定不能であることを対角線論法で証明して」と投げると 35B に切り替わりました。routing の判定も含めて全部 DGX Spark 1 台の中で完結しています。ローカル LLM 運用で「軽い質問に 35B を起こすのはもったいない」と感じていた方には、かなり刺さる構成ではないでしょうか。

1 つ学びがありました。最初は classifier にも 1.7B を割り当てたのですが、小さいモデルは tool calling の強制指定(tool_choice)を無視して普通の文章で答えてしまい、分類がすべて失敗しました。fail-open 設計なので routing 自体は止まらずデフォルト tier に流れ続けるのですが、気づけたのは stats API に classifier のエラー数がそのまま出ていたからです。classifier には tool calling を確実にこなせる規模のモデルを充てる、というのが実践上のポイントになりそうです。

Hermes Agent の普段使いを Switchyard に切り替えてみた

手元では Hermes Agent を LLM Router 経由で運用してきました。せっかくなので、この普段使いのトラフィックも Switchyard に切り替えてみます。変更は接続先の設定ファイルで base_url を差し替えるだけでした。

model:
  default: hermes # Switchyard 側の profile 名
  provider: custom
  base_url: http://localhost:4000/v1
  api_key: dummy
  api_mode: chat_completions

Switchyard の serve はクライアント向けの認証を要求しないので、API キーはダミーで通ります。profile には常駐アシスタント向けの openclaw ポリシーを割り当てました。切り替え後に単発の質問を投げると、簡単な算数は weak の Nemotron に振られ、stats のカウンターが動くところまで確認できています。

定時実行のニュース配信ジョブは LLM Router 側に残してあるので、同じ期間の「LLM Router 運用」と「Switchyard 運用」を並走させて見比べられる構成になりました。

切り替えてから半日程度、日常のやり取りをそのまま流してみました。Switchyard の stats では 125 リクエストでエラーは 0。内訳は 98.4% が weak の Nemotron に振られ、strong の Sonnet に行ったのは動作確認の 2 件だけでした。普段の会話の大半は simple / medium 判定で捌ける、というのが openclaw ポリシーの実際の顔つきのようです。routing 判定の上乗せは中央値で約 2.1 秒。常駐エージェントの返答としては誤差の範囲で、tool を使う処理も含めて体感の劣化は感じませんでした。

面白かったのはコストのほうです。OpenRouter 側の集計(同期間、事前検証の分も含む)では、weak 本体が約 567 万トークンで $0.32 だったのに対して、classifier の Gemini 3.5 Flash は約 59 万トークンで $0.70。判定役が本体の 2 倍強のコストを使っていました。llm-routing は毎ターン直近 4 ターン分の会話を classifier に送るため、エージェントの長いコンテキストがそのまま載って 1 回あたり平均 4,000 トークンになります。初回判定で tier を固定する sticky や、classifier を呼ばずに済ませる cascade が用意されている理由を、財布で理解した格好です。(cassifier のモデル設定を完全に誤りました。。)

もうひとつ、運用に乗せたからこそ見つかった問題があります。Switchyard の stats で weak の token 数がいつまでも 0 のままだったのです。OpenRouter 側には 567 万トークンがしっかり記録されているので、消えたのは集計だけ。切り分けていくと、streaming 応答の usage が stats に載らない(バッファされた応答しか集計されない)実装になっていて、upstream が usage フレームを送ってきていても落ちることまで確認できました。エージェント経由のトラフィックはほぼ streaming なので、実運用ではコスト集計が丸ごと見えなくなります。この件は再現手順と原因箇所を添えて issue として報告しています。

https://github.com/NVIDIA-NeMo/Switchyard/issues/18

気になっているところ

良いところをたくさん書いてきたので、現時点の注意点も正直にまとめておきます。

まず開発ステータスが Alpha で、known issues も公開されています。0.1.0 時点では Codex 連携でトークン集計が 0 になるケースと、tool 付きリクエストが固定 tool スキーマの upstream に振られると失敗するケースの 2 件です。後者は tool を多用するエージェント運用では踏み得るので、routing 先の tier をすべて tool calling 対応モデルで揃えておくのが無難だと思います。

format: の指定漏れにも注意が必要です。省略すると OpenAI 形式として扱われ、Claude 系モデルに送るときに prompt caching 用の cache_control が剥がれるとドキュメントに明記されています。Claude を upstream にする target には format: anthropic を明示しておきましょう。

LLM Router の自分色バージョンは、9 モデルの pool から訓練済み分類器が 1 つを選ぶ多者択一でした。Switchyard の組み込み routing はどれも strong / weak の二者択一に classifier を添える設計なので、それ以上の使い分けをしたければ route を複数定義して、呼び出し側が model 名や /model ピッカーで明示的に選ぶことになります。自動判定は 2 択に絞り、多者択一は呼び出し側の明示指定に任せる、という役割分担です。これは以前紹介した Sakana Fugu で、fugufugu-ultra のどちらを呼ぶかはクライアント側の責任とし、呼んだ先の内部でオーケストレーションが動く、という構成と似た考え方ですね。

https://dev.classmethod.jp/articles/sakana-fugu-ga-first-touch/

では多者択一の自動判定はもう戻ってこないのかというと、コードを読んでいて面白い発見がありました。ドキュメントに載っている routing は 4 方式ですが、ソースには LMSYS の RouteLLM(行列分解ベースの学習型 router)を profile として組み込む型がすでに実装されています。訓練済み router が消えたように見えて、学習型の受け皿はちゃんと用意されている。LLM Router の訓練資産を持っている身としては、ここは続編で掘りたいポイントです。

まとめ

NeMo Switchyard を Mac と DGX Spark で動かし、LLM Router 検証で踏んだ 2 つの罠を再検証しました。model 名が無視される問題は profile / target の ID 使い分けとして公式に解決され、/effort の暴発は変換層の設計レベルで発生しなくなっています。fork へのパッチや CCR のカスタム router として自作していたものが、どちらも不要になりました。

pip で入って GPU も不要、Claude Code とは 1 コマンドで繋がり、DGX Spark 上なら完全ローカルの routing も組める。Alpha 版らしい荒削りな部分はあるものの、LLM Router の「Blueprint を fork して育てる」重さから考えると、導入の敷居は劇的に下がった印象です。

次回は、複数日分の運用統計と classifier コストの最適化(sticky と cascade の実測比較)、そしてコードに眠っていた RouteLLM 統合まわりを掘っていきたいところです。


国内企業 AI活用実態調査2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

国内企業 AI活用実態調査2026

無料でダウンロードする

この記事をシェアする

関連記事