うちは Claude Code を社内開発の中心に据えていて、もうこれ無しでは仕事が回らないところまで来ています。とはいえ、エンタープライズ案件の現場で話を聞くと、一定の頻度でこういう声に当たります。
「外部 API は厳しいんですよね。社外秘データなので」
回線分離、コンプラ要件、契約上の縛り。理由はさまざまですが、要するに Anthropic に投げられない 現場はちゃんと存在します。
そんなとき頭をよぎるのが「Claude Code をローカル LLM で動かせないか」という案。Claude Code は中で Anthropic Messages API を叩いているだけなので、互換プロキシ + ローカル LLM という構成にできれば、理屈上は成立するはずなんですよね。
ちょうど Google が Gemma 4 を出して(2026-04-02 リリース、31B Dense は Arena でオープン #3)、H100 80GB を一日確保できる環境もあったので、本気で検証してみることにしました。
結論を先に言うと、動きます。ただし 動かすまでの儀式が思った以上に長い。記事の半分くらいは事件簿になります。
3 行サマリ
- 動く: vLLM + claude-code-router +
--bareClaude Code で、Gemma 4 31B Dense が tool-calling して 1 ファイル成果物を出すところまで到達した- 動かすまでが長い: 1 つ目のリクエストが通るまで API/設定エラーで何度も蹴られる(vLLM 引数・FP8 KV cache・tool-call parser・router の
max_tokenscap)。最終形に辿り着くまでの試行錯誤がそこそこ重い- 品質はまだ要監視: Gemma 4 が書いた JS は Bash heredoc 経由のバッククォート過剰エスケープで全滅。同じ仕様を本物 Claude Code (Opus 4.7) は 1 ターンで完走
全体の構成図
ざっくり、こういうレイヤ構成です。
text[ Claude Code CLI ] --bare / ANTHROPIC_BASE_URL=http://localhost:3456 │ Anthropic Messages API ▼ [ claude-code-router :3456 ] 形式変換 + maxtoken transformer │ OpenAI chat/completions ▼ [ vLLM 0.20.0 :8000 ] --enable-auto-tool-choice │ --tool-call-parser gemma4 │ --kv-cache-dtype fp8 ▼ [ Gemma 4 31B Dense (BF16) ] H100 80GB weights : 58.9 GiB KV cache: 11.4 GiB (FP8)
要は claude-code-router が主役です。これは Claude Code が叩く /v1/messages(Anthropic 形式)を受けて、OpenAI 互換 API を喋るバックエンド(今回は vLLM)に 形式変換して中継してくれる OSS のプロキシ。Claude Code 側は環境変数 ANTHROPIC_BASE_URL=http://localhost:3456 を向けるだけで、何も知らないまま「ローカルの Gemma 4」と会話できるようになります。
セットアップ手順(最終形)
最終的に動いたコマンド一式です。ここに辿り着くまでの試行錯誤のほうが正直面白いので、本筋を読みたい人は次のセクションへ。
bash# 1. 仮想環境
uv venv --python 3.11 .venv
source .venv/bin/activate
# 2. vLLM
uv pip install vllm
# 3. モデル取得 (HF_TOKEN 必須。約 58 GiB)
hf auth whoami
hf download google/gemma-4-31B-it
# 4. vLLM 起動 — 今回の記事の「最終地点」
VLLM_USE_DEEP_GEMM=0 \
vllm serve google/gemma-4-31B-it \
--dtype bfloat16 \
--kv-cache-dtype fp8 \
--gpu-memory-utilization 0.95 \
--max-model-len 40960 \
--limit-mm-per-prompt '{"image": 4}' \
--enable-auto-tool-choice \
--tool-call-parser gemma4 \
--port 8000 &
Router 側はこんな感じ。
bash# 5. claude-code-router
npm install -g @musistudio/claude-code-router
mkdir -p ~/.claude-code-router
cat > ~/.claude-code-router/config.json <<'JSON'
{
"APIKEY": "dummy-local-key",
"HOST": "127.0.0.1",
"PORT": 3456,
"Providers": [{
"name": "vllm-gemma4",
"api_base_url": "http://localhost:8000/v1/chat/completions",
"api_key": "not-needed",
"models": ["google/gemma-4-31B-it"],
"transformer": { "use": [["maxtoken", { "max_tokens": 8192 }]] }
}],
"Router": { "default": "vllm-gemma4,google/gemma-4-31B-it" }
}
JSON
ccr start
最後に「偽の Claude Code」を起動。
bash# 6. 偽 Claude Code
ANTHROPIC_BASE_URL=http://localhost:3456 \
ANTHROPIC_API_KEY=dummy-local-key \
ANTHROPIC_AUTH_TOKEN=dummy-local-key \
claude --bare --dangerously-skip-permissions --model claude-sonnet-4-6
これで Claude Code は Anthropic に何も投げず、すべて H100 上の Gemma 4 に流れます。--model の値は router 側の Router.default で潰しているので名目だけで OK。
ハマりどころ TOP 3
1. VLLM_USE_DEEP_GEMM=0 の儀式
最初の一手で、vLLM の warmup が RuntimeError: DeepGEMM backend is not available or outdated で死にました。
textdeep_gemm_warmup └ _count_warmup_iterations └ _fp8_linear_may_use_deep_gemm(m) # 全 Linear 層に対して呼ばれる └ get_mk_alignment_for_contiguous_layout()[0] └ RuntimeError
私は BF16 で動かしているのに、なぜ FP8 用の DeepGEMM が必須扱いになるのか。中身を読みに行ったところ、vLLM 0.20.0 の warmup ヘルパが「FP8 かどうか」を判定するためだけに DeepGEMM の関数を呼んでいて、未インストールだと例外で全体が落ちる らしい。BF16 ロードの Gemma 4 には FP8 層が一個もないので、本来不要なのですが。
幸い vllm/utils/deep_gemm.py が envs.VLLM_USE_DEEP_GEMM でゲートされていたので、VLLM_USE_DEEP_GEMM=0 を export してから vllm serve を叩けば回避できます。BF16 ロードしか考えていない人は、最初に必ず食らう類の地雷。
2. max_tokens=32000 vs max_model_len=40960 の絶妙な攻防
vLLM が無事に立ち上がって、ルーター越しの Hello World も「届いてるよ」と返ってきた。意気揚々と Claude Code をフル装備で起動 → また即死。
textThis model's maximum context length is 40960 tokens. However, you requested 32000 output tokens and your prompt contains at least 8961 input tokens, for a total of at least 40961 tokens.
1 トークンオーバー。
Claude Code はリクエスト時に max_tokens=32000(Sonnet 系のデフォルト出力上限)を投げてくるのですが、フル装備の Claude Code(システムプロンプト + 26 個のツール定義 + Skills + auto-memory)は 約 9000 トークン 食う。32000 + 8961 = ジャスト 40961。max が 40960 なので、1 トークン超過で蹴られます。
--max-model-len をさらに上げる手もありますが、KV cache メモリで頭打ちなので不可。--bare で装備を減らす手もありますが、検証としては歪む。
正解は router 側で max_tokens をキャップ することでした。claude-code-router には maxtoken という Provider transformer があって、これを Provider 設定に足すだけで、Claude Code が 32000 を投げてきても 8192 に書き換えてから vLLM に渡してくれます。
json"transformer": {
"use": [["maxtoken", { "max_tokens": 8192 }]]
}
これで context = 40960 - 9000 (system) - 8192 (output) = 23K の余裕。agentic loop で複数ターン回してもまだ余裕があります。
3. Gemma 4 が Bash heredoc で「気を利かせすぎる」
これが一番じわじわ効いてきます。
--bare で起動すると Claude Code のツールセットは Bash / Edit / Read の 3 つだけになって、Write が消えます。Gemma 4 は素直に cat << 'EOF' > clicker.html でファイルを書きにいったのですが、出てきた JS を見ると、こうなっていました。
jsmpsEl.textContent = \`自動増加: \${state.autoValue.toFixed(1)} / 秒\`;
clickCostEl.textContent = \`コスト: \${state.clickUpgradeCost}\`;
JS の template literal(バッククォート)を バックスラッシュでエスケープ してしまっている。
シングルクォート付きヒアドキュメント (<<'EOF') はリテラル展開なので、本来バックスラッシュは要りません。Gemma 4 は「シェルのコマンド置換と衝突するかも」と勘違いして、勝手にエスケープを入れてしまった可能性が高いです。HTML/CSS は正しく書けているので、初期スクショは普通に表示されてしまうのが意地悪なところ。
このバグは Sonnet/Opus では起きませんでした(彼らは Write ツールを優先するので Bash heredoc を回避する)。ローカル LLM 固有のリスク として記憶しておきます。
教訓: Write ツールを生かす のが大事。あるいは system prompt で「ヒアドキュメント内では引用符をエスケープしない」と明記する手もあります。
動かしてみた
ハマりどころを抜けたので、いよいよ本番。実演タスクは「シンプルな放置クリッカーゲーム(HTML 1 ファイル完結)」にしました。仕様はこんな感じ。
① タイトル + スコア表示 ② クリックでスコア +1 ③ 1 秒ごとに自動増加 ④ アップグレード 2 種(クリック単価 / 自動増加レート) ⑤ localStorage に永続化 ⑥ ダーク UI
全く同じ文面 を「偽 Claude Code (Gemma 4)」と「本物 Claude Code (Opus 4.7)」の両方に投入。出てきた clicker.html をそれぞれ Playwright で同一シナリオに通して比較しました(クリック → 待機 → アップグレード → リロード)。
Gemma 4 サイドの 5 枚
| 状態 | スクショ |
|---|---|
| 初期 | ![]() |
| 10 回クリック後 | ![]() |
| 5 秒経過後 | ![]() |
| アップグレード購入後 | ![]() |
| リロード後 | ![]() |
スコアが全部 0 のままです。先ほどのバッククォート問題で JS が 1 行も実行されておらず、HTML/CSS は綺麗なのに中身が完全に死んでいる、という状態。Playwright のコンソールにも pageerror: Invalid or unexpected token が並んでいました。
本物 Claude Code (Opus 4.7) サイドの 5 枚
| 状態 | スクショ |
|---|---|
| 初期 | ![]() |
| 10 回クリック後 | ![]() |
| 5 秒経過後 | ![]() |
| アップグレード購入後 | ![]() |
| リロード後 | ![]() |
スコア 10 → アップグレード Lv.1 購入 → リロードで Lv.1 復元、までを 1 ファイルで安定して通してきました。出力 HTML は localStorage quota の try/catch フォールバックや aria-live まで足してきていて、流石の安定感です。
数字サマリ
| 項目 | Gemma 4 31B-it (vLLM) | 本物 Claude Code (Opus 4.7) |
|---|---|---|
| Wall-clock 時間 | 58.3 s | 34.4 s |
| 会話ターン数 | 2 | 1 |
| ツール呼び出し回数 | 1 (Bash) | 1 (Write) |
| 出力 HTML 行数 / バイト | 247 / 7,401 | 305 / 8,882 |
| Playwright 5 項目通過 | 0/5 | 4/5 (1 は仕様上検証不可) |
vLLM のスループットは 約 40.7 tok/s(H100 単体、BF16 重み + FP8 KV cache)。TTFT は 0.43 秒。Sonnet 系の体感(250+ tok/s)と比べると明らかに遅いものの、ローカル実行・データ機密性を考えれば妥協可能なライン、というのが正直な肌感覚です。
で、結局どうなのか
「Gemma 4 で Claude Code が動くか」 → 動く。ただし
--bare必須、tool-calling は heredoc 経由になる。「実用に耐えるか」 → 単発の小さな成果物なら可。生成コードを必ず実行して検証する人間(or 自動テスト)がループに入っていれば、コスト 0 のローカル開発体験として実用域。一発で完成を期待する用途には未だ厳しい。
セットアップの儀式そのものは長いとはいえ、一度動かしてしまえば次からは即使える状態になります。エンタープライズ案件で「外部 API NG」のような現場なら、これだけで価値がある選択肢になります。
ただ、LLM 単体の信頼度 はまだ Anthropic 系には届かないので、自動検証ループ(Playwright や型チェック)と組み合わせて運用する のが現実解だと思います。今回みたいに「初期スクショだけ見ると動いていそう」という罠があるので、人間の目視だけだと痛い目に遭う。
まとめ
- Claude Code は
ANTHROPIC_BASE_URLを差し替えるだけで Gemma 4 (vLLM) でも動く - 最初の儀式は長い。vLLM 引数・KV cache サイズ・router の
max_tokenscap で連続して詰まる - ローカル LLM の生成物は Bash heredoc のエスケープなど、特有の罠で壊れる。
Writeツールを生かすか自動検証必須 - 単発タスクで 58 秒、40 tok/s。実用域には届く
- セットアップ後の電気代だけで Claude Code 体験。社外秘データ案件には強力な選択肢になる
ちなみに、私たちが提供している Deep Research Agent KumaKumaAI は、こうしたローカル LLM 接続にも対応しています。社外秘データやクライアント機密を扱う現場でも完全オンプレで Deep Research を回せるよう、今回みたいなローカルモデル相手のセットアップ漏れを吸収するチューニングまで含めて評価環境ごと提供しています。
Local LLMs aren't a downgrade — they're an integration problem.









