TL;DR
- OpenRouter の
:freeモデルは API キー 1 本で 10+ モデルを無料で呼び出せる - Claude Code のサブエージェント (
Taskツール) に:freeモデルを組み合わせると、思考ループのコストを大幅削減できる - 本記事では「呼び出しパターン」「モデル選定指針」「フォールバック設計」「プロンプトキャッシュとの併用」「コスト上限ガード」を具体的なコードつきで解説
背景
Claude Code (旧 claude-dev) は Anthropic の API を直接呼び出す設計のため、複雑なタスクを解かせると Opus / Sonnet のトークン消費が積み上がる。
一方 OpenRouter は 2024 年末から :free サフィックス付きモデルを無料枠として提供しており、2025〜2026 年現在では以下のようなモデルが無料で利用できる。
| モデル名 (OpenRouter ID) | 提供元 | 用途感 |
|---|---|---|
google/gemini-2.0-flash-exp:free |
高速・長コンテキスト | |
meta-llama/llama-4-scout:free |
Meta | コード生成・英語 |
qwen/qwen3-235b-a22b:free |
Alibaba | 多言語・推論 |
mistralai/mistral-small-3.2-24b-instruct:free |
Mistral | 軽量・欧州系タスク |
deepseek/deepseek-r1:free |
DeepSeek | 数学・ロジック |
moonshotai/kimi-k2:free |
Moonshot | 長文・要約 |
これらを「思考の下請け」として使い、Claude Sonnet には「統合・判断・最終出力」だけを担わせる構成が、コスト最小・品質維持の王道になりつつある。
パターン 1 — タスク分類ルーター: 「安いモデルで十分か」を最初に判定する
最もシンプルなパターン。入力タスクを A: 定型 / B: 推論 / C: 創造 に分類し、A は :free モデルへ流す。
import httpx
import json
OPENROUTER_API = "https://openrouter.ai/api/v1/chat/completions"
ROUTER_MODEL = "qwen/qwen3-235b-a22b:free"
PREMIUM_MODEL = "anthropic/claude-sonnet-4-5" # ← 有料、最後の砦
HEADERS = {
"Authorization": f"Bearer {OPENROUTER_API_KEY}", # 実環境では os.environ
"Content-Type": "application/json",
}
def classify_task(user_input: str) -> str:
"""A=routine / B=reasoning / C=creative"""
prompt = f"""Classify the following task into exactly one label:
- A: routine (data transform, format, extract, summarize template)
- B: reasoning (multi-step logic, math, code debug)
- C: creative (novel writing, architecture design, strategy)
Task: {user_input}
Reply with only the single letter."""
payload = {
"model": ROUTER_MODEL,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 3,
}
res = httpx.post(OPENROUTER_API, headers=HEADERS, json=payload, timeout=20)
return res.json()["choices"][0]["message"]["content"].strip().upper()
def route_and_call(user_input: str) -> str:
label = classify_task(user_input)
model = ROUTER_MODEL if label in ("A", "B") else PREMIUM_MODEL
payload = {
"model": model,
"messages": [{"role": "user", "content": user_input}],
}
res = httpx.post(OPENROUTER_API, headers=HEADERS, json=payload, timeout=60)
choice = res.json()["choices"][0]["message"]["content"]
return f"[{model}] {choice}"
ポイント: 分類自体も
:freeモデルに任せる。分類は 3 トークンで完結するので高速・ゼロコスト。
パターン 2 — ファンアウト並列: 3 モデルに同じ問いを投げて多数決
コード生成や SQL 生成など「正解がある問い」では、複数の :free モデルに並列で聞いて 多数決 or ユニーク出力をマージ する手法が有効。
import asyncio
import httpx
FREE_MODELS = [
"google/gemini-2.0-flash-exp:free",
"meta-llama/llama-4-scout:free",
"qwen/qwen3-235b-a22b:free",
]
async def call_free(client: httpx.AsyncClient, model: str, prompt: str) -> str:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 512,
}
r = await client.post(OPENROUTER_API, headers=HEADERS, json=payload, timeout=30)
return r.json()["choices"][0]["message"]["content"]
async def fan_out(prompt: str) -> list[str]:
async with httpx.AsyncClient() as client:
tasks = [call_free(client, m, prompt) for m in FREE_MODELS]
return await asyncio.gather(*tasks, return_exceptions=True)
def majority_vote(answers: list[str]) -> str:
# 簡易: 最も文字数が多い (= 最も詳細な) ものを返す
valid = [a for a in answers if isinstance(a, str)]
return max(valid, key=len)
results = asyncio.run(fan_out("Write a SQL query to find top 5 users by order count"))
best = majority_vote(results)
print(best)
注意:
:freeモデルはレートリミットがある (多くは 10 RPM 程度)。
asyncio.gatherの並列度は 3〜5 程度に留め、asyncio.Semaphoreで絞るのが安全。
パターン 3 — フォールバックチェーン: エラー時に自動で上位モデルへ昇格
:free モデルはサーバー過負荷で 429 / 503 を返すことがある。エクスポネンシャルバックオフ+モデル昇格のチェーンを組む。
import time
import httpx
MODEL_CHAIN = [
"deepseek/deepseek-r1:free",
"qwen/qwen3-235b-a22b:free",
"anthropic/claude-sonnet-4-5", # 最終手段・有料
]
def call_with_fallback(prompt: str, max_retries_per_model: int = 2) -> str:
for model in MODEL_CHAIN:
wait = 1
for attempt in range(max_retries_per_model):
try:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
}
r = httpx.post(
OPENROUTER_API, headers=HEADERS, json=payload, timeout=45
)
if r.status_code == 200:
print(f"[OK] model={model} attempt={attempt+1}")
return r.json()["choices"][0]["message"]["content"]
if r.status_code in (429, 503):
print(f"[WAIT] {model} status={r.status_code} sleep={wait}s")
time.sleep(wait)
wait *= 2
else:
# 4xx 系はリトライ不要
break
except httpx.TimeoutException:
print(f"[TIMEOUT] {model}")
time.sleep(wait)
wait *= 2
raise RuntimeError("All models exhausted")
設計思想: 無料モデルが死んでも有料モデルで確実にジョブを完遂する。
本番では有料モデルへの到達頻度をメトリクスで監視して:freeモデルの安定性を評価する。
パターン 4 — Claude Code の Task ツールと組み合わせるプロンプト設計
Claude Code の .claude/commands/ にカスタムコマンドを置くと、Claude が自身でサブエージェントを起動できる。
<!-- .claude/commands/free-draft.md -->
# Draft with Free Model
## Usage
/free-draft <topic>
## Instructions
You are a coordinator. Your job is:
1. Call the OpenRouter API directly via `Bash` tool using curl, targeting model `qwen/qwen3-235b-a22b:free`
2. Pass the user's topic as the prompt
3. Return the raw response as a draft
4. DO NOT use any paid Anthropic model for the draft step
5. After receiving the draft, summarize it in 3 bullet points yourself
This minimizes token usage on the premium model (you) to only the summarization step.
Bash ツールから curl で直接 OpenRouter を叩く例:
# Claude Code の Bash ツール内で実行されるイメージ
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen/qwen3-235b-a22b:free",
"messages": [{"role":"user","content":"Draft an article about Rust async runtimes"}],
"max_tokens": 1024
}' | jq -r '.choices[0].message.content'
ポイント: Claude Sonnet が「ドラフトのサマリー」だけを担当するため、実際のトークン消費は Claude 側で数百トークン程度に収まる。
パターン 5 — コスト上限ガード: トークンカウンタで Sonnet への昇格を抑制
無意識に Sonnet を何度も呼んでしまう問題を防ぐため、セッション内のトークン消費量を累積カウントし、上限に達したら :free モデルへ強制ルーティングする。
from dataclasses import dataclass, field
@dataclass
class CostGuard:
"""
Tracks estimated token usage.
Prices (approx, USD per 1M tokens):
claude-sonnet-4-5 input $3 / output $15
:free models $0 / $0
"""
budget_usd: float = 0.10 # セッション予算 10 セント
_input_tokens: int = field(default=0, init=False)
_output_tokens: int = field(default=0, init=False)
INPUT_PRICE = 3.0 / 1_000_000 # $3 per 1M
OUTPUT_PRICE = 15.0 / 1_000_000 # $15 per 1M
def record(self, input_tokens: int, output_tokens: int) -> None:
self._input_tokens += input_tokens
self._output_tokens += output_tokens
@property
def spent_usd(self) -> float:
return (self._input_tokens * self.INPUT_PRICE
+ self._output_tokens * self.OUTPUT_PRICE)
@property
def over_budget(self) -> bool:
return self.spent_usd >= self.budget_usd
def select_model(self, preferred_premium: str, free_fallback: str) -> str:
if self.over_budget:
print(f"[GUARD] Budget exhausted (${self.spent_usd:.4f}). Using free model.")
return free_fallback
return preferred_premium
guard = CostGuard(budget_usd=0.05)
def smart_call(prompt: str) -> str:
model = guard.select_model(
preferred_premium="anthropic/claude-sonnet-4-5",
free_fallback="qwen/qwen3-235b-a22b:free",
)
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
}
r = httpx.post(OPENROUTER_API, headers=HEADERS, json=payload, timeout=60)
data = r.json()
usage = data.get("usage", {})
guard.record(
input_tokens=usage.get("prompt_tokens", 0),
output_tokens=usage.get("completion_tokens", 0),
)
return data["choices"][0]["message"]["content"]
補足: OpenRouter のレスポンスには
usageフィールドが含まれる。モデルによって精度が異なるが、予算ガードとして十分機能する。
5 パターンのまとめ表
| # | パターン名 | コスト削減効果 | 向いているユースケース |
|---|---|---|---|
| 1 | タスク分類ルーター | ★★★★☆ | 汎用 CLI ツール・チャットボット |
| 2 | ファンアウト並列 | ★★★☆☆ | SQL 生成・コード生成 |
| 3 | フォールバックチェーン | ★★☆☆☆ | バッチ処理・夜間ジョブ |
| 4 | Claude Code Task 連携 | ★★★★★ | 記事執筆・コードレビュー自動化 |
| 5 | コスト上限ガード | ★★★★☆ | 長時間セッション・マルチターン |
よくあるハマりどころ
:free モデルの RPM 制限
ほとんどの :free モデルは 10 RPM / 200 RPD 程度の制限がある。ループ内でそのまま叩くと即 429 になるので、最低でも time.sleep(6) を挟む。
import time
for item in large_batch:
result = call_with_fallback(item)
time.sleep(6) # 10 RPM → 6秒間隔
thinking モード (extended thinking) は :free 非対応
DeepSeek R1 や Qwen3 の「thinking」モードは OpenRouter :free 枠では無効または制限される場合がある。
推論問題でコストを出したくない場合は deepseek/deepseek-r1:free の通常モードを使い、出力に <think>...</think> タグが含まれていたらパース時に除去する。
import re
def strip_thinking(text: str) -> str:
return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
OpenRouter モデルの廃止・名称変更
:free モデルのリストは頻繁に変わる。定期的に https://openrouter.ai/api/v1/models を叩いてモデル一覧を取得し、:free サフィックスを持つものだけをフィルタするコードを保守しておくと安全。
def fetch_free_models() -> list[str]:
r = httpx.get("https://openrouter.ai/api/v1/models", timeout=10)
models = r.json().get("data", [])
return [m["id"] for m in models if m["id"].endswith(":free")]
まとめ
-
OpenRouter
:freeモデルはプロダクションでも十分使える (ただし信頼性は有料に劣る) - 分類→並列→フォールバックの 3 層設計が最もコスト・品質のバランスが良い
- Claude Code の
Taskツールやカスタムコマンドと組み合わせると、Sonnet の消費をサマリーステップのみに絞れる - コスト上限ガードを必ず入れる。ループバグで予算が溶けた時点でフリーモデルへ強制フォールバックする設計は保険として有効
自動化エージェントの「コスト vs. 品質」トレードオフを設計する際の参考になれば幸いです。
参考リンク
- OpenRouter 公式ドキュメント
- OpenRouter Models API
- Claude Code 公式ドキュメント (Anthropic)
- httpx — Async HTTP for Python
- DeepSeek R1 論文 (arXiv:2501.12948)
- Qwen3 技術レポート (Qwen Blog)
✍️ 本記事の著者: 合同会社ジモラボ
ジモラボは、八王子を拠点に AI を活用した SaaS を多数開発しています。本記事の技術検証もそうした開発過程の副産物です。
- 🌐 公式サイト: https://locallab.jp
- 🔍 AI SEO 最適化 SaaS: lookupai.jp
- 📺 YouTube: @locallab_llc
- ✉️ お問い合わせ: info@locallab.jp
興味を持っていただけたら、ぜひ各 SNS のフォローもお願いします!