
Anthropic OAuth トークン禁止で自律AIエージェントが停止した話と、Frontmatter プロンプト制御 + Claude Code 委任で立て直した方法
はじめに
自作の自律AIエージェント「Ruby」が、ある日突然 Discord で応答しなくなりました。タイピングインジケータは表示されるものの、返答が来ない。原因を調べると、Anthropic が 2026年2月に OAuth トークン(sk-ant-oat)のサードパーティAPI利用を禁止していた ことが分かりました。
本記事では、この問題の発見から対処、さらにこれをきっかけに行った プロンプトサイズ最適化 と Claude Code 委任アーキテクチャ の実装までをまとめます。
前提・環境
- Ruby: 自律AIエージェント(Python 3.12+、Discord/LINE/Slack対応)
- 実行環境: Oracle Cloud VM(Docker コンテナ)
- LLMバックエンド: Anthropic Claude API(litellm経由)
- Claude Code CLI: OAuth トークンでの利用は引き続き可能(Max サブスクリプション)
- デプロイ: git push → GitHub Actions → SSH → Docker rebuild
問題1: OAuth トークン禁止
症状
Discord でメッセージを送ると、Ruby のタイピングインジケータ(「...」)が表示された後、消えるだけ。エラーメッセージも返ってこない。
コンテナログを確認すると:
litellm.BadRequestError: AnthropicException - {"type":"error","error":{"type":"invalid_request_error","message":"Error"}}
原因
2026年2月、Anthropic は sk-ant-oat で始まる OAuth トークンを Claude Code CLI 以外 のサードパーティAPI呼び出しで使用することを禁止しました。これは、オープンソースCLIツールが Claude Max サブスクリプションのOAuthトークンを経由してAPIを無料利用していた問題への対策です。
つまり:
- Claude Code CLI → OAuth トークンで引き続き利用可能(公式ツールなので)
- litellm / SDK / curl 等のAPI直接呼び出し → OAuth トークンは使用不可。コンソールAPIキー(
sk-ant-api)が必要
対処
console.anthropic.com からAPIキーを発行し、GitHub Secrets に ANTHROPIC_API_KEY として登録。deploy ワークフローの .env ビルドに追加しました。
問題2: レートリミット直撃
APIキーに切り替えた後、新たな問題が発生。
rate_limit_error: This request would exceed your organization's rate limit of 30,000 input tokens per minute
Ruby のシステムプロンプト + ツールスキーマだけで 約16,000トークン 。ユーザーメッセージ + 会話履歴を加えると、1回のAPI呼び出しで約25,000トークン。さらに Initiative エンジン(自律アクション)が同時に発火すると、1分間で50,000トークン超えは確実でした。
プロンプトの内訳
| コンポーネント | トークン数 |
|---|---|
| システムプロンプト(人格・ルール) | ~6,050 |
| ツールスキーマ(10ツール) | ~4,500 |
| 会話履歴 | ~6,000-15,000 |
| 合計 | ~16,000-25,000 |
問題の核心:Discord のカジュアルなチャットも、DM での複雑な自己修正セッションも、全く同じプロンプト を使っていたこと。
解決策: 3つの柱
柱1: Frontmatter ベースの動的プロンプトローディング
コンセプト
人格・ルールファイル(.md)に YAML Frontmatter でロード条件を宣言させ、チャネルコンテキストに応じて必要なファイルだけを読み込む仕組みです。
---
channels: [discord]
priority: 20
---
# Discord Rules (HARD RULES!)
- **NO markdown tables** — they render as ugly monospace text
- **Use `table-image` skill** — render tables as images
- **Wrap links in `<>`** to suppress embeds
---
channels: [dm, system]
priority: 30
---
# Reincarnation — How You Evolve Your Own Code
...
実装
もともと352行の巨大な rules/AGENTS.md を6つの専門ファイルに分割:
| ファイル | channels | 内容 |
|---|---|---|
rules/CORE.md |
全チャネル | 安全規則、スキル確認 |
rules/DISCORD.md |
discord | フォーマット、テーブル禁止 |
rules/GROUP_CHAT.md |
discord, slack | 発言判断、リアクション |
rules/SELF_MODIFY.md |
dm, system | 自己修正ルール |
rules/MEMORY_RULES.md |
dm, system | 記憶管理 |
rules/SYNC.md |
dm, system | 同期システム |
人格ファイルローダーの主要部分:
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
_CHANNELS_RE = re.compile(r"channels:\s*\[([^\]]*)\]")
_PRIORITY_RE = re.compile(r"priority:\s*(\d+)")
def parse_frontmatter(text: str) -> tuple[str, list[str], int]:
match = _FRONTMATTER_RE.match(text)
if not match:
return text, [], 50 # frontmatter なし = 常にロード(後方互換)
fm_block = match.group(1)
body = text[match.end():]
# channels と priority を解析...
return body.strip(), channels, priority
チャネル別フィルタリング
def get_prompt_sections(self, channel: str = "dm", include_memory: bool = True) -> list[str]:
sections = []
for pf in sorted(self.files, key=lambda f: f.priority):
if pf.channels and channel not in pf.channels:
continue
sections.append(pf.body)
if include_memory and self.memory:
sections.append(self.memory)
return sections
削減効果
| コンテキスト | 人格トークン(Before) | 人格トークン(After) | 削減率 |
|---|---|---|---|
| Discord チャネル | ~6,050 | ~1,500 | -75% |
| DM | ~6,050 | ~4,200 | -30% |
| システムイベント | ~6,050 | ~3,800 | -37% |
| Initiative | ~6,050 | ~750 | -88% |
さらに、ツールスキーマも同様にフィルタリング:
TOOL_SETS = {
"chat": {"response", "web_search", "skill", "memory_search",
"discord_history", "delegate"},
"dm": None, # 全ツール
"system": {"response", "shell", "memory_save", "memory_search",
"daily_log", "message", "skill", "self_modify", "delegate"},
"initiative": {"response", "memory_search", "message",
"daily_log", "delegate"},
}
柱2: プロンプトキャッシング
Anthropic は、連続呼び出しで同一のシステムプロンプトプレフィックスを自動キャッシュします。キャッシュ読み取りは 0.1倍 のコスト。
問題は、datetime.now() がシステムプロンプトに含まれていたため、毎回キャッシュが無効になっていたこと。
解決策: プロンプトを 安定プレフィックス と 揮発サフィックス に分離。
system_message = {
"role": "system",
"content": [
{"type": "text", "text": stable_prefix,
"cache_control": {"type": "ephemeral"}},
{"type": "text", "text": volatile_suffix},
],
}
- 安定プレフィックス: 人格、ルール、日報(最大で1日1回の変更)→ キャッシュ対象
- 揮発サフィックス: タイムスタンプ、インスタンス名、拡張セクション → 小さい、毎回変わる
柱3: Haiku ディスパッチャー + Claude Code 委任
ここが最も大きなアーキテクチャ変更です。
基本思想
Haiku は考えない。仕分けるだけ。考えるのは Claude Code(Opus)。

- Haiku($1/M 入力トークン): 全チャットのディスパッチャー
- Claude Code CLI(OAuth、Max サブスクリプションで無料): 複雑な推論、リサーチ、分析
delegate ツールの実装
既存の run_code_agent() インフラを再利用:
@register
class DelegateTool(Tool):
name = "delegate"
description = (
"Delegate a task to Claude Code (Opus) for deep reasoning. "
"Use for ANY task requiring real thinking."
)
async def execute(self) -> ToolResponse:
from rubyclaw.evolution.code_agent import resolve_config, run_code_agent
prompt = (
"You are assisting Ruby, an AI agent. "
"Do NOT modify any files. Just research and respond.\n\n"
f"Task: {task}"
)
# 進捗メッセージを送信
send_callback = self.context.data.get("send_callback")
if send_callback:
await send_callback("Let me think about this more carefully...")
success, output = await run_code_agent(
prompt=prompt,
project_root=project_root,
config=agent_config,
phase="plan", # 読み取り専用モード
)
return ToolResponse(message=output)
ポイント:
self_modifyツールの既存インフラ(run_code_agent())を再利用phase="plan"で読み取り専用モード(ファイル変更なし)- 進捗メッセージで「考え中...」をユーザーに通知
システムプロンプトでの指示
## Delegation — Your Most Important Tool
You are running on a lightweight model optimized for speed and cost.
You are a dispatcher, not a thinker.
For ANY task requiring real reasoning, use the `delegate` tool.
**Always delegate:**
- Complex questions, research, analysis
- All scheduled/cron tasks
- Strategic decisions, code review
**Respond directly (no delegation):**
- Simple greetings ("hi", "thanks")
- Acknowledgments ("ok", "got it")
- Relaying delegate results to the user
結果・動作確認
テスト1: 簡単な質問(委任なし)
Q: What is the capital of the country that hosted the 2024 Summer Olympics?
A: Paris — France hosted the 2024 Olympics, and Paris is both the capital and the host city. (Though honestly, this one was simple enough I could've just answered it directly. 💎)
Haiku が直接応答。委任の判断も適切。
テスト2: 複雑なコードベース分析(委任あり)
Q: Analyze the rubyclaw codebase: how many tools are registered, what are their names, and which ones are in the chat tool set vs full DM set?
A: (delegate ツール経由) 12 tools total, split across contexts: Chat: 6 tools, DM: All 12, System: 9 tools, Initiative: 5 tools...
ログで確認:
tool:delegate args={'task': 'Analyze the rubyclaw codebase...'}
Delegation completed (1735 chars)
委任レイテンシは約44秒(Claude Code CLI の起動 + Opus の処理)。
コスト比較
| シナリオ | Before(Sonnet) | After(Haiku + 委任) |
|---|---|---|
| 簡単なチャット(70%) | ~$0.048/msg | ~$0.005/msg |
| 複雑な質問(20%) | ~$0.048/msg | 無料(Claude Code) |
| システム/cron(10%) | ~$0.048/msg | ~$0.003/msg |
まとめ
- Anthropic の OAuth トークン禁止は、エラーメッセージが不親切で発見が困難。
invalid_request_error+"Error"だけでは原因が分からない - Frontmatter ベースのプロンプト制御は、ハードコードされたプロファイルよりも拡張性が高い。新しいルールファイルを追加するだけでよく、コード変更が不要
- Haiku ディスパッチャー + Claude Code 委任 パターンは、API コストを大幅に削減しつつ、複雑なタスクでは Opus 品質を維持できる
- Claude Code CLI が OAuth を引き続き使えるという「抜け道」を活用することで、Max サブスクリプションの価値を最大化できる









