1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code Modsの自動モデル振り分けを、自分のトークン実測ログで検証してみた

1
Last updated at Posted at 2026-10-12

Claude Codeのサブスクを使っていて、「このタスクにOpusは要らないのに、デフォルトのまま投げてコストを溶かしている」と感じたことはないでしょうか。私は個人開発のAI業務基盤を1年以上回していて、タスクの種類ごとにモデル階層を変える運用を手でやってきました。最近、同じ課題をClaude Code純正の拡張機構で解決した記事を見つけたので、自分の実測ログと突き合わせて検証してみます。

元記事:「サブスクで使えるモデルルーター」

参考にしたのは moritalous さんの Claude Code ModsとJevでモデルルーターを構築(サブスクで使えるゾ!!) です。要点は次の2つでした。

  • Claude Code Mods というプラグイン機構を使い、プロンプトの難易度に応じてモデル・思考の深さを自動で振り分ける仕組みを作った
  • 当初は TypeSafe AI の構造化判定モデル「Jev」を分類器として組み込んでいたが、最終的には Claude Code 本体が持つ $.model.classify 機能だけで十分だと分かり、Jev無しの実装に落ち着いた

この「サブスク契約の中で、APIを別途叩かずにモデルを自動で切り替えられる」という点が差別化ポイントでした。私はAPI直叩きでのコスト最適化は散々やってきましたが、Mods経由でサブスクのまま振り分けるアプローチは試したことがなかったので、まずは中身を公式ドキュメントで裏取りしました。

Claude Code Modsの実物を公式リファレンスで確認する

Claude Code Mods は Anthropic 公式の Mods reference(2026年10月時点、Claude Code v2.1.290以降のAPIを前提とした版)に仕様が載っています。確認できたポイントは以下です。

  • Mod は hooks/hooks.json が指す1つのモジュールを持ち、そのモジュールが register(on, options) をエクスポートする。on でイベント名ごとにフック関数 ($, e, next) を登録する構造
  • $.model 名前空間には complete / fork / classify の3メソッドがある。classify はテキストと選択肢を渡して分類するための呼び出し
  • turn.step イベントは「1回分のリクエストがモデルに送られる直前」に発火し、next に { ...e, model } を渡せばそのリクエストだけモデルを変更でき、{ ...e, effort } を渡せばエフォート(思考の深さ)だけを変えられる
  • agent.spawn イベントでも同様に next へ { ...e, model } を渡すことで、サブエージェントに使うモデルを個別に指定できる

つまり元記事が組んでいたのは、「$.model.classify でプロンプトの難易度を判定し、その結果を使って turn.step で model か effort を切り替える」という2段構成です。モデルを丸ごと差し替えるだけでなく、モデルは固定したままエフォートだけ下げるという選択肢もAPI上は用意されている、というのは実際に公式リファレンスを読むまで知りませんでした。

Jevについても念のため TypeSafe AIの公式ドキュメントを確認しました。入力トークン課金・出力トークン無料で100万トークンあたり$0.042、レート制限は1秒あたり10万トークン/80リクエスト、コンテキストは1リクエストあたり64kトークン(state部分と最長の質問を合わせて32kトークンまで)、入力はテキストのみという仕様でした(確認日2026-10-10)。元記事がこれを分類器として使っていたのに、最終的に不要だったと書いているのは、タスクの分類程度ならClaude Code本体の $.model.classify で足りる、という実用上の判断として素直に読めます。

上記のAPI仕様に基づいて実装を試そうとしましたが、この環境ではClaude Codeのネイティブバイナリが入っておらず実行できていません。試しに動かそうとしたところ次のエラーになりました。

$ claude --version
Error: claude native binary not installed.

Either postinstall did not run (--ignore-scripts, some pnpm configs)
or the platform-native optional dependency was not downloaded
(--omit=optional).

このエラーは私の開発環境で以前から何度か出ている既知の制約で、今回も claude plugin test まで辿り着けませんでした。そのため、ここではコードを載せず、公式リファレンスから読み取れる組み立て方だけを書きます(未実行です)。

  • turn.step に渡される入力(TurnStepInput)には、ターンID・何回目のリクエストか・モデル・エフォート・メッセージ数・エージェントIDといった情報が入り、プロンプト本文は入っていません。そのため「turn.step の中でプロンプトを分類する」書き方はできず、分類はプロンプトを受け付ける側のイベントで行い、その結果を turn.step で参照する2段構成になります
  • turn.step はストリーミングのイベントで、フックは async 関数で next() の結果を返すのではなく、async generator として yield* next(...) で応答を中継する形が公式の書き方です

実際に書くときは、イベントごとの入力型とフックの書き方を Mods APIリファレンス で確認してから組んでください。

自分の実測ログと突き合わせる

ここからが本題です。私はClaude Code・他LLMの呼び出しをタスク種別付きでログに残しており、「モデル階層を下げるとコストはどれだけ下がるか」を何度か検証してきました。結論から言うと、モデルを安く振り分けるだけでは効かないタスクがあるというのが実測での発見です。

調査系のタスクでは、会話が長くなるほど毎ターン過去の履歴をまるごと再送信する cache_read の比率が支配的になり、モデル階層を1段安いものに切り替えても1件あたりのコストがほとんど変わらないケースがありました。モデルを切り替える前に「何を毎回送っているか」を見直さないと、ルーターを作っても効果が出ないタスクがある、というのが私の環境での観測です。

これは体験DBに残している過去の経験とも一致します。以前、あるMySQLサーバーのレスポンスが全体的に遅く、topで見るとCPU使用率は低いのにI/O waitだけが高いという状態に遭遇しました。最初はクエリ自体を疑いましたが、実際の原因はinnodb_buffer_pool_sizeが128MBのデフォルトのまま放置されていたことで、物理メモリ16GBに対して12GBまで増やす設定変更だけで、ディスクI/Oが80%減りクエリの平均レスポンスタイムが3倍改善しました。モデルの自動振り分けも同じで、仕組みを入れ替える前に基本設定を見直すだけで効くケースがある、というのは今回のcache_read比率の話と構図が重なります。

ルーティングの仕組み自体を評価する作り方にも、心当たりがあります。以前、技術選定で5つのフレームワークを比較する必要があったとき、要件をClaude Codeに渡して比較表を作らせたところ30分で完成しました。今回「元記事のMods+Jev方式」と「自分の実測ベースの手動ルール」を比べるのも、結局は同じ「候補を並べて判断材料にする」作業なので、下に比較表を作りました。

また、サブスクやSaaSの契約そのものを棚卸しする経験も活きています。以前、社内で使っているSaaSが35個に膨れていたのを調査し、3ヶ月以上ログインの無いものを解約して22個まで減らし、月額費用を45万円から28万円に落としたことがあります。今回の「サブスク内で振り分けを自動化する」という発想も、結局は契約を増やさずに既存の枠内でやりくりする発想で、相性の良い考え方だと感じました。

キャッシュ戦略についても1つ補足します。以前、ダッシュボードの集計クエリをRedisにキャッシュしたとき、効果を左右したのは「何をキャッシュするか」よりも「いつ無効化するか」の設計でした。モデルルーターも同じで、「どのタスクを安いモデルに回すか」の分類ロジックより、「分類がブレた時にどう戻すか」「効果が出ていないタスクをどう検知するか」の運用設計の方が、長く使う上では重要になりそうです。

比較表:元記事のアプローチ vs 自分の実測ベース運用

観点 元記事(Mods + $.model.classify) 私の実測ベース運用
振り分けの単位 プロンプト単位(turn.stepで1リクエストごと) タスク種別単位(開始前にモデル・エフォートを選ぶ)
判定方法 モデル自身に4段階で分類させる 過去の実測ログから「効くタスク/効かないタスク」を人が仕分け
効果の前提 分類が当たればコストが下がるという前提 コンテキスト量(特にcache_read比率)次第で効果が出ないタスクがある
サブスク内で完結するか する(API別契約不要) する(Max契約内の/model切り替えのみ)
自分で試せたか 未実行(環境制約でビルド不可) 実測ログは既存運用の実データ

元記事は「分類の自動化」が強みで、私は「どのタスクでなぜ効くか」を実測で把握できているのが強みという住み分けに見えます。両方を組み合わせるなら、cache_read比率が高い長時間タスクだけは分類をバイパスして固定ルールに落とす設計を次に試したいです。

まとめ

結局のところ、モデルルーターは「何を送るか」を削った後の二段目の最適化だと今回あらためて確認できました。Mods経由でサブスク内に実装できるのは便利ですが、分類ロジックを組む前に自分のタスクでcache_readがどれだけ支配的かを見ておかないと、振り分けを頑張っても財布への効果が薄いタスクに時間を使ってしまいます。次は実際にClaude Codeのネイティブバイナリが入った環境で、turn.stepフックを動かして分類の当たり外れを計測してみたいと思っています。


私はこの順番(送るものを削る→振り分ける)に落ち着きましたが、もっと良い検証の手順があれば知りたいところです。「うちはモデル振り分けより先にこれをやって効いた」という実測があれば、コメントで教えてもらえると助かります。

関連記事

追記(2026-10-15)

公開後の技術レビューで誤りが見つかったため、次の2点を訂正しました。

  • 当初掲載していた turn.step フックのコード例で、通常の async 関数から next() の結果を返していましたが、公式の書き方は async generator で yield* next(...) により応答を中継する形でした。コード例を削除し、組み立て方の説明に置き換えました
  • 同じコード例で turn.step の入力から e.text(プロンプト本文)を読んでいましたが、turn.step の入力にプロンプト本文は含まれません。分類はプロンプトを受け付ける側のイベントで行う、と説明を改めました
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?