LLM 統合
プロバイダー抽象化、プロンプトキャッシュ、モデルルーティング
Minara Agent は単一の内部インターフェースを通じて複数の LLM プロバイダーと通信します。そのため、エージェントループに手を加えることなく、Anthropic、OpenAI、OpenRouter、ローカルモデルを自由に切り替えられます。このページでは、プロバイダーレイヤー、プロンプトキャッシュの構造、モデルルーティングの仕組みを説明します。
ベンダー SDK を直接使わずカスタムルーターを使う理由は? エージェントループ、バックテスト、リフレクション、サブエージェントなど、各機能にはコストとレイテンシのトレードオフがあります。Minara のルーターを使えば、同じコードベースから、キャッシュ安定性の高い重い処理は Sonnet に、安価なバッチ評価は Haiku にルーティングできます。またプロバイダーがレートリミットに達した場合もグレースフルにフォールバックできます。ベンダー SDK を直接使うと、特定プロバイダーの料金モデルに縛られてしまいます。
実際の設定方法: 環境変数でプロバイダーを設定します(ANTHROPIC_API_KEY、OPENAI_API_KEY、OPENROUTER_API_KEY)。
プロバイダーインターフェース
すべてのプロバイダーは apps/agent/src/core/agent-loop.ts で定義された LLMClient を実装します。
interface LLMClient {
createMessage(params: {
model: string;
system: SystemPrompt; // string OR cacheable blocks
messages: MessageParam[];
tools?: ToolDefinition[];
maxTokens?: number;
stream?: boolean;
}): Promise<LLMResponse>;
streamMessage?(params: ...): AsyncIterable<LLMStreamEvent>;
vision?(call: LLMVisionCall): Promise<LLMVisionResult>;
}具体的な実装は apps/agent/src/llm/ に置かれています。
| ファイル | プロバイダー | 認証 |
|---|---|---|
anthropic-api-key.ts | Anthropic | ANTHROPIC_API_KEY |
anthropic-oauth.ts | Anthropic (Claude.ai) | OAuth リフレッシュトークン |
anthropic-wire.ts | 共有ワイヤープロトコル | |
openai-wire.ts | OpenAI / OpenRouter | OPENAI_API_KEY / OPENROUTER_API_KEY |
openrouter.ts | OpenRouter ルーター | OPENROUTER_API_KEY |
select-provider.ts は起動時に環境変数を参照し、以下の優先順位で具体的なクライアントを選択します。
- Anthropic OAuth(
CLAUDE_CODE_OAUTH_TOKENが設定されている場合) - Anthropic API キー(
ANTHROPIC_API_KEYが設定されている場合) - OpenRouter(
OPENROUTER_API_KEYが設定されている場合) - OpenAI(
OPENAI_API_KEYが設定されている場合)
4つすべてが未設定の場合、プロセスは明確なエラーを出して起動を拒否します。選択ロジックは app.ts の1行で完結しており、以降の処理はすべて LLMClient を通じて行われます。
プロンプトキャッシュ(必須)
Anthropic のプロンプトキャッシュは TTL が5分で、キャッシュヒット時の料金は通常の入力 token 価格の10%です。1ターンに多数の類似した呼び出しを行う Agent(ツール呼び出しのラウンドトリップごとにカタログと Identity を送るなど)では、キャッシュヒットの有無が「許容範囲」と「高コスト」の分かれ目になります。
そのため、システムプロンプトは明示的なブロックに分割されています。
system: [
{ type: "text", text: identityPrompt, cache: true },
{ type: "text", text: skillCatalog, cache: true },
{ type: "text", text: activeSkillPrompts, cache: false },
{ type: "text", text: signalContextBlock, cache: false },
{ type: "text", text: pendingConfirmation, cache: false },
]プロンプトビルダーが守る不変条件は以下の通りです。
- キャッシュ可能なブロックを先頭に置く。 キャッシュキーは厳密なプレフィックス一致のため、キャッシュ不可ブロックの後にあるものはキャッシュできません。
- キャッシュ可能なブロックは安定している。 Identity とカタログは、スキルレジストリかベースプロンプトが変更されたときのみ更新され、ターンごとには変わりません。
- 動的なコンテンツは末尾に置く。 アクティブなスキルフラグメント、シグナルコンテキスト、確認待ちの内容、会話履歴はすべてキャッシュ境界の後に配置します。
anthropic-wire.ts は SystemPromptBlock[] を Anthropic の cache_control: {type: "ephemeral"} マーカーにマッピングし、レスポンスのメタデータからキャッシュヒット率を追跡します。この情報は構造化ログ上で llm.cache_read_input_tokens、llm.cache_creation_input_tokens として確認できます。
プロンプトキャッシュをサポートしていないプロバイダー(執筆時点の OpenAI など)では、ワイヤーレイヤーがブロックを単一のシステム文字列に無音で結合します。呼び出し側から見た差異はなく、動作上の差異も生じません。
モデル選択
各ターンで使用するモデルは apps/agent/src/learning/llm-router.ts が決定します(スキルルーターとは無関係で、LLM の呼び出しをモデルにルーティングするものです)。デフォルトポリシーは以下の通りです。
- メインエージェントターン。 コンテキスト 1M の Claude Sonnet 4.6。
- ディープリサーチサブエージェント。 Claude Opus 4.6(高度な推論向け)。
- ビジョン呼び出し。 ビジョンをサポートするプロバイダー。
- エンベディング。 専用の
EMBEDDING_PROVIDER環境変数で指定。
すべての呼び出しは {provider, model, reason} を監査ログに記録します。「このターンのコストが X になった理由」は、コードをたどることなく SQL クエリで確認できます。
オーバーライド設定:
AGENT_MODELでメインエージェントループのモデルを固定できます。minara config set model.defaultModel <id>コマンドまたは/modelスラッシュコマンドでインタラクティブに変更することも可能で、いずれも$MINARA_DATA_DIR/envに永続化されます。
ツール呼び出しの形式
Anthropic と OpenAI はともにツール呼び出しをサポートしていますが、ワイヤーフォーマットが異なります。エージェントループは内部で Anthropic の形式を使用し、openai-wire.ts が双方向に変換します。その結果として以下の特性があります。
- ツールスキーマは1箇所で定義する。
ToolEntry.schema.parametersの JSON Schema は両方のワイヤーレイヤーで共通して使用されます。 - 停止理由は正規化される。 Anthropic の
"end_turn"/"tool_use"/"max_tokens"と OpenAI の"stop"/"tool_calls"/"length"は、ループが扱う共通の enum にマッピングされます。 - ストリーミングイベントは正規化される。 共通の
LLMStreamEventユニオンに変換されるため、/chat/streamは上流のプロバイダーを意識しません。
ビジョン
すべてのプロバイダーがツール呼び出しとインラインでビジョンをサポートしているわけではないため、ビジョン呼び出しは専用の LLMClient.vision() メソッドを通じて処理されます。vision_analyze ツールはこのメソッドを明示的に呼び出し、スクリーンショットを読み取る必要があるツール(browser.* セット)は呼び出し前に画像を base64 にエンコードします。
Anthropic の OAuth フロー
Anthropic OAuth を使うと、API キーを提供する代わりに Claude.ai アカウントでサインインできます。フローは apps/agent/src/llm/oauth/ に実装されています。
/auth/anthropic/initで PKCE フローを開始し、認証 URL を返します。- ユーザーが URL にアクセスして承認すると、リダイレクトで戻ってきます。
/auth/anthropic/exchangeでコードをリフレッシュトークンと交換します。- リフレッシュトークンはローカルキーで暗号化され、SQLite に保存されます。
- LLM 呼び出しのたびに、必要に応じてアクセストークンが透過的にリフレッシュされます。
同じパターンが OpenAI と OpenRouter にも適用されます。正確なルートは Auth エンドポイントリファレンスを参照してください。
プロバイダーの追加方法
新しい LLM プロバイダーをサポートするには、以下の手順を実施します。
apps/agent/src/llm/<name>-wire.tsにLLMClientを実装します。プロンプトキャッシュのサポートは任意ですが、強く推奨されます。select-provider.tsに選択ブランチを追加します。.env.exampleと環境変数一覧に環境変数を追加します。tests/fakes/llm/のワイヤーレベルフェイクを使い、tests/integration/llm/以下に統合テストを作成します。
プロバイダー固有のコードパスをエージェントループに直接追加しないでください。プロバイダー固有の特性がある場合は、ワイヤーレイヤー内に隠蔽してください。