MINARA

LLM 統合

プロバイダー抽象化、プロンプトキャッシュ、モデルルーティング

Minara Agent は単一の内部インターフェースを通じて複数の LLM プロバイダーと通信します。そのため、エージェントループに手を加えることなく、Anthropic、OpenAI、OpenRouter、ローカルモデルを自由に切り替えられます。このページでは、プロバイダーレイヤー、プロンプトキャッシュの構造、モデルルーティングの仕組みを説明します。

ベンダー SDK を直接使わずカスタムルーターを使う理由は? エージェントループ、バックテスト、リフレクション、サブエージェントなど、各機能にはコストとレイテンシのトレードオフがあります。Minara のルーターを使えば、同じコードベースから、キャッシュ安定性の高い重い処理は Sonnet に、安価なバッチ評価は Haiku にルーティングできます。またプロバイダーがレートリミットに達した場合もグレースフルにフォールバックできます。ベンダー SDK を直接使うと、特定プロバイダーの料金モデルに縛られてしまいます。

実際の設定方法: 環境変数でプロバイダーを設定します(ANTHROPIC_API_KEYOPENAI_API_KEYOPENROUTER_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.tsAnthropicANTHROPIC_API_KEY
anthropic-oauth.tsAnthropic (Claude.ai)OAuth リフレッシュトークン
anthropic-wire.ts共有ワイヤープロトコル
openai-wire.tsOpenAI / OpenRouterOPENAI_API_KEY / OPENROUTER_API_KEY
openrouter.tsOpenRouter ルーターOPENROUTER_API_KEY

select-provider.ts は起動時に環境変数を参照し、以下の優先順位で具体的なクライアントを選択します。

  1. Anthropic OAuth(CLAUDE_CODE_OAUTH_TOKEN が設定されている場合)
  2. Anthropic API キー(ANTHROPIC_API_KEY が設定されている場合)
  3. OpenRouter(OPENROUTER_API_KEY が設定されている場合)
  4. 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 },
]

プロンプトビルダーが守る不変条件は以下の通りです。

  1. キャッシュ可能なブロックを先頭に置く。 キャッシュキーは厳密なプレフィックス一致のため、キャッシュ不可ブロックの後にあるものはキャッシュできません。
  2. キャッシュ可能なブロックは安定している。 Identity とカタログは、スキルレジストリかベースプロンプトが変更されたときのみ更新され、ターンごとには変わりません。
  3. 動的なコンテンツは末尾に置く。 アクティブなスキルフラグメント、シグナルコンテキスト、確認待ちの内容、会話履歴はすべてキャッシュ境界の後に配置します。

anthropic-wire.tsSystemPromptBlock[] を Anthropic の cache_control: {type: "ephemeral"} マーカーにマッピングし、レスポンスのメタデータからキャッシュヒット率を追跡します。この情報は構造化ログ上で llm.cache_read_input_tokensllm.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/ に実装されています。

  1. /auth/anthropic/init で PKCE フローを開始し、認証 URL を返します。
  2. ユーザーが URL にアクセスして承認すると、リダイレクトで戻ってきます。
  3. /auth/anthropic/exchange でコードをリフレッシュトークンと交換します。
  4. リフレッシュトークンはローカルキーで暗号化され、SQLite に保存されます。
  5. LLM 呼び出しのたびに、必要に応じてアクセストークンが透過的にリフレッシュされます。

同じパターンが OpenAI と OpenRouter にも適用されます。正確なルートは Auth エンドポイントリファレンスを参照してください。

プロバイダーの追加方法

新しい LLM プロバイダーをサポートするには、以下の手順を実施します。

  1. apps/agent/src/llm/<name>-wire.tsLLMClient を実装します。プロンプトキャッシュのサポートは任意ですが、強く推奨されます。
  2. select-provider.ts に選択ブランチを追加します。
  3. .env.example環境変数一覧に環境変数を追加します。
  4. tests/fakes/llm/ のワイヤーレベルフェイクを使い、tests/integration/llm/ 以下に統合テストを作成します。

プロバイダー固有のコードパスをエージェントループに直接追加しないでください。プロバイダー固有の特性がある場合は、ワイヤーレイヤー内に隠蔽してください。

目次