diff --git a/AGENTS.md b/AGENTS.md index e4ebbc9a..a2dc32f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -設計指針を固め、全体の設計を進めるために、全体の俯瞰と細かいディテールを往復している。 +全体設計が概ね固まり、随所の細かい仕様を詰めながら実装を進めている。 --- diff --git a/CLAUDE.md b/CLAUDE.md index e4ebbc9a..a2dc32f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -設計指針を固め、全体の設計を進めるために、全体の俯瞰と細かいディテールを往復している。 +全体設計が概ね固まり、随所の細かい仕様を詰めながら実装を進めている。 --- diff --git a/TODO.md b/TODO.md index a6846208..dc1425b8 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,10 @@ - [ ] ツール設計 - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) +- [ ] LLM プロバイダ統合 + - [ ] モデル設定の構造再編(providers 層廃止 + Pod 宣言化) → [tickets/llm-model-config.md](tickets/llm-model-config.md) + - [ ] OpenAI Responses scheme の新設 → [tickets/llm-scheme-openai-responses.md](tickets/llm-scheme-openai-responses.md) + - [ ] Codex OAuth 認証の流用 → [tickets/llm-auth-codex-oauth.md](tickets/llm-auth-codex-oauth.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) - [ ] Pod オーケストレーション - [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md) diff --git a/tickets/llm-auth-codex-oauth.md b/tickets/llm-auth-codex-oauth.md new file mode 100644 index 00000000..756e1aa2 --- /dev/null +++ b/tickets/llm-auth-codex-oauth.md @@ -0,0 +1,70 @@ +# Codex OAuth 認証の流用 + +## 背景 + +決定済み方針(`docs/plan/llm_providers.md`)で、ChatGPT サブスクリプションの OAuth トークンを流用して OpenAI Responses API を叩く経路を第一級サポートとする。OpenAI は Codex CLI を Apache-2.0 で公開し、ChatGPT OAuth の第三者ツール利用を service terms で名指し禁止していない(互換経路)。 + +Codex CLI の実装(github.com/openai/codex、`codex-rs/login/` 配下)から以下が確定: + +- トークンは `~/.codex/auth.json` に `{ auth_mode, tokens: { id_token, access_token, refresh_token, account_id }, last_refresh }` 形式で保存 +- リクエストは `https://chatgpt.com/backend-api/v1/responses` に投げる(Responses API wire) +- 認証ヘッダは `Authorization: Bearer ` + `ChatGPT-Account-ID: `(+ FedRAMP 組織で `X-OpenAI-Fedramp: true`) +- リフレッシュは `https://auth.openai.com/oauth/token` に `grant_type=refresh_token`, `client_id=app_EMoamEEZ73f0CkXaXp7hrann`, `refresh_token` を POST + +## 要件 + +1. **`AuthRef::CodexOAuth` の追加**: `llm-model-config` で定義する認証列挙型に ChatGPT OAuth バリアントを追加 + +2. **トークン読み取り**: `~/.codex/auth.json` を読み、以下を取り出す + - `tokens.access_token` + - `tokens.refresh_token` + - `tokens.account_id` + - `last_refresh`(期限判定用) + - `tokens.id_token` の JWT claims(`chatgpt_account_is_fedramp`、`chatgpt_plan_type` 参照) + +3. **ヘッダ注入**: `HttpTransport` が `AuthRef::CodexOAuth` を解決するとき以下を組み立てる + - `Authorization: Bearer ` + - `ChatGPT-Account-ID: ` + - FedRAMP 組織なら `X-OpenAI-Fedramp: true` + +4. **base_url の自動適用**: `ModelConfig.base_url` 未指定時は `https://chatgpt.com/backend-api` を既定とする。ユーザーが明示的に `https://api.openai.com` を指定した場合は尊重(API key 経路と共用したいケース用) + +5. **トークンリフレッシュ**: + - `https://auth.openai.com/oauth/token` に `{ client_id: "app_EMoamEEZ73f0CkXaXp7hrann", grant_type: "refresh_token", refresh_token }` を POST + - 有効期限が近い(access_token が期限切れ or 残り時間が閾値以下)とき自動更新 + - 更新後の token を `~/.codex/auth.json` に書き戻す + - 複数プロセス(Codex CLI との並行実行等)を想定した排他制御 + +6. **scheme/openai_responses との組合せで動作**: `ModelConfig { scheme: OpenAIResponses, base_url: (既定), model_id: "gpt-5-codex" 等, auth: CodexOAuth }` で ChatGPT 枠を使って Codex 相当の動作ができる + +7. **完了時の動作**: ChatGPT アカウント保持者が `codex login` 済みの環境で insomnia を起動すると、追加設定なしで Codex と同じモデル(`gpt-5-codex` 等)が利用可能 + +## 設計課題 + +### 1. auth.json の書き戻しと競合制御 + +`~/.codex/auth.json` は Codex CLI と共用。両方が並行動作する場面で refresh token が古い値で上書きされると再認証が必要になる。ファイルロックを取るか、更新前に再読み込みして最新値で書くか。 + +### 2. refresh token の失効時の挙動 + +refresh token も失効するケース(Anthropic のサーバ側遮断のようにサブスク側が塞ぐケース)で、ユーザーにどう通知するか。insomnia CLI で `codex login` の再実行を促すメッセージを返す。 + +### 3. ChatGPT OAuth 専用モデル + +`gpt-5-codex` / `codex-mini-latest` 等、ChatGPT backend でのみ利用可能なモデル ID がある場合、API key 経路と区別が必要。`ModelCapability` で「CodexOAuth でのみ有効」フラグを持つか、API 側の 401 を受けて動的にフォールバックするか。 + +### 4. セキュリティと権限 + +`~/.codex/auth.json` のパーミッション(600)を尊重。読み取り時にパーミッション確認 + 書き込み時にパーミッション再設定。 + +## Scope 外 + +- Claude Pro/Max OAuth 経路(方針上非採用) +- `claude -p` CLI fork 経路 +- `codex login` 自体の実装(Codex CLI に任せ、insomnia は auth.json を読むのみ) +- ChatGPT backend の rate limit 観測(Retry-After 処理は HttpTransport 共通の責務) + +## 依存 + +- `tickets/llm-model-config.md`(`AuthRef` / `HttpTransport` 構造) +- `tickets/llm-scheme-openai-responses.md`(`/v1/responses` wire format) diff --git a/tickets/llm-model-config.md b/tickets/llm-model-config.md new file mode 100644 index 00000000..14c5fa41 --- /dev/null +++ b/tickets/llm-model-config.md @@ -0,0 +1,118 @@ +# LLM モデル設定の再編 + +## 背景 + +決定済みの LLM プロバイダサポート方針(`docs/plan/llm_providers.md`)に従って llm-worker のプロバイダ層を再編する。Pod 側で「使う LLM モデル」を宣言する構造にし、共通の通信層 + scheme の組合せで任意のプロバイダを収容できるようにする。 + +### 現状の問題 + +- `crates/llm-worker/src/llm_client/providers/{anthropic,openai,gemini,ollama}.rs` は各々 HTTP 骨格 + scheme 呼び出しの薄いアダプタで、構造が重複 +- `providers/ollama.rs` (67行) は scheme 未分離で整合性が崩れている +- OpenAI 互換ルーター系(xAI / Groq / OpenRouter / Together / BLACKBOX 等)を追加するたびに新ファイルを書く必要がある + +## 要件 + +1. **Pod マニフェストで LLM モデルを宣言できる** + + ``` + ModelConfig { + scheme: Scheme, // Anthropic / OpenAIChat / OpenAIResponses / Gemini + base_url: Url, + model_id: String, + auth: AuthRef, + } + ``` + + 継承(commit ebee0b9)と整合し、親 Pod の定義を子 Pod が override 可能。 + +2. **`providers/` 層の廃止**: llm-worker は `HttpTransport` 相当の汎用通信層 1 本を持ち、`ModelConfig` を食わせてインスタンス化する + +3. **既存 scheme の再編**: + - `scheme/openai` を `scheme/openai_chat` にリネーム + - `scheme/anthropic` / `scheme/gemini` はそのまま + - `scheme/openai_responses` は別チケット(llm-scheme-openai-responses)で新設 + - Ollama は **scheme/anthropic を base_url 差し替えで流用**(独自 scheme は作らない) + +4. **認証の分離**: `AuthRef` は `ApiKey(EnvVar | ConfigRef)` / `CodexOAuth` / `None` を表現でき、scheme とは直交する層で管理される。`CodexOAuth` の実装自体は別チケット(llm-auth-codex-oauth) + +5. **決定済みプロバイダ方針との整合**: + - 第一級: Ollama / Codex OAuth / Anthropic API + - 二次: OpenAI 互換共通枠(`{ scheme: OpenAIChat, base_url: 各社, auth: ApiKey }` の宣言だけで収容) + +6. **ModelCapability を分離**: モデルに紐づく機能差を別メタデータとして表現 + + ``` + ModelCapability { + tool_calling: ToolCallingSupport, // parallel 可否含む + structured_output: StructuredOutput, // JsonObject / JsonSchema + reasoning: Option, // effort / budget_tokens + vision: bool, + prompt_caching: CacheStrategy, // Explicit { max_breakpoints } / Auto + } + ``` + + `ReasoningControl { effort, budget_tokens }` は共通型、scheme アダプタで各社形式に投影。プロバイダ側高次ツール(web_search 等)は採用しないため軸から除外。 + +7. **Streaming は現状維持**: 既存 `BlockStart / BlockDelta / BlockStop / BlockAbort` + `DeltaContent::{Text, Thinking, InputJson}` を変更しない。Gemini や Ollama のように ToolCall 引数 delta を送らないプロバイダは scheme アダプタで「BlockStart → InputJson(全体 1 回) → BlockStop」の擬似ストリーム化で吸収 + +8. **Ollama 運用の注意点**: scheme/anthropic 流用前提で以下を守る + - `cache_control` は送らない(`ModelCapability::prompt_caching = Auto`) + - `tool_choice` / `metadata` / URL 画像は送らない(Ollama 側非対応) + - `/v1/messages/count_tokens` は叩かない(issue #13949 でサーバ不安定化) + - `/v1/chat/completions` は stream+tools バグ (#9092) のため使わない + +9. **完了時の動作**: 既存の動作は変わらず、Pod マニフェストで `ModelConfig` を宣言するだけでモデル切替できる。OpenAI 互換の新規プロバイダは新コードなしで追加可能 + +## 設計決定 + +### 1. `Scheme` trait の境界(方針A: 全面抽象化) + +trait で URL 組立・認証要件・body 変換・SSE パースをすべて抽象化し、`HttpTransport` は 1 本にする。trait スケッチ: + +```rust +trait Scheme { + fn path(&self, model: &str) -> String; // "/v1/messages" 等 + fn required_auth(&self) -> AuthRequirement; // Bearer / XApiKey / QueryParam / None + fn additional_headers(&self) -> Vec<(&str, String)>; // anthropic-version 等 + fn build_request_body(&self, model: &str, req: &Request, cap: &ModelCapability) -> Value; + fn parse_sse(&self, event_type: &str, data: &str) -> Result, ClientError>; + fn default_base_url(&self) -> &'static str; +} +``` + +`parse_sse` は `Vec` に統一(Anthropic は 1 要素 Vec で扱う)。 + +### 2. `AuthRef` と `Scheme` の組合せ検証(方針B: 構築時検証) + +`Scheme::required_auth()` で要求する `AuthRequirement` を宣言し、`build_client` 時に `AuthRef` と照合。非対応組合せは構築エラーにする(実行時に落とさない)。 + +### 3. `crates/provider/` の去就(方針A: 残す) + +`provider` クレートは保持。`build_client(ModelConfig) -> Box` は `(Scheme, AuthRef)` 照合 + `HttpTransport::new` の薄ラッパーに縮退する。`~/.codex/auth.json` 読み取り等の認証ストア解決は `provider` クレートに肉付けしていく(llm-worker は低レベル基盤に留める方針と整合)。 + +### 4. `ModelCapability` の保持(方針: ハイブリッド) + +- scheme 実装側に `model_id → ModelCapability` の静的テーブルを持つ(既知モデル分) +- `ModelConfig` で明示宣言すれば override +- 未知モデルは scheme ごとの安全側デフォルト(`prompt_caching: Auto` 等)にフォールバック + +今チケットのスコープ: +- 型定義 + 既知モデルの固定値 +- `CacheStrategy::Explicit` で `cache_control` マーカー挿入(既存実装を capability で分岐) +- `ReasoningControl { effort, budget_tokens }` を scheme 側で各社形式に投影 +- `CacheStrategy::Auto` は何もしない(Ollama で重要) + +### 5. マニフェスト継承との統合(方針: フィールド単位 override) + +`ProviderConfigPartial` と同じ方針で、`ModelConfig` の全フィールド(`scheme` / `base_url` / `model_id` / `auth`)を `Option` で継承。子 Pod は `base_url` だけ差し替える等が可能。 + +### 6. TOML 後方互換 + +旧 `[provider] kind = "..."` フォーマットは互換を切る。新 `[model]` セクションで `scheme` / `base_url` / `model_id` / `auth` を宣言する。 + +## Scope 外 + +- OpenAI Responses scheme の新設(`tickets/llm-scheme-openai-responses.md`) +- Codex OAuth 認証アダプタの実装(`tickets/llm-auth-codex-oauth.md`) +- OpenAI 互換ルーター各社の動作確認 +- プロバイダ選択 UI(将来の native GUI / TUI 拡張) diff --git a/tickets/llm-scheme-openai-responses.md b/tickets/llm-scheme-openai-responses.md new file mode 100644 index 00000000..2319a7b9 --- /dev/null +++ b/tickets/llm-scheme-openai-responses.md @@ -0,0 +1,68 @@ +# OpenAI Responses scheme の新設 + +## 背景 + +現状の `crates/llm-worker/src/llm_client/scheme/openai` は OpenAI Chat Completions (`/v1/chat/completions`) wire format のみ実装。OpenAI の Responses API (`/v1/responses`) はリクエスト body・SSE イベント構造ともに Chat Completions と別物で、同じ scheme には乗らない。 + +Codex CLI (github.com/openai/codex) の実装を確認したところ、ChatGPT OAuth 経路でも OpenAI API Key 経路でもすべて `/v1/responses` を叩いており、Chat Completions は使っていない。Codex 流用(別チケット `llm-auth-codex-oauth`)を実現する前提として、この scheme が必要。 + +また OpenAI 本家の最新モデル(GPT-5 系・o シリーズの reasoning)は Responses API 経由が主要な経路であり、長期的にも Chat Completions の地位は低下していく。 + +## 要件 + +1. **`scheme/openai_responses` を新設**し、`HttpTransport` に差し込めるようにする + +2. **リクエスト body** は `/v1/responses` の item-based 形式: + - `model`, `instructions` (system prompt 相当), `input: [ResponseItem]`, `tools`, `tool_choice`, `parallel_tool_calls` + - `reasoning: { effort?, summary? }` + - `store`, `stream: true`, `include: [String]` + - `service_tier?`, `prompt_cache_key?`, `text?: { verbosity?, format? }` + - `previous_response_id` は **使わない**(stateless で運用、履歴は insomnia 側管理) + +3. **SSE event パース**: + - `response.created` / `response.completed` / `response.failed` / `response.incomplete` + - `response.output_item.added` / `response.output_item.done` + - `response.output_text.delta` + - `response.custom_tool_call_input.delta`(ToolCall 引数の partial JSON) + - `response.reasoning_text.delta` / `response.reasoning_summary_text.delta` + +4. **BlockType / DeltaContent との対応**: + - `response.output_text.delta` → `DeltaContent::Text` + - `response.reasoning_text.delta` / `response.reasoning_summary_text.delta` → `DeltaContent::Thinking` + - `response.custom_tool_call_input.delta` → `DeltaContent::InputJson` + - `response.output_item.done` (tool_use) → `BlockMetadata::ToolUse { id, name }` の `BlockStart` 生成 + +5. **reasoning の item 構造対応**: `summary[]` / `encrypted_content` を持つ reasoning item の送受信をロスなく扱える + - 送信時は `BlockMetadata::Thinking` から `input[]` に再構築 + - 受信時は `BlockType::Thinking` のブロックとしてストリームに流す + +6. **認証は `AuthRef::ApiKey` のみ対応**: `Authorization: Bearer ` ヘッダ。`base_url` デフォルトは `https://api.openai.com`、パスは `/v1/responses`。ChatGPT OAuth 経路(`CodexOAuth`)は別チケット(`llm-auth-codex-oauth`)で追加 + +7. **Usage の正規化**: `response.completed` の `usage: { input_tokens, output_tokens, total_tokens }` を `UsageEvent` に変換。Chat Completions の `prompt_tokens` 等との表記揺れを scheme 側で吸収 + +8. **完了時の動作**: OpenAI API key (`OPENAI_API_KEY`) + モデル `gpt-5` 等で `ModelConfig { scheme: OpenAIResponses, base_url: https://api.openai.com, model_id: "gpt-5", auth: ApiKey }` を宣言すると、reasoning + tool call を含む会話が動作する + +## 設計課題 + +### 1. reasoning item の encrypted_content + +reasoning item の `encrypted_content` はサーバ側で暗号化された状態で返されることがあり、再送時にそのまま添える必要がある(ZDR 組織や `store=false` 運用時)。insomnia の `Item` enum に透過的に保持する仕組みが要る。 + +### 2. `include[]` と `store` のデフォルト + +- `include: ["reasoning.encrypted_content"]` を常に付けるか、capability / config で制御するか +- `store=false` をデフォルトにするか `true` にするか(ZDR 既定なら false) + +### 3. Responses 非対応パラメータ + +`service_tier` / `prompt_cache_key` / `text.verbosity` は当面不要かもしれないが、将来対応時に scheme 拡張で入れられる構造にしておく。 + +## Scope 外 + +- ChatGPT OAuth 認証(`llm-auth-codex-oauth`) +- `previous_response_id` を使う stateful 運用 +- 高次ツール(`web_search` / `code_interpreter` / `computer_use`)— insomnia では採用しない方針 + +## 依存 + +- `tickets/llm-model-config.md`(`HttpTransport` 構造と `AuthRef` が前提)