From bd8c5601c7fc3e8ffffe5e570f9aa1ee26a72d73 Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 20 Apr 2026 03:00:48 +0900 Subject: [PATCH] =?UTF-8?q?openai-responses=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - tickets/llm-scheme-openai-responses.md | 96 ------------------- tickets/llm-scheme-openai-responses.review.md | 77 --------------- 3 files changed, 174 deletions(-) delete mode 100644 tickets/llm-scheme-openai-responses.md delete mode 100644 tickets/llm-scheme-openai-responses.review.md diff --git a/TODO.md b/TODO.md index 0185f499..ce568e9d 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,6 @@ - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] LLM プロバイダ統合 - - [ ] 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 オーケストレーション diff --git a/tickets/llm-scheme-openai-responses.md b/tickets/llm-scheme-openai-responses.md deleted file mode 100644 index 219bf608..00000000 --- a/tickets/llm-scheme-openai-responses.md +++ /dev/null @@ -1,96 +0,0 @@ -# OpenAI Responses scheme の新設 - -> **レビュー完了(close 可)** — 詳細は [`llm-scheme-openai-responses.review.md`](llm-scheme-openai-responses.review.md) -> 9 要件すべて達成、指摘事項は優先度低の 2 件のみ(tool 引数 delta 先行時の空メタデータ、`tools[].strict` ハードコード)。いずれも実害薄。 - - -## 背景 - -現状の `crates/llm-worker/src/llm_client/scheme/openai_chat` は 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: "auto"` / `parallel_tool_calls: true` は **scheme 固定値**で常時送信(将来必要になれば Request / RequestConfig に昇格、今は YAGNI) - - `reasoning: { effort?, summary? }` は `ReasoningControl` から投影 - - **`store: false` + `include: ["reasoning.encrypted_content"]` を scheme 固定値**で送信(stateless 運用 + 再送のため encrypted reasoning を取得) - - `stream: true` 固定 - - `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.content_part.added` / `response.content_part.done` - - `response.output_text.delta` - - `response.function_call_arguments.delta`(通常 function tool の引数 partial JSON) - - `response.custom_tool_call_input.delta`(custom tool のフリーフォーム入力 partial JSON) - - `response.reasoning_text.delta` / `response.reasoning_summary_text.delta` - -4. **BlockType / DeltaContent との対応**: - - **text BlockStart** は `response.content_part.added`(Anthropic の `content_block_start` と対称) - - **tool_use BlockStart** は `response.output_item.added`(id と name が確定する時点、streaming に乗せるためここ) - - `response.output_text.delta` → `DeltaContent::Text` - - `response.reasoning_text.delta` / `response.reasoning_summary_text.delta` → `DeltaContent::Thinking` - - `response.function_call_arguments.delta` と `response.custom_tool_call_input.delta` → 両方とも `DeltaContent::InputJson` に正規化 - - `response.content_part.done` / `response.output_item.done` → `BlockStop` - -5. **`Item::Reasoning` の拡張**(llm-worker/types.rs への変更を含む): - - ```rust - Item::Reasoning { - text: String, - summary: Vec, - encrypted_content: Option, - } - ``` - - - 送信時は `input[]` の reasoning item に再構築(`encrypted_content` があれば添える) - - 受信時は SSE から `text` / `summary[]` / `encrypted_content` を組み立てて `Item::Reasoning` に格納 - - 既存 `Item::Reasoning { text }` の 1 フィールドからの拡張。`summary` は空 Vec、`encrypted_content` は `None` で既存互換を保つ - - 将来 Anthropic の extended thinking で `signature: Option` を追加する余地を残す - -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` に変換 - -8. **capability テーブル**: GPT-5 / o3 / o4 のモデル ID 判定は `scheme/openai_chat/capability.rs` と重複するため **共通関数に切り出して共有**(配置は `scheme/openai_chat/capability.rs` に `pub(crate) fn classify(model_id) -> Option` を置くか、`scheme/openai_common/` を切り出すかは実装時判断)。Responses 側は `ReasoningSupport::Effort` 固定でマッピング - -9. **完了時の動作**: 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. scheme-specific 設定の override フィールド - -`store` / `include[]` を scheme 固定値にしたが、将来 ZDR 非対応環境で `store=true` を許したくなる可能性がある。`OpenAIResponsesScheme` 自身にフィールド (`store: bool`, `include_encrypted_content: bool` 等) を持たせ、`new()` 時に上書きできる形にする。`ModelCapability` には入れない(scheme-specific な wire 設定なので)。 - -### 2. 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 では採用しない方針 -- `tool_choice` / `parallel_tool_calls` の Request 昇格(必要性が出てから別チケット) - -## 依存 - -- `tickets/llm-model-config.md` 完了済(`HttpTransport` 構造と `AuthRef` が前提) - -## 影響範囲 - -llm-worker 単独ではなく以下にまたがる: -- `crates/llm-worker/src/llm_client/types.rs`: `Item::Reasoning` の拡張 -- `crates/llm-worker/src/llm_client/scheme/openai_responses/`: 新規 -- `crates/llm-worker/src/llm_client/scheme/openai_chat/capability.rs`: モデル family 判定を `pub(crate)` に露出 -- `crates/llm-worker/src/llm_client/scheme/mod.rs`: `pub mod openai_responses;` -- `crates/provider/src/lib.rs`: `build_client` の `SchemeKind::OpenaiResponses` アームを `SchemeNotImplemented` から実装に差し替え diff --git a/tickets/llm-scheme-openai-responses.review.md b/tickets/llm-scheme-openai-responses.review.md deleted file mode 100644 index 176911b9..00000000 --- a/tickets/llm-scheme-openai-responses.review.md +++ /dev/null @@ -1,77 +0,0 @@ -# OpenAI Responses scheme の新設 — レビュー - -## 前提・要件の再確認 - -チケット本体の 9 要件 + 2 設計課題を前提に、実装が意図と整合しているかを確認した。`cargo check`(warning 1 / 旧由来)・`cargo test --workspace`(全 pass)通過。変更量: 新規 5 ファイル(1407 行)+ 既存 5 ファイル微修正。 - -## 要件達成度 - -| # | 要件 | 状況 | メモ | -|---|---|---|---| -| 1 | `scheme/openai_responses` 新設、`HttpTransport` に差し込める | ✓ | `Scheme` trait 実装完了 | -| 2 | Request body: `tool_choice: "auto"` / `parallel_tool_calls: true` / `store: false` / `include` / `stream: true` 固定、`reasoning` 投影 | ✓ | `ResponsesRequest` 構造体に全項目、`build_request` で capability 照合して reasoning 投影 | -| 3 | SSE event パース (response.* 一式) | ✓ + α | ticket 列挙に加えて `response.content_part.done` / `response.reasoning_summary_part.added/done` / top-level `error` もカバー | -| 4 | BlockType / DeltaContent 対応(text は `content_part.added`、tool_use は `output_item.added` で BlockStart) | ✓ | `OpenAIResponsesState` の 3 種 SlotKey(OutputItem / ContentPart / Summary)で (output_index, content_index) → flat index を管理 | -| 5 | `Item::Reasoning` 拡張(text + summary + encrypted_content) | ✓ | `with_reasoning_summary` / `with_encrypted_content` ビルダー追加、既存コンストラクタは空値で互換 | -| 6 | `AuthRef::ApiKey` / `Authorization: Bearer` / base_url `https://api.openai.com` / パス `/v1/responses` | ✓ | scheme_impl.rs の `required_auth` / `default_base_url` / `path` | -| 7 | Usage 正規化 | ✓ | `response.completed` の `usage` を `UsageEvent` に変換、`total_tokens` 未提供時は input+output で補完 | -| 8 | capability 共通判定関数 | ✓ | `openai_chat/capability.rs::classify -> OpenAiFamily` を `pub(crate)` で公開、`openai_responses/capability.rs::lookup` が共有 | -| 9 | 完了時動作 | ✓ | `provider/lib.rs::build_client` の `OpenaiResponses` アームが `SchemeNotImplemented` から実装に差し替え済み | - -## 設計決定への反映 - -| 決定 | 反映 | -|---|---| -| `store` / `include[]` を `OpenAIResponsesScheme` フィールドで override 可能、`ModelCapability` には入れない | ✓ `with_store` / `with_include_encrypted_content` ビルダー、デフォルトは stateless + ZDR 相当 | -| `ReasoningSupport::Effort / Both` 対応時のみ `reasoning` 送信 | ✓ `build_request` で capability と `request.config.reasoning.effort` の両方が揃う時のみ投影 | -| Responses 未使用パラメータ (`service_tier` / `prompt_cache_key` / `text.verbosity`) は予約のみ | ✓ `ResponsesRequest` 構造体には入れず、必要時に追加できる構造 | - -## アーキテクチャ評価 - -### 良い点 -- **`ensure_and_delta` による防御的設計**: `content_part.added` が欠落しても delta 単独で BlockStart + Delta を発行できる。`delta_without_prior_start_recovers` テストで確認済 -- **`OpenAIResponsesState` の 3 種 SlotKey**: `OutputItem` (tool 全体) / `ContentPart { output, content }` (text/reasoning) / `Summary { output, summary }` (reasoning 要約) で Responses の 2 次元座標を flat index に自然にマップ。`parallel_output_items_get_distinct_indices` で並列 tool call の独立性を検証 -- **テストの充実**: request 12 ケース (scheme defaults / tool_choice 固定 / role 別 content 型 / reasoning 有無 / round-trip / serialize shape)、events 10 ケース (text / function_call / custom_tool / reasoning_text / summary / 並列 / failed / unknown / 防御) -- **capability の一次情報共有**: `OpenAiFamily` enum を `openai_chat` に置いて両 scheme で共有、DRY と結合度のバランスが良い -- **`Item::Reasoning` 拡張の互換性**: `Vec::is_empty` / `Option::is_none` での skip_serializing、既存 `Item::reasoning()` コンストラクタは空値で互換。他 scheme の `request.rs` は `Item::Reasoning { text, .. }` で `..` 省略しており、追加フィールドで壊れない - -## 指摘事項 - -### 優先度: 低 - -#### 1. tool 引数 delta 先行時の空メタデータ - -`ensure_and_delta` が `function_call_arguments.delta` / `custom_tool_call_input.delta` で BlockStart 未発行の場合、`BlockMetadata::ToolUse { id: String::new(), name: String::new() }` を合成する。実運用では `output_item.added` が先行するはずで発動しないが、仮に発火すると後段で空 `call_id` を使うリスクがある。 - -対応案: -- `ToolUse` では `ensure_and_delta` を使わず、`output_item.added` 必須で、欠落時は warning ログ + Delta 破棄 -- もしくは現状維持で「防御コードとして warning」出す - -現状、`output_item.added` が保証されている Responses API の仕様に依存しており、実害は薄い。 - -#### 2. `tools[].strict: false` ハードコード - -`ResponseTool::strict: false` を常時送信。Responses の `strict: true` は JSON Schema 準拠を強制する。`ModelCapability::structured_output == JsonSchema` のときに `strict: true` に昇格させる余地があるが、本チケットではスコープ外として許容範囲。 - -### 優先度: 極低(構造的なスコープ内) - -#### 3. `summary` / `encrypted_content` が他 scheme で落ちる - -`Item::Reasoning { text, .. }` で `..` 省略されている他 scheme(Anthropic / OpenAI Chat / Gemini)の `request.rs` は `summary` と `encrypted_content` を送信しない。ただし: -- Anthropic は独自の `signature` が別途必要で将来拡張 -- OpenAI Chat と Gemini は reasoning を first-class では送らない -- scheme をまたいだ履歴引き継ぎは現状想定外 - -構造的には設計通り、問題なし。 - -#### 4. `OpenAIResponsesScheme` の override が `build_client` から届かない - -`with_store` / `with_include_encrypted_content` は存在するが、`build_client` 経由では常に `OpenAIResponsesScheme::new()` デフォルト。ZDR 非対応環境で `store=true` にする場合は provider 側で新しい経路が必要。 - -チケットの設計課題 1 で「将来対応」と明示されており、スコープ通り。 - -## 総合判定 - -**close 可能**。9 要件すべて達成、設計決定も正確に反映、テストも充実。指摘事項はいずれも実害が薄いか、チケット設計課題として明示されたスコープ内の将来対応。 - -`tickets/llm-auth-codex-oauth.md` (次のチケット) を進められる状態。