Claude CodeでローカルLLMを使う

## はじめに
事の発端は、X（旧Twitter）で見かけたこちらのポストでした。
https://x.com/millionbiz_/status/1930298478347071975

AnthropicのIDE拡張である `Claude Code` は非常に便利ですが、当然出費がかさみます。
そんな中、Claude Codeのリクエストを他のLLMプロバイダーに中継できるという夢のようなライブラリ、**`claude-bridge`** を発見。早速GPUサーバー上にローカルLLM環境を構築し始めました。
* **`claude-bridge` GitHubリポジトリ:** https://github.com/badlogic/lemmy/tree/main/apps/claude-bridge

本記事では、その `claude-bridge` を使うという目的のために、まず高性能な推論エンジン **vLLM** のセットアップに挑戦し、数々のエラーと格闘した末、最終的に **Ollama** にたどり着くまでの試行錯誤の記録をまとめます。相棒であるgemini 2.5 proと二人三脚で構築しましたが、案外時間がかかってしまいました。

`claude-bridge` とローカルLLMの連携でハマっている方や、これからGPUサーバーで環境構築をしようとしている方の助けになれば幸いです。

**本記事の登場人物:**

* **vLLM:** 高速なLLM推論とサービングのためのライブラリ
* **Ollama:** ローカルでLLMを簡単に実行するためのプラットフォーム
* **uv:** Rust製の超高速なPythonパッケージインストーラ
* **Docker:** コンテナ型仮想化ツール
* **claude-bridge:** 手元のマシンからLLMを利用するためのCLIツール

---

## フェーズ1: vLLM + uv + Docker 環境の構築

最初の目標は、高速なvLLMを、モダンなuvを使い、Dockerコンテナ内で動かすことでした。サーバー上にuvをインストールしたくなかったのでDocker on uvの構成にしました。
初めてuvを使ったのですが、確かにインストールが速かった...かも

### Dockerfileの作成とビルド

まずは以下のようなDockerfileを作成しました。

```dockerfile
# ベースイメージを選択
FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04

# 環境変数
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV PYTHON_VERSION=3.10
ENV PATH="/root/.local/bin:/opt/venv/bin:${PATH}" # uv と venv のパス
ENV VENV_PATH="/opt/venv"

# 必要なパッケージのインストール
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python${PYTHON_VERSION} \
    python${PYTHON_VERSION}-dev \  # 最終的に必要になった開発用ヘッダ
    python${PYTHON_VERSION}-venv \
    python3-pip \
    curl \
    git \
    build-essential \ # 念のため
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Pythonのデフォルトを更新
RUN update-alternatives --install /usr/bin/python python /usr/bin/python${PYTHON_VERSION} 1 && \
    update-alternatives --set python /usr/bin/python${PYTHON_VERSION}

# uv のインストール
RUN curl -LsSf [https://astral.sh/uv/install.sh](https://astral.sh/uv/install.sh) | sh

# 仮想環境の作成
RUN uv venv ${VENV_PATH} --python $(which python)

# PyTorch と vLLM のインストール
RUN export VIRTUAL_ENV=${VENV_PATH} && \
    uv pip install --no-cache-dir torch torchvision torchaudio --index-url [https://download.pytorch.org/whl/cu121](https://download.pytorch.org/whl/cu121) && \
    uv pip install --no-cache-dir vllm

# 作業ディレクトリの設定とポート開放
WORKDIR /app
EXPOSE 8000

# デフォルトコマンド
CMD ["python", "-m", "vllm.entrypoints.openai.api_server", \
     "--model", "mistralai/Mistral-7B-Instruct-v0.1", \
     "--host", "0.0.0.0", \
     "--port", "8000" \
    ]
```

しかし、このDockerfileが完成するまでには、数々のエラーとの戦いがありました。

#### エラー1: `uv: not found`

ビルドの途中で、`uv` コマンドが見つからないというエラーに遭遇。

* **原因:** `uv` のインストール後、そのパスが後続の `RUN` 命令の実行コンテキストに引き継がれていなかった。
* **解決策:** `uv` のインストール先である `/root/.local/bin/` を明示的に指定することで解決。
    ```dockerfile
    # RUN uv venv ... を以下のように修正
    RUN /root/.local/bin/uv venv ${VENV_PATH} --python $(which python)
    ```
    最終的には、`ENV PATH="/root/.local/bin:${PATH}"` の設定でこの問題は解消されました。

#### エラー2: `/opt/venv/bin/uv: not found`

仮想環境作成後、仮想環境内の `uv` を使ってパッケージをインストールしようとして失敗。

* **原因:** `uv venv` で仮想環境を作成しても、`uv` コマンド自体は仮想環境内にはコピーされない。グローバルにインストールされた `uv` を使う必要があった。
* **解決策:** `VIRTUAL_ENV` 環境変数を設定し、グローバルな `uv` (`/root/.local/bin/uv`) を使うことで、指定した仮想環境にパッケージをインストールするように修正。
    ```dockerfile
    RUN export VIRTUAL_ENV=${VENV_PATH} && \
        /root/.local/bin/uv pip install ...
    ```

#### エラー3: `fatal error: Python.h: No such file or directory`

70Bモデルのロード中に、vLLM (内部的にTriton) がC拡張をコンパイルしようとして失敗。

* **原因:** C拡張のコンパイルに必要なPythonの開発用ヘッダーファイル (`Python.h`) がコンテナ内に存在しなかった。
* **解決策:** Dockerfileの `apt-get install` に `python3.10-dev` を追加。
    ```dockerfile
    RUN apt-get update && \
        apt-get install -y --no-install-recommends \
        python${PYTHON_VERSION} \
        python${PYTHON_VERSION}-dev \  # <-- これを追加
        ...
    ```

#### エラー4: `RuntimeError: NCCL error: unhandled system error`

マルチGPU (`tensor_parallel_size=4` など) で起動しようとすると、GPU間の通信を担当するNCCLがエラーで停止。

* **原因:** Dockerコンテナのプロセス間通信(IPC)や共有メモリの設定が、マルチGPUの高度な通信には不十分だった。
* **解決策:** `docker run` コマンドに `--ipc=host` と `--shm-size=16g` (またはそれ以上) を追加。
    ```bash
    docker run \
      --gpus all \
      --ipc=host \
      --shm-size=16g \
      ...
    ```

#### エラー5: モデルダウンロードのタイムアウト (`IncompleteRead`, `Read timed out`)

巨大な70Bモデルのダウンロード中に、ネットワークがタイムアウトしてコンテナが起動に失敗。

* **原因:** コンテナ内からのHugging Face Hubへのネットワーク接続が不安定、または時間がかかりすぎた。
* **解決策:** **モデルの事前ダウンロード戦略**を採用。
    1.  ホストマシンに `huggingface-hub` ライブラリをインストール (`pip install huggingface-hub`)。
    2.  以下のPythonスクリプトをホストで実行し、モデルファイル一式をDockerボリュームとしてマウントするディレクトリ (`~/vllm/huggingface_cache`) にダウンロード。
        ```python
        from huggingface_hub import snapshot_download
        import os

        model_id = "tokyotech-llm/Llama-3.1-Swallow-70B-Instruct-v0.1"
        cache_root_dir = os.path.expanduser("~/vllm/huggingface_cache")
        os.environ['HF_HOME'] = cache_root_dir

        print(f"Downloading model {model_id} into: {cache_root_dir}")

        snapshot_download(
            repo_id=model_id,
            local_dir_use_symlinks=False,
            resume_download=True,
            max_workers=8,
            etag_timeout=120, # タイムアウトを延長
        )
        print("Download complete!")
        ```
    3.  このキャッシュディレクトリをマウントして `docker run` を実行。これにより、コンテナ起動時のダウンロード処理が不要になった。

---

## フェーズ2: `claude-bridge` との連携

vLLMサーバーの起動に成功したところで、次に手元のマシンから `claude-bridge` を使って接続を試みました。

### SSHポートフォワーディング

手元のマシンからGPUサーバーへの接続にはSSHローカルポートフォワーディングを利用。手元のポート `8001` をサーバーのポート `8000` に転送します。

`~/.ssh/config` に追記:
```ssh
Host gpu**
  HostName gpu***
  User sample
  LocalForward 8001 localhost:8000
```
その後 `ssh gpu**` で接続。

### `claude-bridge` でのハマりポイント

* **問題1:** `API key not found` -> `--apiKey "dummykey"` で解決。
* **問題2:** `claude-bridge` が `--baseURL http://localhost:8001/v1` を無視してAnthropic APIに接続してしまう。
    * `--debug` オプションでログ (`.claude-bridge/requests-*.jsonl`) を確認したところ、リクエスト先が `https://api.anthropic.com` になっていることが判明。
* **原因切り分け:** モデル名に `/` (スラッシュ) を含む場合 (`tokyotech-llm/Llama-3.1-Swallow-70B-Instruct-v0.1`) にこの問題が発生し、スラッシュなしのモデル名 (`Llama-3.1-Swallow-70B-Instruct-v0.1`) にすると `--baseURL` は機能するが、今度はvLLM側が「そのモデルは存在しない」と404エラーを返すジレンマに。

### vLLM側での解決策

このジレンマを解決するため、vLLMの起動オプション `--served-model-name` を利用しました。

```bash
docker run \
  ...
  /opt/venv/bin/python -m vllm.entrypoints.openai.api_server \
  --model tokyotech-llm/Llama-3.1-Swallow-70B-Instruct-v0.1 \   # 実際のロードはこのID
  --served-model-name Llama-3.1-Swallow-70B-Instruct-v0.1 \    # APIで公開する名前 (スラッシュなし)
  ...
```

これで、`claude-bridge` からはスラッシュなしのモデル名でアクセスでき、vLLM側もリクエストを正しく受け取れるようになりました。

しかし、それでも `claude-bridge` 側で `400 Bad Request` が発生。vLLMのログを見ると、`/v1/chat/completions` へのリクエスト形式に問題があるようでした。色々調査しましたが、原因がわからずvLLMの使用はここで一旦断念することにしました。

---

## フェーズ3: Ollamaへの移行 - シンプルさは正義

vLLMのセットアップとクライアントとの連携の複雑さから、より手軽に利用できる **Ollama** に切り替えることにしました。

### Ollamaの導入手順 (Docker)

1.  **データ用ディレクトリ作成:**
    ```bash
    mkdir -p ~/ollama_data
    ```

2.  **Ollamaコンテナ起動:**
    ```bash
    docker run -d \
      --gpus all \
      -v ~/ollama_data:/root/.ollama \
      -p 11434:11434 \
      --name ollama_server \
      ollama/ollama
    ```
    vLLMであれほど苦労したマルチGPU設定や共有メモリの問題が、このコマンド一発であっさりと解決しました。

3.  **モデルのプル:**
    コンテナ起動後、使いたいモデルをプルします。
    ```bash
    # 70Bモデルをプル
    docker exec -it ollama_server ollama pull llama3:70b
    ```

4.  **動作確認:**
    `curl` で簡単に動作確認ができます。
    ```bash
    curl http://localhost:11434/api/generate -d '{
      "model": "llama3:70b",
      "prompt": "日本の首都はどこですか？",
      "stream": false
    }'
    ```

5. Claude-bridge で使ってみる。
   vLLMと同じようにSSHポートフォワーディングを設定した後に以下を実行しました。
    ```bash
    claude-bridge openai llama3.3:70b --baseURL http://localhost:11434/v1
    ```
    ログを残し忘れましたが、これでOllamaでホストされたローカルLLMをClaude Codeで使用することができました。


---

## まとめ

* **vLLM** は間違いなく高性能ですが、その能力を最大限に引き出すには、Docker、CUDA、NCCL、Pythonのビルド環境など、低レイヤーの知識と細かなチューニングが求められます。
* **Ollama** は「とりあえず動かす」までの手軽さが圧倒的で、ローカルLLMの入門や、複雑な設定を避けたい場合に最適の選択肢です。
* `--ipc=host` や `--shm-size` は、DockerでマルチGPUを利用する際の「おまじない」として覚えておくと良さそうです。
* `--served-model-name` のようなオプションは、クライアントとの互換性を保つ上で非常に有用な機能だと知りました。

最終的に `claude-bridge` との連携は断念しましたが、vLLMとOllama、両方のセットアップを経験できたのは大きな収穫でした。これからローカルでLLM環境を構築する方にとって、この奮闘記が少しでも参考になれば嬉しいです。

参考文献
- https://x.com/millionbiz_/status/1930298478347071975
- https://github.com/badlogic/lemmy/tree/main/apps/claude-bridge