[[models]] エントリは通常ひとつの宛先を指定します。ルーターを使うと、広告する id
ひとつに代わりに決めさせることができます。エントリに [models.router] テーブルを置き、
その type キーがルーティングアルゴリズムを選び、アルゴリズムがリクエストごとに宛先を
選びます。委譲された作業は、さらに
[models.subagents] オーバーレイを通じて別のターゲットへ送る
こともできます。
これはオプトインで、既定では何もしません。どちらのテーブルもどこにも設定しなければ、
shunt はこれまでどおりリクエストの model id だけを見てルーティングします。
共通の形
どのルーターも、[[models]] エントリ 1 つと [models.router] テーブル 1 つです。
[[models]]
id = "claude-auto" # /model に出すには claude/anthropic で始める必要があります
display_name = "Auto (stage router)"
[models.router]
type = "stage_router"
capable_target = "claude-opus-4-8"
efficient_target = "claude-sonnet-4-6"ターゲットは普通の公開モデル id です。 ルーターの出力は shunt の通常のルーティング
階段に再び入り、各ターゲットは自分のフェイルオーバーチェーン、アカウントプール、アダプター、
effort、service_tier をそのまま保ち、追加のコードなしに /v1/models のディスカバリーに
現れます。ターゲットがまたルーターであれば起動エラーなので、解決は深さカウンターではなく
構造的に 1 ホップです。
クライアントは今も自分が要求した id を見ます。 選ばれたターゲットは upstream_model に
乗って上流へ行くだけです。レスポンスは要求された id を報告するので、Claude Code の
/model 表示と --resume のモデル復元はそのまま動きます。代わりに判断を報告するのは 2 つの
レスポンスヘッダー — x-gateway-routed-model(ルーターが選んだターゲット)と
x-gateway-route-source(それを選んだ理由)— で、どのルーターの type でも付きます。詳しくは
ステージルーターガイドで説明します。
ルーターテーブルがなければ何も変わりません。 [models.router] テーブルがなければ、
ルーターの分岐は既存のデプロイが持っていない設定フィールドに対する Option チェック 1 つ
です。ルーティング経路について変わるものはなく、ルーター付きのエントリがどの type を
宣言していてもこのチェックのコストは同じです。
同じエントリに [models.router] と [models.upstream_model] を併記することはできません。
すべてのキー、既定値、検証規則は
設定リファレンスにあります。
ルーターの type を選ぶ
| ルーター | 使いどころ | 宣言の仕方 | ジャッジを呼ぶか | 提供前にターンを留め置くか | ガイド |
|---|---|---|---|---|---|
auto |
調整なしで、上流のプリセットのままステージルーターを使いたい | type = "auto" |
いいえ — プリセットにジャッジはありません | いいえ | 下記 |
stage_router |
直近の tool-result 履歴に、ターンごとに強力なティアか効率的なティアかを選ばせたい | type = "stage_router" |
任意の [models.router.classifier] を置いたときだけ、しかもシグナルが決着させられなかったターンでだけ |
いいえ | ステージルーター |
llm_classifier、capability |
タスクの解決確率から、ジャッジに強力なターゲットと弱いターゲットのどちらかを選ばせたい | type = "llm_classifier"、mode = "capability" |
はい、classify_trigger が許す範囲で |
いいえ | LLM 分類器 |
llm_classifier、custom |
自分で書いたプロンプトとスキーマに、2 つ以上のモデルグループから選ばせたい | type = "llm_classifier"、mode = "custom" |
はい、classify_trigger が許す範囲で |
いいえ | LLM 分類器 |
llm_classifier、escalation |
各セッションを効率側のターゲットで始め、完成した作業が行き詰まっているとジャッジが判定し続けたら強力なターゲットに固定したい | type = "llm_classifier"、mode = "escalation" |
はい、固定前のすべてのターンで | はい、固定前は | エスカレーション |
composite |
ステージルーターが fall-open するティアをジャッジに決めさせ、シグナルの採点には手を触れたくない | type = "composite" |
はい、user_turn または new_session ごとに |
いいえ | コンポジット |
advisor |
1 つの実行モデルにすべてのターンを提供させつつ、より強力なレビュアーにその締めくくりのターンを承認させるか差し戻させたい | type = "advisor" |
はい — レビューごとに、セッションあたり最大 max_reviews 回 |
はい、セッションにレビューの予算があるあいだは | advisor ゲート |
[models.subagents] オーバーレイ |
委譲された作業を親セッションとは別のターゲットへ送りたい | type = "passthrough" または type = "llm_classifier" の [models.subagents] |
llm_classifier 形式でだけ、(session, agent) ごとに一度 |
いいえ | サブエージェントルーティング |
random |
ターゲット間を重み付きで分けたい — カナリア | type = "random" |
いいえ | いいえ | 下記 |
noop |
トークンを使わずにクライアントの配線をスモークテストしたい | type = "noop" |
いいえ | いいえ | 下記 |
prefill_router |
学習済みの分類器に直近のユーザーターンを採点させたい(ソースからのビルドが必要) | type = "prefill_router" |
いいえ — モデルはプロセス内で動きます | いいえ | 下記 |
上流 Switchyard のルートタイプのうち 2 つには、shunt に対応するものがありません。
plan_execute は実装していません。上流の passthrough ルートタイプ — ターゲット 1 つで、
ルーティングの判断をしないもの — も、ここではルーターの type ではありません。
[models.router] テーブルを持たない素の [[models]] エントリがすでに固定モデルの場合に
あたり、上流の「passthrough with subagents」はまさに
[models.subagents] オーバーレイを載せたそうしたエントリです。
GET /routes は設定済みのルーターを routers にすべて載せ、その algorithm と targets を
示します。ジャッジを設定したルーターは、それを judges にも示します。
shunt.router.decisions はすべてのルーターの type の決定を数えます。
auto
上流のステージルータープリセットです。picker = "efficient_first" と
confidence_threshold = 0.5 を使い、他のステージキーはすべて shunt の既定値になります。
type のほかは 2 つのターゲットしか受け付けません。ジャッジを含め、他のステージキーを
設定したい場合は type = "stage_router" を使ってください。
[models.router]
type = "auto"
capable_target = "claude-opus-4-8"
efficient_target = "claude-sonnet-4-6"random
2 つ以上のターゲットに重みを付けてトラフィックを分けます。カナリア用です。既定では Claude Code のセッション 1 つが同じ枝に留まります。
[models.router]
type = "random"
targets = ["claude-sonnet-4-6", "gpt-5.6-terra"]
weights = [9, 1]affinity = "session"(既定値)では、枝は sha256(seed ‖ model ‖ セッション id) を重みの
範囲に写した値です。何も保存しないので再起動しても枝は変わらず、同じ設定を読み込んだ
レプリカ間でも同一です。そのかわり seed、targets、weights を変えると枝が動くことが
あります。x-claude-code-session-id を送らないリクエストは 1 つの枝を共有せず、その
リクエストだけの重み付き抽選を行います。セッションを送らないクライアントでも 90/10 の分割は
90/10 のままです。affinity = "request" ではリクエストごとに抽選し、seed を設定すると
抽選列を再現できます。
セッションアフィニティはアクセス制御ではなく固定(stickiness)です。 セッション id は クライアントが決めるので、id を変えて再試行する呼び出し元は自分を望みの枝へ寄せられます。 それで守られるものはありません — どのターゲットも、その呼び出し元が名前で直接指定できる 公開モデル id だからです。アクセス制御は managed model ポリシーの仕事です。
リクエストボディのない面 — GET /routes、/v1/models ディスカバリ、shunt check — は、
重みが正の最初のターゲットを報告します。セッション id で枝が決まったターンはルートソース
random_session を、重み付き抽選で決まったターンは random を報告します。
noop
アップストリームをまったく呼ばずに応答します。呼び出し元と同じモードで、空の終端
アシスタントメッセージを返します。stream: true には妥当な SSE シーケンス
(message_start、stop_reason: "end_turn" を載せた message_delta、message_stop)を、
そうでなければ Message の JSON オブジェクト 1 つを返します。count_tokens は
input_tokens: 0 と答えます。このルートも他と同じく認証を通るので、認証の穴ではありません
— トークンを 1 つも使わずにインバウンド経路全体を確かめる、クライアント配線のスモーク
テストです。受け付けるキーは type だけです。
prefill_router
学習済みルーターです。上流自身の分類器が直近のテキストユーザーターンを採点してエントリの
ターゲットのひとつを選びます。判定のためにアップストリームのジャッジを呼ぶのではなく、
モデルをこのプロセス内で動かします。これは既定で無効な prefill-router カーゴ
フィーチャーを有効にしてビルドしたバイナリでのみ使え、このフィーチャーはリリース
バイナリにも Homebrew formula にも入っていません。フィーチャーのないバイナリはこの type を
指定した設定をロードできず、そのことを伝えるエラーを出します。実行時には torch、
transformers、numpy、accelerate を持つ Python と、自分で用意したルーター
チェックポイントも必要です。
キー、前提条件、アドミッションの規則、ルートソースは 設定リファレンスに、クレートを フィーチャーの後ろに置いて取り込んでいる理由は Switchyard 統合にあります。
ジャッジ呼び出し
ジャッジとは、ターンの行き先や、完成した作業が十分かどうかを尋ねられるモデルです。
尋ねるだけで提供はしません — クライアントに応答することはありません。ジャッジを呼ぶ形は
5 つあります。stage_router の
[models.router.classifier]、3 つの
モードすべての llm_classifier、
composite、advisor(レビュー 1 回が
ジャッジ呼び出し 1 回です)、そして classifier 形式の
[models.subagents] です。それ以外の type はジャッジを呼びません。
ジャッジは正解率を取り戻します — アップストリームの分類器併用構成が測ったものの実質的な 一部です — 代わりにレイテンシと、ジャッジのターゲット自身のプールのクォータを支払います。 次の規則はどのジャッジにも当てはまります。
- 呼び出し元のものではなく、自分のルートの資格情報で実行されます。 予約済みの
x-shunt-*スロットとcookie、authorization、x-api-key、anthropic-betaは 呼び出しの前にすべて取り除かれます。passthrough のアップストリームに解決される ジャッジのターゲットが起動エラーになるのはこのためです — passthrough は呼び出し元の 資格情報をそのまま転送するという意味ですが、取り除いたのがまさにその資格情報だから です。auth = "none"は受け付けられます — 認証なしで動くローカル・セルフホストの ジャッジはサポートされた構成です。呼び出しはそのターゲット自身のアカウントプールの クォータを消費するので、ジャッジには専用の[[models]]エントリを与えてください。 - アドミッションが先です。 ジャッジが呼ばれるのは、リクエストが認証を通ったあとだけ です。ジャッジを呼びうるターンでは、インバウンド認証は要求された id に加えてそのエントリが 指定しうるすべてのターゲットとジャッジを、それぞれのフェイルオーバーチェーン全体まで含めて 対象にします。そのため応答ターゲットがすべて passthrough のエントリに資格情報を注入する ジャッジを加えると、そうしたターンはクライアントの資格情報を要求するようになります。 認証に失敗したリクエストやポリシーが拒否したリクエストは、ジャッジ呼び出しを 1 回も 行いません。
- そのコストをどれだけ頻繁に払うかは
classify_triggerが決めます(このキーを取る type の場合)。毎ターン判定する(every_request)、人間のユーザーターンごとに判定して その間のツール呼び出しでは判定を保持する(user_turn)、あるいはセッションに一度だけ 判定する(new_session)、の 3 つです。 - どの呼び出しにも上限があります。 6 つのキー —
judge_timeout_ms、judge_max_response_bytes、max_judge_calls、そして 3 つのgated_*— がエントリの 行うすべての内部呼び出しに上限を課し、上限を超えるとアップストリーム呼び出しは キャンセルされます。max_judge_callsは(session, agent)ごとのジャッジ呼び出し数の 上限で、呼び出しを送るときに差し引くので、同時に走るターンでも超えられません。保持された 判定を再生するターンは呼び出しをしないので、拒否されることもありません。 呼び出しごとの上限を参照してください。 - ジャッジの失敗はリクエストを失敗させず、アルゴリズム自身の既定値に戻ります —
タイムアウトでも
400でも、サイズ超過やパースできない判定でも、予算切れでも同じです。 判定するターン 1 回につき、ジャッジ呼び出しはちょうど 1 回です。各 type の既定値はそれぞれの ページにあります。完成したターンを判定する 2 つの type は、代わりにそのターンを返します。 escalation は弱いターンを、advisorは実行モデルのターンを返します。 ただしエントリでfail_open = falseを設定した advisor は、リクエストを502で失敗させます。 count_tokensのプローブがジャッジを呼ぶことはありません。 セッションがすでに保持して いるターゲットがあればそれで応答します。そうでない場合に何で応答するかは、各 type の ページにあります。
各呼び出しは、決着した結果とともに shunt.router.judge_calls で数えられます。
留め置くターン
ここまでの形はどれも、ターンが実行される前にその行き先を決めます。実行されたあとで決める
形が 2 つあります。mode = "escalation" は効率側のターゲットの
完成したターンをジャッジに判定させ、advisor はより強力な
レビュアーに実行モデルの締めくくりのターンを承認させるか、差し戻させます。
どちらもすでに存在するターンを判定するので、shunt はそのターンを留め置きます。 留め置かれる(gated)ターン — 固定前の escalation の効率側ターン、またはレビュー予算が 残るセッションの advisor の実行モデルのターン — は、先に作ってバッファリングし、判定して から、はじめてクライアントに再生します。これはストリームをバッファリングしないという shunt のルールの唯一の例外で、この 2 つのエントリがゲートするターンにだけ当てはまります。これらの エントリのほかのターンと、ほかのすべてのルートのすべてのターンは、これまでとまったく同じ ようにストリーミングされます。
ターンを留め置くことで、アップストリームの設計が shunt の post-2xx 無再生ルールの内側に 収まります。再生が始まるまで応答ヘッダーを確定しないので、捨てられたターン — 強力な ターゲットに置き換えられたものも、REDO で差し戻されたものも — をクライアントが見ることは ありません。
呼び出し元のモードを保ちます。 stream: true の呼び出し元の留め置く呼び出しは
ストリーミングします。SSE フレームは届いた順に保持し、ターンを提供するときはバイトどおりに
再生します。stream: false の呼び出し元は、完成したターンの JSON メッセージを 1 つ
受け取ります。Anthropic アダプターはこの呼び出しをストリーミングせずに送ります。OpenAI Responses
アダプターは、どのフレーバーでもすべてのターンを上流へストリーミングで送り、同じ上限のもとで
そのストリームからメッセージを 1 つ組み立てます。ChatGPT/Codex バックエンドは非ストリーミングの
リクエストを拒否すると報告されていますが、標準の OpenAI Platform は受け付けるため、そちらでの
ストリーミングはコードパスを 1 つに保つための選択です。ゲートが変えるのは応答をいつ送るか
だけで、応答の形は変えません。再生される message_start.model は実行モデルの id ではなく
ルーター自身の id で、Anthropic の実行モデルでも OpenAI Responses の実行モデルでも同じです。
そのため Claude Code の /model 表示と --resume は要求した id を見ます。
完成したターンだけを提供します。 留め置いたターンは終端マーカーがあってはじめて提供
できます。ストリーミング呼び出しでは message_stop、ストリーミングしない呼び出しでは
1 つのメッセージとしてパースできる完全なボディです。切り詰められた 200 が再生されることは
ありません。ライブのストリームと同じく、ターンは message_stop フレームで終わります。その後に
届いたものは再生せず、その後で接続が切れても開いたままでも、ターンは打ち切られません。
留め置くターンは、ターゲットの順序付きフェイルオーバーチェーンをたどり、3 つの gated_*
呼び出しごとの上限が、留め置くターンの
大きさ、アイドル時間、長さを制限します。
コスト。 以下はエントリごとに選んで支払うコストです。
- 留め置くターンでは、ターン全体が完成して判定されるまでクライアントは何も受け取らない ため、最初のトークンまでの時間が最後のトークンまでの時間になります。
- escalation は固定前のすべてのターンでジャッジ呼び出しを 1 回行います。固定されるターンは、 捨てる効率側の呼び出しのコストも支払います。
- 捨てた効率側のターンや実行モデルのターンも、アップストリームのクォータはすでに消費して
います。クライアント自身の応答のディスパッチなので、
shunt.requestsではcaller="client"として数えます。
留め置いたターンが報告するルートソースと、留め置いたターン、そのアップストリーム、または そのジャッジが失敗したときの動作は 設定リファレンスに まとめてあります。
設計記録
意図的に除外したものを含むステージルーターの設計記録は
ADR-0004
にあり、[models.router] の判別子と残りのアルゴリズムについての記録は
ADR-0005
にあります。各アルゴリズムの出どころと、上流のベンチマークがそれについて何を言い、何を
言っていないかは Switchyard 統合にあります。