yoi/tickets/llm-scheme-openai-responses.md
2026-04-20 02:59:16 +09:00

97 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<S: Scheme>` に差し込めるようにする
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<String>,
encrypted_content: Option<String>,
}
```
- 送信時は `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<String>` を追加する余地を残す
6. **認証は `AuthRef::ApiKey` のみ対応**: `Authorization: Bearer <api_key>` ヘッダ。`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<OpenAiFamily>` を置くか、`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<S>` 構造と `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` から実装に差し替え