mcpField
R
runpod-vllm-claude-code

runpod-vllm-claude-code

github.com/marks97クラウド・DevOps0874 回インストール4 週間前に更新
リポジトリ

runpod-vllm-claude-code について

RunPod で vLLM モデルを起動し、同梱のプロトコル変換プロキシ経由で Claude Code に接続するスキルです。モデルと GPU の確認、Pod の構成、安全な分離テスト、セッションの再開を案内します。

主な機能

  1. 01モデルサイズと GPU 要件の確認
  2. 02RunPod Pod の作成と検証
  3. 03Anthropic Messages API と vLLM の変換
  4. 04分離した Claude Code 実行の設定
  5. 05Pod の交換とセッション再開

活用シーン

  1. 01デプロイ前の GPU 要件の見積もり
  2. 02Claude Code から RunPod モデルを利用
  3. 03ツール呼び出しやコンテキスト制限の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marks97/runpod-vllm-claude-code --skill runpod-vllm-claude-code

インストール後、Claude Code で/runpod-vllm-claude-codeを入力して読み込まれたか確認します。

同梱ファイルreferences/transcript-surgery.mdscripts/anthropic-openai-proxy.js

詳細

RunPod vLLM + Claude Code ブリッジ

ここでは、2 つのことを同時に解決します。それぞれ何を解決するのか、明確にしておきましょう。

  1. RunPod でモデルを実行すること。 簡単です。GPU を選び、vLLM コンテナを起動して、ヘルスチェックが通るのを待ちます。
  2. Claude Code が実際にモデルと通信できるようにすること。 こちらは簡単ではありません。Claude Code は Anthropic の Messages API(/v1/messages、コンテンツブロック、SSE イベント)を使用します。vLLM が使用するのは OpenAI の /v1/chat/completions です。デフォルトでは、この 2 つを橋渡しするものはありません。ANTHROPIC_BASE_URL を使えば、Claude Code の接続先を Messages API の形式を実装する 任意の サーバーにできます。そのため、間に小さな変換プロキシを置くことで、「Claude Code の接続先を自分の RunPod インスタンスにする」ことが実際に可能になります。scripts/anthropic-openai-proxy.js がそのプロキシです。再試行に 1 時間を費やす原因になる、2 つのプロトコルの不一致はすでに修正済みです(下記の注意点を参照)。そのまま使用してください。一から作り直さないでください。

これは本質的に試験的で使い捨てのインフラです。GPU ポッドは作成した瞬間から、1 時間ごとに実際の料金が発生します。作成する前に、ユーザーがマシンをすでに指定して費用を了承している場合を除き、使用する GPU とおおよその料金($/時)を伝えてください。

ステップ 0:ハードウェアを推測する前に、モデルの実際の数値を確認する

モデル名で一般的な「VRAM 要件」のブログ記事を信用しないでください。GPU レンタルの SEO 分野には、(「willitrunai.com」や「canitrun.dev」、「orcarouter.ai」のような)あらゆるモデル名について、もっともらしい仕様ページを自動生成するコンテンツファームのサイトがあふれています。まだほとんど存在しないモデルまで対象に含まれています。こうしたサイトの記述は、互いに食い違っていても、どれも自信満々です。

代わりに、HuggingFace API から正確な情報を取得してください。

# Real param count / architecture (don't trust the repo name's number):
curl -s "https://huggingface.co/<org>/<model>/raw/main/config.json"

# Real total weight size, which is what actually has to fit in VRAM:
curl -s "https://huggingface.co/<org>/<model>/raw/main/model.safetensors.index.json" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['metadata']['total_size'] / 1e9, 'GB')"

bf16 の重みサイズ(GB)の概算 ≈ パラメーター数(B)× 2。重みを配置した後、KV キャッシュとアクティベーション用に少なくとも約 20~30GB の余裕が残るように GPU を選んでください。密な Transformer モデルの場合は、さらに余裕を見込んでください(完全な KV キャッシュはコンテキスト長に応じて増加します)。一方、ハイブリッド/線形アテンションアーキテクチャの場合は、より少なくて済みます(Mamba/GDN レイヤーは固定サイズの状態を保持するため、同じサイズの密なモデルと比べて、長いコンテキストでもコストが大幅に低くなります)。

モデルが問題の起きやすいアーキテクチャ(非常に新しいハイブリッドアテンションや、リリースされたばかりのモデルなど)の場合は、GPU を決める前に "<architecture class name>" vllm github issues を検索してください。たとえば、Qwen3_5ForConditionalGeneration vllm issues です。新しいアーキテクチャには、vLLM で正しく動作しないバグ(出力の文字化け、テンソル形式の不一致など)が未解決のまま残っていることがあります。このリスクをユーザーに伝え、動くことを黙って期待するのは避けてください。

ステップ 1:GPU の空き状況と価格を確認して選ぶ(タイプ文字列を推測しない)

RunPod の REST API で /v1/gputypes と /v1/datacenters を呼び出すと 404 になります。もっともらしいエンドポイント名ですが、実際には存在しません。代わりに GraphQL API を使用してください。

curl -s -X POST https://api.runpod.io/graphql \
  -H "Authorization: Bearer $RUNPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"query":"query { gpuTypes(input: {id: \"NVIDIA H100 80GB HBM3\"}) { id lowestPrice(input: {gpuCount: 1}) { uninterruptablePrice stockStatus } } }"}'

有効な gpuTypeIds には、"NVIDIA H100 80GB HBM3"(SXM)、"NVIDIA H100 PCIe"、"NVIDIA H100 NVL"、"NVIDIA A100 80GB PCIe"、"NVIDIA A100-SXM4-80GB" などがあります。全リストを確認する場合は、query { gpuTypes { id displayName memoryInGb } } を実行してください。最初に選んだタイプが在庫切れの場合に RunPod が自動的に代替を選べるよう、ポッドの作成時(下記参照)に許容できるタイプを短いリストで渡してください。これにより、プロビジョニング全体が失敗するのを防げます。

API キーの保存場所:まず環境変数 $RUNPOD_API_KEY を確認してください。設定されていない場合はエクスポートしてください(RunPod コンソールの Settings -> API Keys から作成できます)。

Step 2: ポッドを作成する

curl -s -X POST https://rest.runpod.io/v1/pods \
  -H "Authorization: Bearer $RUNPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "vllm-<model-slug>-test",
    "gpuTypeIds": ["NVIDIA H100 PCIe", "NVIDIA H100 80GB HBM3", "NVIDIA H100 NVL"],
    "imageName": "vllm/vllm-openai:latest",
    "containerDiskInGb": <weights_GB * 1.5, minimum 80>,
    "ports": ["8000/http"],
    "dockerStartCmd": [
      "--model", "<org>/<model>",
      "--host", "0.0.0.0", "--port", "8000",
      "--trust-remote-code",
      "--enforce-eager",
      "--gpu-memory-utilization", "0.90",
      "--max-model-len", "65536",
      "--served-model-name", "<served-name>",
      "--enable-auto-tool-choice",
      "--tool-call-parser", "hermes"
    ],
    "env": { "HF_HOME": "/root/.cache/huggingface" }
  }'

フラグに関する注意事項:

  • --max-model-len:大きめに設定し、必要だと感じる値よりさらに大きくしてください。 65536 は十分大きいように思えましたが、実際の数時間にわたるコーディングセッション(Git の操作や大量のツール出力)では約 127K トークンに達して上限にぶつかりました。さらに悪いことに、/compact 自体にも入力と要約のための十分な出力枠が必要です。そのため、会話がすでに上限近くにあると、まさに必要なときに、同じオーバーフローエラーでコンパクト化に失敗します。モデル本来のコンテキスト長を優先してください。Step 0 の HF config.json で max_position_embeddings を確認し、GPU メモリに余裕があればその値を使います(80GB の H100 で、約 28B モデルに 262144 トークンのウィンドウを設定した場合は余裕がありました)。65536 からモデル本来の最大値へ直接引き上げることで、同じ問題に再び直面せずに済みました。
  • --tool-call-parser:モデルファミリーに合ったものを選びます(多くのファインチューニング済みモデルでは hermes が妥当なデフォルトです。Qwen3.x-coder スタイルのモデルでは qwen3_coder と --reasoning-parser qwen3 を使います)。パーサーの選択を間違えても、明確なエラーは発生しません。モデルがツール呼び出しを一切出力せず、テキストだけを返すようになります。Claude Code がツールを呼び出さない場合は、まずパーサーを確認してください。
  • --enforce-eager:最初のスモークテストでは有効にしておきます。モデルが正常に応答するか確認する間、CUDA グラフのコンパイル時間と、それに関連する不安定さを避けられます。正しく動作することを確認し、速度を重視する段階になったら外してください。
  • すでに同じ name の RUNNING ポッドが存在する場合は、重複して作成せずに再利用できるよう、まず GET /v1/pods をポーリングしてください。

vLLM の URL は常に https://<podId>-8000.proxy.runpod.net です。/health をポーリングしてください(イメージの取得と重みのダウンロード中は、404 の後に 502 が複数回続き、その後 200 になります)。50GB 以上のモデルでは数分かかることを見込んでください。会話をブロックしないよう、ポーリングループはバックグラウンドで実行します(run_in_background: true)。

正常な状態になったら、Claude Code を使う前に、まず生の curl リクエストで動作を確認してください。

curl -s "$VLLM_URL/v1/chat/completions" -H "Content-Type: application/json" -d '{
  "model": "<served-name>", "messages": [{"role":"user","content":"What is 2+2? One word."}],
  "max_tokens": 20, "temperature": 0
}'

ここで一貫性のある出力が得られれば、Claude Code のレイヤーが加える複雑さを持ち込む前に、モデルと vLLM の組み合わせが機能することを確認できます。この手順で文字化けしたトークンや同じトークンの繰り返しが返る場合は、モデルまたは vLLM のバージョンに問題があります。ブリッジの問題ではないため、まだ Step 3 に進まないでください。

ステップ 3:Claude Code を接続します

scripts/anthropic-openai-proxy.js を作業場所の隣にコピーして、実行します。

VLLM_URL="https://<podId>-8000.proxy.runpod.net" PORT=<free-port> SERVED_MODEL="<served-name>" \
  node anthropic-openai-proxy.js

まず、本当に空いているローカルポートを選びます。ss -ltn | grep :<port> を実行してください。このマシンでは多くの Claude セッションが並行して実行されており、8787 のようなよく使われるポートは、他のプロセスによってすでに使用されていることがよくあります。ポートが空いていると決めつけないでください。

その後、隔離された Claude Code の呼び出しを実行します。ここでいう隔離とは、オーバーライドが効かなかった場合に、ユーザーの実際の課金対象である Anthropic アカウントにフォールバックしないことを意味します。

env -u ANTHROPIC_AUTH_TOKEN -u CLAUDE_CODE_OAUTH_TOKEN \
  ANTHROPIC_BASE_URL="http://localhost:<port>" \
  ANTHROPIC_API_KEY="sk-local-dummy-key" \
  CLAUDE_CODE_MAX_CONTEXT_TOKENS=<max-model-len used above> \
  claude --bare -p "<test task>" \
  --dangerously-skip-permissions \
  --model <served-name> \
  --output-format text
  • --bare によって安全性が確保されます。認証を ANTHROPIC_API_KEY/apiKeyHelper に厳密に限定し、OAuth やキーチェーンは使用しません。これがないと、オーバーライドの設定ミスにより、ユーザーが実際にログインしているアカウントへ気づかないままフォールスルーして課金される可能性があります。--bare は環境変数によるオーバーライドが完璧であることに頼らず、構造的にその事態を防ぎます。
  • CLAUDE_CODE_MAX_CONTEXT_TOKENS:これを設定しないと、認識されない --model 名を指定した場合に、Claude Code は予算計算でコンテキストウィンドウを 200k と想定します。その結果、vLLM サーバーの --max-model-len で許可される数を大幅に超える max_tokens をリクエストすることになります。vLLM に渡したものと同じ数値を設定してください。
  • CLAUDE_CODE_MAX_OUTPUT_TOKENS:別途設定が必要な、見落としやすい上限です。/compact で非常に長い会話を要約する必要がある場合、Claude Code は必要な出力予算を計算します。その際、内部のハードリミット(「Claude's response exceeded the 128000 output token maximum」)に達すると、バックエンドに接続する前にクライアント側で拒否されます。プロキシのログにも記録されず、リモート側でデバッグする手がかりもありません。CLAUDE_CODE_MAX_CONTEXT_TOKENS と合わせて、妥当な値(例:8192)を設定してください。
  • 実行中はプロキシ自身の標準出力ログを確認してください。ターンごとに受信メッセージ数とツール数が表示されるため、リクエストがリモートモデルに実際に届いているのか、それともブリッジで失敗しているのかを最も手早く確認できます。
  • 実際のコーディングタスクでは、2~3 分以上かかることを想定してください。--dangerously-skip-permissions -p の実行が 180 秒経っても戻らないのは通常のことで、停止したわけではありません。失敗したと決めつけず、バックグラウンドで実行させてください。

セッション中:進行中の会話でポッドを入れ替える

実際のセッションで、開始時のコンテキストウィンドウでは足りなくなった場合でも、会話を失わずに対処できます。チャットの履歴はポッド上ではなく、Claude Code のトランスクリプトにローカルに保存されているためです。ポッドは破棄して入れ替えられます。

  1. 古いポッドを終了し、--max-model-len を大きくした新しいポッドを作成します(同じイメージ、同じ served-model-name を使用)。
  2. ステップ 2 と同様に、新しいポッドの /health をポーリングします。
  3. 実行中のプロキシプロセスを終了し、同じ PORT で再起動して、VLLM_URL を新しいポッドに向けます。Claude Code 側で変更は不要です。同じ ANTHROPIC_BASE_URL、同じポートを使います。
  4. ユーザーに、CLAUDE_CODE_MAX_CONTEXT_TOKENS を新しいウィンドウに合わせて増やしたうえで、claude --bare --resume <session-id> ... を使って再開してもらいます。

リクエストの処理中にプロキシを再起動すると、そのリクエストはエラーになります(Claude Code に API エラーが表示され、通常は自動で再試行されます)。ユーザーがターンの途中にいる場合は、黙って実行せず、事前に伝えてください。

既知の注意点(同梱プロキシではすでに修正済みです。さらにデバッグが必要な場合に備えて把握しておいてください)

  1. max_tokens の超過。 Claude Code のデフォルトの要求 max_tokens は、実際の --max-model-len に関係なく、約 32000 になることがあります。プロンプトと max_tokens の合計がサーバーのコンテキストウィンドウを超えると、vLLM は毎回のターンで 400 エラーを返し、Claude Code は延々と再試行して GPU 時間を無駄に消費します。対処法:転送する max_tokens を妥当な値に制限します(同梱プロキシではデフォルトで 4096 です。MAX_TOKENS_CAP で変更できます)。
  2. 会話の途中に挿入される role:"system" メッセージ。 Claude Code は <system-reminder> の内容を、会話の途中に個別の role:"system" ターンとして送信します(最上位の system フィールドだけではありません)。Anthropic の API では問題ありませんが、vLLM のチャットテンプレートは「system は最初のメッセージでなければならない」という制約があり、それ以外の場合は 400 エラーになります。対処法:先頭以外の system ロールのメッセージはすべて、転送前に user にまとめます。
  3. pkill -f <pattern> が実行中のシェル自身に一致すると、現在のコマンドが終了します(終了コード 144)。 停止したネストされた claude プロセスを終了する場合は、ps aux | grep で正確な PID を確認して kill <pid> を実行してください。実行時のコマンド文字列にも「claude」が含まれるシェル内から、pkill -f "claude" を実行してはいけません。これは claude 固有の注意点ではありません。シェルラッパー自身のコマンドラインに同じ文字列が含まれていたため、pkill -f "node proxy.js" でも自身に一致しました。必ず正確な PID を指定して終了し、実行元のコマンドを表す可能性のあるパターンで終了しないでください。
  4. バックエンドからの単一の長いブロッキング応答が、RunPod のエッジプロキシのタイムアウトを超えることがあります。 通常の RunPod Pods では、API 経由で設定可能なタイムアウト設定は公開されていません(executionTimeoutMs/idleTimeout は RunPod の API に存在しますが、Pods ではなく Serverless 専用です)。そのため、回答前に 100K 以上のトークンを処理する必要がある /compact は、気付かないうちにタイムアウトを超えることがあり、プロキシには JSON ではなく HTML のゲートウェイエラーページが返されます(Unexpected token '<', "<!DOCTYPE")。対処法:vLLM には常に stream: true(および stream_options: {include_usage: true})を要求し、チャンクを受信したら順次中継します。バイトが継続的に流れていれば、生成にどれだけ時間がかかってもアイドルタイムアウトは発生しません。同梱プロキシではすでにこの方法を使用しています。
  5. vLLM がストリーミングの最後の使用量チャンクで prompt_tokens: 0 を報告することがあります。 completion_tokens は正しく返され、実際に生成が行われている場合でも発生します(多数のツール呼び出しを含むターンの後に確認済みです)。Claude Code は報告された入力トークン数を使って自動コンパクトのタイミングを判断するため、誤った 0 によって実際よりはるかに余裕があると判断し、このスキルが防ごうとしているのとまったく同じ超過が気付かないうちに起きる可能性があります。バックエンドの値が falsy の場合、同梱プロキシは概算の文字数カウント(fallbackInputTokens)を使用します。この処理は削除しないでください。

外部から実行中のセッションのコンテキスト使用量を確認する

作業中にユーザーが定期的に「コンテキストを何%使用しているか」と確認したい場合は、実行中の --resume セッションのトランスクリプトから最新のアシスタントターンの使用量を読み取ることで、セッションに触れずにコンテキストの使用状況を監視できます。

# --bare sessions land under ~/.claude/projects/<cwd-slug>/<session-id>.jsonl —
# NOT the account-specific ~/.claude-<acct>/ dirs. find it with:
find ~/.claude -iname "*<session-id>*"

python3 -c "
import json
with open('<transcript.jsonl>') as f:
    lines = f.readlines()
for line in reversed(lines):
    line = line.strip()
    if not line: continue
    try: o = json.loads(line)
    except: continue
    if o.get('type') == 'assistant':
        usage = o.get('message', {}).get('usage')
        if usage and usage.get('input_tokens'):
            print(o['timestamp'], usage['input_tokens'], usage['input_tokens']/<max_model_len>*100)
            break
"

固定の tail -N ウィンドウではなく、ファイル末尾から逆方向にスキャンしてください。多数のツールとのやり取りを含む単一のターンによって、使用量を含む最新の行が小さな tail の範囲よりもかなり前に押し出され、実際には発生しているのに何も起きていないように見えることがあります。

不適切な /compact から復旧する

/compact によって要約が失われる情報の多いものになり、モデルがタスクを追えなくなった場合(あるいは混乱して破壊的な操作を行ってしまった場合)、コンパクト化前の生のメッセージはトランスクリプトファイルに残っています。compact は、これまでのすべてを再生する代わりに、今後のターンで要約を使うよう指示するだけです。つまり、compact を狙いどおりに取り除けば、compact が実行される前の状態から再開できます。これを続けて二度失敗するのは本当に簡単です(ここでもそうでした)。試す前に references/transcript-surgery.md を読み、基本原則だけで見切り発車しないでください。

ステップ 4:後片付け — 「sin dejar rastro」は推測ではなく確認を意味します

ユーザーがすべてを停止・削除するよう求めた場合、削除の呼び出しを行うだけでなく、残っていないことを確認してください。

# kill exact PIDs (not pkill -f, see gotcha 3)
ps aux | grep -E "anthropic-openai-proxy|claude --bare" | grep -v grep
kill <pid> <pid>

# terminate the pod
curl -s -X DELETE "https://rest.runpod.io/v1/pods/<podId>" -H "Authorization: Bearer $RUNPOD_API_KEY" -w "\n%{http_code}\n"

# confirm nothing is left running or costing money
curl -s https://rest.runpod.io/v1/pods -H "Authorization: Bearer $RUNPOD_API_KEY"          # expect []
curl -s https://rest.runpod.io/v1/networkvolumes -H "Authorization: Bearer $RUNPOD_API_KEY" # expect [] unless one was deliberately kept
curl -s -X POST https://api.runpod.io/graphql -H "Authorization: Bearer $RUNPOD_API_KEY" \
  -H "Content-Type: application/json" -d '{"query":"query { myself { clientBalance currentSpendPerHr } }"}'
# currentSpendPerHr should read 0

# delete scratch files (proxy copy, test project, logs) from wherever they were put —
# the session scratchpad directory, never the repo
rm -rf <scratch-dir>

ネットワークボリューム(永続ディスク)はポッドとは別の課金対象リソースです。ポッドを終了しても、アタッチされたネットワークボリュームは削除されません。このセッション用に作成したもの(既存のものではない場合)があれば、明示的に削除してください。 DELETE /v1/networkvolumes/<id>。

最終状態は「完了」とだけ伝えるのではなく、実際の数値(残高、currentSpendPerHr、空のポッド/ボリューム一覧)を報告してください。それが「sin dejar rastro」で証明を求めていることです。

コメント