見出し画像

Ollamaなしで複数GGUFを自動で入れ替える(コピペで動くローカルLLM切替) - CodeRouter v2.9.2

2026/7/12 23時現在 v2.9.3までリリース済みなのでテストはできます。

llama.cppを直に使っていると、ひとつ困ることがある。1プロセス=1モデルなので、別のモデルを使いたくなるたびに今のllama-serverを止めて、別のGGUFを指定して起動し直す。これがだるい。

Ollamaを使えばこの手間はない。呼んだモデルが勝手に立ち上がって、しばらく使わなければ勝手に片付く。でもOllamaを挟みたくない、生のllama.cppでGGUFを直に叩きたい、という人も多いと思う。自分もそうだった。

この「呼んだモデルだけ立ち上げて、放っておいたら片付ける」をllama.cppでやる先行ツールにllama-swapがある。よくできていて実際に人気もある。今回はそれとは別のアプローチとして、自分が作っているOSS「CodeRouter」に v2.9.1(2026-07-12)で入れた`launcher.swap`という機能で同じことをやる(この記事の構成はv2.9.2以降が前提)。作者が自分の道具を紹介する記事なので、そこは正直に書いておく。

何ができるか

やることは単純だ。OpenAI互換のリクエストが来たら、その`model`名を見て、まだ起動していないllama-serverを自動で立ち上げる。ロードが終わるまでリクエストは保留するので、初回でも失敗にはならない。

そして一定時間そのモデルを使わないでいると、自動でアンロードしてメモリをゼロに戻す。この待ち時間は設定で決められる。

手元のM3 Max(Metal)で、350MBの小型モデル(LFM2.5-350M)で測ったときの挙動はこんな感じだった。

  • コールドスタートは2秒くらい

  • 2回目以降は待ちゼロ(もう起動済みなので)

  • アイドル25秒に設定したら、放置でちゃんと自動で片付いた

モデルが大きくなればコールドスタートはロード時間ぶん伸びる。数秒から数十秒、サイズ次第だ。あと、起動できるのは設定ファイルに書いたモデルだけ。任意のパスを勝手に起動したりはしない仕様になっている。

動かす前提

読者側で必要なものはこれだけ。

  • llama.cppがビルド済みで、`llama-server`が動く状態(例: `~/llm/apps/llama.cpp/build/bin/llama-server`。PATHに通っていれば設定でパスを省ける)

  • GGUFファイルが1つのフォルダに置いてある(例: `~/models/`)

  • Python 3.12以上

  • macOS(Metal)でもLinuxでもOK(自分の実測はmacOS)

Ollamaは要らない。ここからはコピペで進む。

インストール

まずCodeRouterを入れる。pipでもuvでもいい。

pip install "coderouter-cli>=2.9.2"
# または uv:  uv tool install coderouter-cli
coderouter --version   # 2.9.2 以上ならOK

`2.9.2`以上が出れば準備完了。`launcher.swap`自体はv2.9.1からあるが、この記事のシンプルな設定が書けるのはv2.9.2以降だ。

設定ファイルを書く

`~/.coderouter/providers.yaml`を用意する。下の内容をそのまま貼って、モデル名とファイルパスだけ自分のGGUFに書き換える。

default_profile: auto

auto_router:
  # カタログ外の model 名が来たときの行き先(好みのモデルに)
  default_rule_profile: launcher-swap-qwen3-4b
  rules: []

launcher:
  model_dirs:
    - ~/models
  backends:
    llama.cpp:
      binary: ~/llm/apps/llama.cpp/build/bin/llama-server   # PATHにあれば省略可
  swap:
    enabled: true
    ttl_seconds: 300          # アイドル5分で自動アンロード
    models:
      - name: qwen3-4b
        backend: llama.cpp
        model_path: ~/models/Qwen3-4B-Q4_K_M.gguf
        port: 18081
      - name: gemma-4-12b
        backend: llama.cpp
        model_path: ~/models/gemma-4-12B-Q4_K_M.gguf
        port: 18082

`qwen3-4b`と`gemma-4-12b`はあくまで例なので、手持ちのGGUFに置き換えてもらいたい。ポートは固定しておくのがおすすめ。

見ての通り、swapで使うモデルは`launcher.swap.models`に並べるだけでいい。それ以外のバックエンド(Ollamaやクラウド)も併用したい場合は、通常どおり`providers`/`profiles`を足せば同居できる。

起動する

設定を指定してサーバーを立てる。

coderouter serve --config ~/.coderouter/providers.yaml --port 8088

この時点ではまだどのモデルもロードされていない。リクエストが来て初めて対象のllama-serverが立ち上がる、という動きになる。

使ってみる

OpenAI互換のエンドポイントに投げる。初回だけロード待ちが入るが、失敗にはならず、ロードが終わってから応答が返る。

curl http://localhost:8088/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"qwen3-4b","messages":[{"role":"user","content":"こんにちは"}]}'

これでqwen3-4bが立ち上がって返事が返ってくる。

別モデルに切り替える

切替は拍子抜けするほど簡単で、`model`の値を変えるだけ。さっきのcurlを`gemma-4-12b`にすれば、今度はそっちが自動で立ち上がる。プロセスを止めたり起動し直したりは一切しない。

Anthropic互換のエンドポイントも同じ設定のまま使える。

curl http://localhost:8088/v1/messages \
  -H 'Content-Type: application/json' \
  -H 'anthropic-version: 2023-06-01' \
  -d '{"model":"qwen3-4b","max_tokens":128,"messages":[{"role":"user","content":"こんにちは"}]}'

Claude Codeから使う — 実運用の形

curlで仕組みが分かったら、実際の使い道はこっちが本命だと思う。Claude Codeの接続先をCodeRouterに向けると、ローカルのGGUFがClaude Codeの会話相手になる。毎回環境変数を打つのは面倒なので、エイリアスにしておく。

alias claude-local='ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude'
claude-local --model qwen3-4b

コンソールの見え方は普段のClaude Codeと何も変わらない。違いに気づくのは2箇所だけ。`/status`のモデル表示が`qwen3-4b`になっていることと、最初のメッセージだけ応答が始まるまで待たされることだ(裏でllama-serverがGGUFをロードしている。画面上はスピナーが回っているだけ)。2回目からは普通に返ってくる。裏側が見たければ、serveのログか`http://localhost:8088/dashboard`を開いておくと、リクエストが来て起動→登録される流れがそのまま見える。

セッションの途中でモデルを替えたくなったら、スラッシュコマンドで切り替える。

/model gemma-4-12b

次の発言からgemma-4-12bが自動で立ち上がって応答する(こちらも初回はロード待ち)。もし手元のバージョンの`/model`ピッカーがカスタム名を受け付けなかったら、一度抜けて`claude-local --model gemma-4-12b`で起動し直せばいい。

実運用パターン1: 軽い/重いを手で使い分ける

日常の相談や下書きは軽い4Bで回して、込み入った依頼のときだけ`/model`で12Bに上げる、という使い方。切替のたびにプロセスを立て直す作業はもう無いし、使わなくなった側はTTLで勝手に消えるので、「さっき使った重いモデルがメモリを占領し続けている」ことも起きない。合計がメモリに載る範囲なら、載せ替えのことは忘れていられる。

実運用パターン2: サブエージェントだけ別モデルに振る

もう少し凝った形。Claude Codeのサブエージェント定義(`.claude/agents/*.md`)はfrontmatterでモデルを指定でき、この名前がそのままリクエストに載ってCodeRouterに届く。つまりカタログのモデル名を書くだけで、そのサブエージェントの呼び出しだけ別のGGUFに振れる

---
name: reviewer
description: コードレビュー担当。use proactively after code changes.
model: gemma-4-12b
---
あなたはコードレビュー担当です。変更点を読み、問題点を指摘してください。

メインの会話は軽いモデルのまま、「reviewerにレビューさせて」と頼んだときだけ12Bが自動で立ち上がり、終わってしばらくすればTTLで消える。呼んだ道具だけ出てきて勝手に片付く、という感覚に近い。

Claude Code運用の注意

  • この記事の設定では、カタログ外のmodel名は全部`default_rule_profile`の行き先(qwen3-4b)に落ちる。なので`--model`を付けなくても一応繋がるが、どのモデルと話しているかは自分で選んで明示するほうが事故がない

  • チャットは動くが、Claude Codeの本領であるツール操作(Bash/Edit/Task)は、そのGGUFとllama.cpp側のtool calling対応に依存する(この組み合わせでのツール往復は自分はまだ検証していない)。まず素朴な質問で疎通を確かめてから、ツールを使う指示を試す順がおすすめ

  • Claude Codeは毎ターン大きめのシステムプロンプトを送るので、小型モデルではprefillの待ちが体感に乗る。会話の相棒には応答速度重視のモデルを選ぶと快適だ

動きを覗く

本当に自動で立ち上がって自動で消えているのか、`lsof`で見てみる。

lsof -i :18081          # モデル使用中はLISTEN、TTL後は消える

モデルを使っている間はLISTENしていて、TTLが切れると消える。serveのログにも判断の材料が出ていて、見どころは次の3つ。

  • `auto-router-resolved` + `rule_id: "swap:qwen3-4b"` — model名からルーティング先を決めたところ

  • `launcher provider sync: registered ...` — ロード完了(readiness確認後)に登録されたところ

  • `swap-unload` + `reason: "ttl"` — アイドルで自動アンロードされたところ

ログを追うと、リクエストが来てからルーティング決定→起動→登録、放置でアンロード、という一連の流れがそのまま追える。

仕組み

中で何をしているかを一段落で。CodeRouterはOpenAI/Anthropicの両互換をしゃべるローカルLLMルーターで、`launcher.swap`はカタログに宣言したモデル名ごとにルーティングルールとプロファイルを自動生成する。リクエストが届くとllama-serverを子プロセスとして起動し、`/health`で応答可能になったのを確認してからリクエストを流す。

同じモデルに同時にリクエストが来ても起動は1回だけだし、ストリーミング応答の途中でアンロードされることもない。外部依存も増えていなくて、コアの依存は5個のまま。仕組み自体は素直だ。

llama-swapとの対応と、正直な制約

llama-swapを使ったことがある人向けに、対応関係と現状できないことを正直に書いておく。まず対応するところ。

  • llama-swapの`ttl` → `ttl_seconds`(こちらはグローバルに1値)

  • モデルカタログ(cmd/proxy) → `models:`(コマンドの組み立てはlauncher側が担う)

次にまだ埋まっていないところ。llama-swapの`groups`(排他ロードや常駐の制御)は未実装で、Phase 2で入れる予定だ。今は「使ったぶんだけ立ち上がって、TTLで各自消える」方式で、同時ロード数の上限を制御する仕組みはない。

ここは大事な注意点で、上限がないということは、次々と別モデルを叩けばその全部がメモリに載りうるということ。TTLで抜けるまでは残るので、メモリに載る範囲でカタログを組んでおくこと。メモリ会計のような賢い制御はまだ入っていないと思ってもらっていい。

llama.cpp本体のrouter modeという手もある

もうひとつ正直に併記しておく。実はllama.cpp本体にもrouter mode(`--models-dir`/`--models-max`、2025-12以降)がある。追加のツールを入れなくても、ディレクトリを指定すれば似たようなモデル切替ができる。

なので「とにかく追加ツールなしで済ませたい」なら、まずはそっちを試すのもありだ。ただTTLでの自動アンロードのような寿命の制御はまだ弱いので、そこを重視するなら今回の`launcher.swap`のほうが手に馴染むと思う。どちらもGGUFを直に使う点は同じで、Ollamaは挟まない。

使ってみて

`enabled: true`を書かない限り何も変わらないopt-inの機能なので、既存の使い方を壊さずに気軽に試せる。自分の環境では、モデルを切り替えるたびにターミナルでプロセスを止めて立て直していた手間が、curlのmodel名を書き換えるだけになった。地味だけど効く。

リポジトリとパッケージはこちら。MITなので中身も見てもらえる。

あなたはローカルLLMのモデル切替、今どうしてる? Ollama、llama-swap、それとも生のllama.cppを手で回している? もし手で回してだるさを感じているなら、この記事のyamlをコピペして試してみてほしい。

#ローカルLLM #llamacpp #Ollama不要 #LLM #GGUF #CodeRouter #llamaswap #ClaudeCode #生成AI #オープンソース

いいなと思ったら応援しよう!

zephel01 サーバー代とコーヒー代になります☕ 役に立ったら応援よろしくお願いします!