Files
agcore/design/pdd/28-phase28-openai-response-api-provider.md
T
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

791 lines
32 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.
# Phase 28-30 — OpenAI Response API Provider 实施方案
> **版本**v1 | **作者**Writer Agent | **日期**2026-07-20
>
> **阅读前提**:本文档假设读者已熟悉现有的 Provider 实现模式(`AnthropicProvider` 独立实现方式)、IR 类型系统(`MessageRequest` / `MessageResponse` / `ContentBlock` / `StreamEvent` / `LlmProvider trait`)以及 Cargo features 门控机制。
>
> **前置条件**v0.3.2Phase 20-27)已发布,Cargo features 拆分完成,CI 矩阵 6 种组合全部通过。
---
## 1. 背景与目标
### 1.1 背景
OpenAI 于 2025 年下半年发布了 **Response API**`POST /responses`),作为 Chat Completions API`POST /chat/completions`)的下一代接口。Response API 不仅提供了更简洁的请求/响应结构,还将 `web_search``file_search``computer_use` 等内置工具提升为一等公民,并引入了 `previous_response_id` 多轮续写等新机制。
agcore 当前通过 `GenericOpenaiProvider` 实现了 OpenAI Chat Completions 协议。`ProviderType::OpenaiResponse` 枚举项已在 `src/llm/provider.rs` 中定义,但工厂函数返回 `Err("Phase 1 暂不实现;请使用 OpenaiChat")`
### 1.2 目标
- 实现独立的 `OpenaiResponseProvider`(不套用 `GenericOpenaiProvider`,参考 `AnthropicProvider` 模式)
- 覆盖 Response API 的核心能力:文本对话、流式输出、Vision 输入、工具调用(function calling
- 新增独立 feature `provider-openai-response`,加入 `full` 快捷组合
- 内置工具(`web_search` / `file_search` / `computer_use`)通过 `MessageRequest.extra` 逃生舱传递
- 多轮接续第一版走全量消息历史模式
### 1.3 范围
| 维度 | 包含 | 不包含 |
|------|------|--------|
| 协议端点 | `POST /responses` | `/responses/{id}/input_items` 等管理端点 |
| 输入模式 | 全量消息历史 + `previous_response_id` | 增量续写优化 |
| 内置工具 | 通过 `extra` 逃生舱透传 | 原生 ToolDef 结构改动 |
| 流式 | SSE 语义事件 → `StreamEvent` | — |
| 结构化输出 | `text.format` | 暂不专项封装 |
---
## 2. 需求分析
### 2.1 功能需求
| # | 需求 | 优先级 | 说明 |
|---|------|--------|------|
| F1 | 文本对话(非流式 + 流式) | P0 | 最基础的对话能力 |
| F2 | Vision 图片输入 | P0 | `UserImage``input_image` |
| F3 | Function Calling 工具调用 | P0 | `ToolDef``{type: "function", ...}` |
| F4 | 多轮接续 | P1 | 全量消息历史模式 |
| F5 | System 消息处理 | P0 | 多个 System 消息拼接到 `instructions` |
| F6 | 流式 SSE 事件映射 | P0 | 按 Response API SSE 事件序列映射 |
| F7 | 内置工具逃生舱 | P2 | `extra` 字段透传 `web_search` / `file_search` |
| F8 | 结构化输出逃生舱 | P2 | `extra` 字段透传 `text.format` |
### 2.2 非功能需求
| # | 需求 | 指标 |
|---|------|------|
| N1 | 编译隔离 | 新增 feature 不增加 `light` / `chat` 组合的依赖 |
| N2 | 测试覆盖 | wiremock 覆盖非流式 + 流式 + 错误路径 |
| N3 | 错误映射 | 复用 `GenericOpenaiProvider` 的错误映射逻辑 |
| N4 | Clippy 合规 | `cargo clippy --all-features --lib -- -D warnings` 通过 |
### 2.3 与 Chat Completions 的差异回顾
| 维度 | Chat Completions | Response API |
|------|-----------------|--------------|
| 端点 | `POST /chat/completions` | `POST /responses` |
| 输入 | `messages: [{role, content}]` | `input: string \| items[]` + 顶层 `instructions` |
| 输出 | `choices[n].message` | `output: []` 异构 items 数组 |
| 内置工具 | 无(仅 function calling | `web_search` / `file_search` / `computer_use` 一等公民 |
| 多轮接续 | 调用方拼接 messages | `previous_response_id` 参数 或 全量回传 |
| 流式 | SSE chunk `choices[n].delta` | SSE 语义事件:`response.text.delta` / `response.output_item.added` 等 |
| 结构化输出 | `response_format` | `text.format` |
| 认证 | `Authorization: Bearer` | 相同 |
| 错误结构 | 相同(401/429/500 | 相同 |
---
## 3. 方案设计
### 3.1 设计决策
| # | 决策 | 选项 | 选择 | 理由 |
|---|------|------|------|------|
| D1 | 实现方式 | 独立 Provider vs 套用 GenericOpenaiProvider | **独立 Provider** | Response API 请求/响应结构与 Chat Completions 差异过大,序列化/反序列化无共用价值 |
| D2 | Feature 粒度 | 合并到 `provider-openai` vs 独立 | **独立 feature** | 与 `AnthropicProvider` 对齐,避免 `full` 组合膨胀 |
| D3 | 加入快捷组合 | 加入 `full` 但不加入 `light` | **`full` 包含** | Response API 属于高级能力,`light` 保持轻量 |
| D4 | 多轮方案 | 全量历史 vs 增量 | **全量历史(模式 A** | 功能正确,无需改动 `LlmCycle` |
| D5 | 内置工具支持 | 改 ToolDef vs extra 逃生舱 | **extra 逃生舱** | 不改已有 IR 类型,最小侵入 |
### 3.2 Feature 定义
```toml
provider-openai-response = ["llm", "reqwest", "bytes", "futures-util"]
```
`provider-openai` / `provider-anthropic` 的依赖集合一致——`llm` 已包含 `tokio` / `async-stream` / `futures-core` / `futures-util` / `tokio-stream`,此处补充 `reqwest`HTTP 客户端)和 `bytes`(流式 buffer 操作)。
`full` 快捷组合追加 `"provider-openai-response"`
### 3.3 新增文件
所有实现集中在单一文件:
```
src/llm/provider/openai_response.rs ← 全部实现(Wire 类型 + Provider 结构体 + 请求转换 + 响应转换 + 流式处理 + 测试)
```
不在 `provider/` 下创建子目录。模块声明在 `src/llm.rs`,在现有 Provider features cfg 条件中追加 `feature = "provider-openai-response"`
```rust
#[cfg(any(
feature = "provider-openai",
feature = "provider-anthropic",
feature = "provider-deepseek",
feature = "provider-qwen",
feature = "provider-ollama",
feature = "provider-openai-response",
))]
pub mod provider;
```
### 3.4 架构概览
```
┌──────────────────────────────────────────────┐
│ OpenaiResponseProvider │
│ ┌──────────────────────────────────────────┐ │
│ │ convert_request() │ │
│ │ MessageRequest → OpenaiResponseRequest │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────┐ │
│ │ HTTP POST /responses │ │
│ │ (reqwest Client) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────┐ │
│ │ convert_response() │ │
│ │ OpenaiResponseBody → MessageResponse │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────┐ │
│ │ ResponseSseEventStream │ │
│ │ SSE bytes → StreamEvent 流 │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
### 3.5 Wire 类型设计
#### 请求体类型
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct OpenaiResponseRequest {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
pub input: Vec<ResponseInputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ResponseTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Value>,
}
```
#### Input Item 枚举
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum ResponseInputItem {
Message {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
item_type: Option<String>, // 可选,固定为 "message"assistant 回传时使用)
role: String,
content: Vec<ResponseInputContent>,
},
FunctionCall {
#[serde(rename = "type")]
item_type: String, // 固定为 "function_call"
call_id: String,
name: String,
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
FunctionCallOutput {
#[serde(rename = "type")]
item_type: String, // 固定为 "function_call_output"
call_id: String,
output: String,
},
}
> ** `ResponseInputItem` `ResponseOutputItem` **
>
> - **`ResponseInputItem`**`#[serde(untagged)]`****`convert_request` `item_type` untagged fallback
> - **`ResponseOutputItem`** untagged`item_type: String` ****`convert_response` API `item_type` §3.7 `ContentBlock::Extension` fallback item serde
/// 消息内容块(嵌套在 Message 变体的 content 数组中)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ResponseInputContent {
InputText {
text: String,
},
InputImage {
image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
}
```
> **补充说明**Response API 的 `input` 字段还支持简化格式——`input: "Hello"`(单字符串)或 `input: ["Hello", "Hi"]`(字符串数组),但这些格式只能表达纯文本消息。为支持多模态内容(文本 + 图片)和工具调用,本实现使用完整的消息对象数组格式。
#### Tool 类型
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ResponseTool {
Function {
name: String,
description: String,
parameters: Value,
},
}
```
#### 响应体类型
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct OpenaiResponseBody {
pub id: String,
pub model: String,
pub output: Vec<ResponseOutputItem>,
pub usage: Usage,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ResponseOutputItem {
pub id: String,
#[serde(rename = "type")]
pub item_type: String,
pub status: Option<String>,
pub role: Option<String>,
pub content: Option<Vec<ResponseContentPart>>,
pub call_id: Option<String>,
pub name: Option<String>,
pub arguments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ResponseContentPart {
#[serde(rename = "type")]
pub part_type: String,
pub text: Option<String>,
}
```
#### SSE 事件类型
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ResponseSseEvent {
#[serde(rename = "response.created")]
ResponseCreated { response: ResponseSseMeta },
#[serde(rename = "response.completed")]
ResponseCompleted { response: ResponseSseMeta },
#[serde(rename = "response.failed")]
ResponseFailed { error: Option<serde_json::Value> },
#[serde(rename = "response.output_item.added")]
ResponseOutputItemAdded { item: ResponseOutputItem },
#[serde(rename = "response.output_item.done")]
ResponseOutputItemDone { item: ResponseOutputItem },
#[serde(rename = "response.output_text.delta")]
ResponseOutputTextDelta { delta: String, item_id: String },
#[serde(rename = "response.output_text.done")]
ResponseOutputTextDone { text: String, item_id: String },
#[serde(rename = "response.refusal.delta")]
ResponseRefusalDelta { delta: String, item_id: String },
#[serde(rename = "response.refusal.done")]
ResponseRefusalDone { refusal: String, item_id: String },
#[serde(rename = "response.function_call_arguments.delta")]
ResponseFunctionCallArgumentsDelta { delta: String, item_id: String },
#[serde(rename = "response.function_call_arguments.done")]
ResponseFunctionCallArgumentsDone { arguments: String, item_id: String },
#[serde(rename = "error")]
Error { code: String, message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ResponseSseMeta {
pub id: String,
pub model: String,
pub status: String,
}
```
### 3.6 请求转换(convert_request
#### 消息类型映射
| 输入场景 | Message 类型 | → Response API input item |
|---------|-------------|--------------------------|
| 文本 User | `Message::User { content: [Text] }` | `{role: "user", content: [{type: "input_text", text}]}` |
| Vision | `Message::UserImage { data, mime_type, detail }` | `{role: "user", content: [{type: "input_image", image_url: "data:{mime};base64,{data}", detail}]}` |
| User 多模态 | `Message::User { content: [Text, Image, ...] }` | `{role: "user", content: [{type: "input_text", text}, {type: "input_image", image_url, detail}]}` |
| Assistant 文本 | `Message::Assistant { content: [Text] }` | User 侧:`{role: "assistant", content: [{type: "output_text", text}]}`(无 `type` 字段);回传时: `{type: "message", role: "assistant", content: [{type: "output_text", text}]}`(有 `type: "message"` |
| Assistant 工具调用 | `Message::Assistant { content: [ToolUse] }` | `FunctionCall { call_id, name, arguments }` |
| Assistant 文本+工具 | `Message::Assistant { content: [Text, ToolUse, ...] }` | 一个 `Message(assistant)` + 一个或多个 `FunctionCall` 项 |
| 工具结果 | `Message::ToolResult { tool_call_id, content, is_error }` | `FunctionCallOutput { call_id, output: content }` |
| System | `Message::System { content }` | 拼接到顶层 `instructions` 字段(非 input |
#### 字段映射
| MessageRequest 字段 | → Response API 字段 |
|---------------------|---------------------|
| `model` | `model` |
| `max_tokens` | `max_output_tokens` |
| `temperature` | `temperature` |
| `top_p` | `top_p` |
| `stop_sequences` | `stop` |
| `stream` | `stream` |
| `tools` (ToolDef) | `tools` = `[{type: "function", name, description, parameters}]` |
| `tool_choice` | `tool_choice` |
#### extra 字段映射
| `MessageRequest.extra` key | → Response API 字段 |
|---------------------------|---------------------|
| `previous_response_id` | `previous_response_id` |
| `store` | `store` |
| `metadata` | `metadata` |
| `truncation` | `truncation` |
| `reasoning.effort` | `reasoning: {effort: ...}` |
| 内置工具(`web_search` / `file_search` 等) | 追加到 `tools` 数组 |
> **备注**:当前仅支持 `reasoning.effort` 子字段(值为 `low`/`medium`/`high`),其他子字段(如 `reasoning.summary`)将在后续版本支持。
### 3.7 响应转换(convert_response
| Response API output item | → MessageResponse 中的表示 |
|-------------------------|------------------------------|
| `{type: "message", role: "assistant", content: [{type: "output_text", text}]}` | `Message::Assistant { content: [ContentBlock::Text { text }] }` |
| `{type: "function_call", name, arguments, call_id}` | `ContentBlock::ToolUse { id: call_id, name, input: arguments }` |
| `{type: "web_search_call", ...}` | `ContentBlock::Extension { kind: "web_search_call", data: ... }` |
| `{type: "reasoning", ...}` | `ContentBlock::Extension { kind: "reasoning", data: ... }` |
| `{type: "file_search_call", ...}` | `ContentBlock::Extension { kind: "file_search_call", data: ... }` |
**status → StopReason 映射**
- `completed``StopReason::Stop`
- `incomplete``StopReason::Length`
- `failed``StopReason::Other`
`response.output` 为空数组时,返回 `LlmError::Request { status: 200, body: "empty output" }`,表示响应格式异常。
对于未知的 `item_type`(非 `message`/`function_call`/`web_search_call`/`file_search_call`/`reasoning`),转换为 `ContentBlock::Extension { kind: item_type, data: serde_json::to_value(item)? }` 以保持前向兼容。
### 3.8 流式 SSE 事件映射
| Response API SSE event | → StreamEvent |
|------------------------|---------------|
| `response.created` | `MessageStart { id, model }` |
| `response.output_item.added` (type: message) | `ContentBlockStart { index, block_type: Text }` |
| `response.output_text.delta` | `TextDelta { text }` |
| `response.output_text.done` | `ContentBlockEnd { index }` |
| `response.refusal.delta` | `RefusalDelta { text }` |
| `response.refusal.done` | `ContentBlockEnd { index }` |
| `response.function_call_arguments.delta` | `ToolCallArgumentsDelta { index, arguments }` |
| `response.function_call_arguments.done` | `ToolCallEnd { index }` |
| `response.completed` | `MessageComplete { full_response }` |
| `response.failed` | `Error { message }` |
### 3.9 流式 SSE 状态机
`ResponseSseEventStream` 维护以下状态:
```
字段:
- byte_stream: reqwest 的 bytes_stream
- buffer: Vec<u8>SSE 行缓冲)
- partial: PartialMessageResponse(累积响应状态)
- block_index: u32(输出 block 序号计数器)
- saw_terminal: bool(是否已见到 response.completed / response.failed
流程:
line 级解析 → event: + data: 配对
→ 反序列化 ResponseSseEvent
→ try_into_stream_event() 映射为 StreamEvent
→ StreamEvent::apply_to(&mut partial)
→ yield StreamEvent
response.completed → partial.finalize() → yield MessageComplete
response.failed → yield Error
```
### 3.10 错误映射
复用 `GenericOpenaiProvider``handle_error_response()` 逻辑:
| HTTP 状态码 | → LlmError |
|------------|------------|
| 401 | `LlmError::Authentication(body)` |
| 429 | `LlmError::RateLimit { retry_after }` |
| 5xx | `LlmError::Request { status, body }` |
| 400 + `context_length_exceeded` | `LlmError::ContextLength` |
### 3.11 Provider 结构体
```rust
pub(crate) struct OpenaiResponseProvider {
http_client: Client,
base_url: String,
api_key: String,
model: String,
timeout_secs: u64,
}
```
#### 工厂方法
```rust
impl OpenaiResponseProvider {
pub(crate) fn from_parts(
base_url: String,
api_key: String,
model: String,
http_client: Client,
timeout_secs: u64,
) -> Self {
Self { http_client, base_url, api_key, model, timeout_secs }
}
}
```
### 3.12 LlmProvider trait 实现
```rust
#[async_trait]
impl LlmProvider for OpenaiResponseProvider {
async fn chat(&self, request: MessageRequest) -> Result<MessageResponse, LlmError> {
self.chat_blocking(request).await
}
async fn chat_stream(
&self,
request: MessageRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError> {
self.chat_stream_inner(request).await
}
fn capabilities(&self) -> ProviderCapabilities { ... }
}
```
### 3.13 Capabilities
```rust
ProviderCapabilities {
provider_name: "openai-response",
supported_models: Some(vec![model]),
features: ProviderFeatures {
streaming: true,
thinking: true, // o-series reasoning
vision: true, // image input
audio_input: false,
tool_use: true,
parallel_tool_calls: true,
system_prompt_in_messages: false,
max_context_window: 200_000,
},
}
```
### 3.14 工厂函数注册
```rust
ProviderType::OpenaiResponse => {
let client = build_client_with_timeout(config.timeout_secs)?;
Ok(Box::new(openai_response::OpenaiResponseProvider::from_parts(
config.base_url,
config.api_key,
config.model,
client,
config.timeout_secs,
)))
}
```
### 3.15 多轮接续方案
第一版走**全量消息历史模式(模式 A)**:
1. `convert_request()``MessageRequest.messages` 全部转换为 `input` items
2. System 消息拼接到 `instructions`
3. User / Assistant / ToolResult 消息转换为对应的 input items
4. 如果 `extra` 中有 `previous_response_id`,也传入请求体
此模式与 `LlmCycle::submit_with_tools()` 完全兼容——`LlmCycle` 在每次提交时都会填充完整的历史 messages,`OpenaiResponseProvider` 只是把这些 messages 全部序列化为 Response API 格式。无需改动 `LlmCycle`
---
## 4. 实施计划
实施拆分为 3 个 Phase9 个 Step。
### Phase 28Feature gate + Wire 类型 + Provider 骨架(~140 行)
#### Step 28.1Cargo.toml feature 定义
**文件操作**:修改 `Cargo.toml`
```toml
# 在 [features] 的 Provider features 区域追加
provider-openai-response = ["llm", "reqwest", "bytes", "futures-util"]
# 在 full 快捷组合中追加
full = [
"...",
"provider-openai-response",
]
```
**验证**`cargo build --features "provider-openai-response"` 编译通过
#### Step 28.2Wire 类型定义
**文件操作**:新建 `src/llm/provider/openai_response.rs`
定义 §3.5 中的所有 Wire 类型:
- `OpenaiResponseRequest`
- `ResponseInputItem`untagged 枚举:Message / FunctionCall / FunctionCallOutput
- `ResponseInputContent`tagged 枚举:InputText / InputImage
- `ResponseTool`
- `OpenaiResponseBody`
- `ResponseOutputItem`
- `ResponseContentPart`
- `ResponseSseEvent`(完整时序事件枚举)
- `ResponseSseMeta`
**无逻辑代码**,只有 `#[derive(Debug, Clone, Serialize, Deserialize)]` 的结构体和枚举。
**验证**`cargo build --features "provider-openai-response"` 编译通过
#### Step 28.3Provider 结构体 + from_parts
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
- `OpenaiResponseProvider` 结构体
- `from_parts()` 工厂方法
- 基础 HTTP 工具函数(`build_request_builder``handle_error_response``map_reqwest_error`
**验证**`cargo build --features "provider-openai-response"` 编译通过
#### Step 28.4Factory 注册 + 模块门控
**文件操作**
1. 修改 `src/llm.rs` — 在 cfg 条件中追加 `feature = "provider-openai-response"`
2. 修改 `src/llm/provider.rs` — 注册 factory
`src/llm.rs` 中修改现有 Provider features cfg 条件:
```rust
#[cfg(any(
feature = "provider-openai",
feature = "provider-anthropic",
feature = "provider-deepseek",
feature = "provider-qwen",
feature = "provider-ollama",
feature = "provider-openai-response",
))]
pub mod provider;
```
以及在 `src/llm/provider.rs``create_provider()` match 中替换当前 `Err` 为真实构造。
**验证**
- `cargo build --features "provider-openai-response"` 编译通过
- `cargo build --features "full"` 编译通过
---
### Phase 29:核心 Provider 实现(~680 行)
#### Step 29.1convert_request~200 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `OpenaiResponseProvider::convert_request(&self, request: MessageRequest) -> Result<OpenaiResponseRequest, LlmError>`
处理逻辑:
1. 遍历 `request.messages`,按 §3.6 消息类型映射表转换
2. Assistant 消息回传时设置 `item_type: Some("message".to_string())`,使序列化结果为 `{type: "message", role: "assistant", content: [...]}`User 消息保持 `item_type: None`,序列化为 `{role: "user", content: [...]}`(无 `type` 字段)
3. `request.tools``tools` 数组(`ToolDef``ResponseTool::Function`
4. `request.extra` → 解析 `previous_response_id` / `store` / `metadata` / `truncation` / `reasoning`
5. 标准字段映射(model / max_tokens / temperature / top_p / stop / stream
#### Step 29.2convert_response~100 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `OpenaiResponseProvider::convert_response(&self, response: OpenaiResponseBody) -> Result<MessageResponse, LlmError>`
处理逻辑:
1. 遍历 `response.output`,找到第一个 `type: "message"` 的 item,提取 text
2. 其他 items`function_call``ContentBlock::ToolUse`,内置工具 → `ContentBlock::Extension`
3. `response.status``StopReason`
4. `response.usage``Usage`
#### Step 29.3:非流式 chat()~80 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `OpenaiResponseProvider::chat_blocking()`
- `convert_request()` → serde 序列化 → HTTP POST `{base_url}/responses`
- Auth header: `Authorization: Bearer {api_key}`
- 错误处理映射
- 解析响应体 → `convert_response()`
#### Step 29.4SSE 事件类型 + 状态机(~230 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `ResponseSseEventStream` 结构体及其 `Stream` trait
- 字段:`byte_stream`, `buffer`, `partial: PartialMessageResponse`, `block_index: u32`, `saw_terminal: bool`
- 行级 SSE 解析:`event:` + `data:` 配对
- 事件 → `StreamEvent` 映射
- `PartialMessageResponse::apply_to()` 累积
- 流结束时 `finalize()``MessageComplete`
#### Step 29.5:流式 chat_stream()~50 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `OpenaiResponseProvider::chat_stream_inner()`
- `convert_request()` 设置 `stream: true`
- HTTP POST → bytes_stream → 包装为 `ResponseSseEventStream`
#### Step 29.6LlmProvider impl~50 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs`
实现 `LlmProvider for OpenaiResponseProvider`
- `chat()``chat_blocking()`
- `chat_stream()``chat_stream_inner()`
- `capabilities()` → 返回 `ProviderCapabilities`
#### Step 29.7:单元测试(~70 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs``#[cfg(test)] mod tests {}`
| 测试 | 场景 |
|------|------|
| `convert_request_text_only` | 纯文本输入转换 |
| `convert_request_vision` | Vision 输入转换 |
| `convert_request_tool_call` | 工具调用输入转换 |
| `convert_response_message` | 响应 message item 转换 |
| `convert_response_tool_use` | 响应 function_call item 转换 |
---
### Phase 30:测试 + CI + 文档(~520 行)
#### Step 30.1wiremock 非流式测试(~200 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs` 内联测试
| 测试 | 场景 | 验证 |
|------|------|------|
| `response_api_basic_text` | 纯文本响应 | `response.text()` 正确 |
| `response_api_tool_call` | 工具调用 | `stop_reason == ToolUse` |
| `response_api_multi_turn` | 两轮对话 | 第二轮携带历史 |
| `response_api_vision` | 图片输入 | 正确构造 `input_image` |
| `response_api_unauthorized` | 401 错误 | `LlmError::Authentication` |
| `response_api_rate_limit` | 429 错误 | `LlmError::RateLimit` |
| `response_api_server_error` | 500 错误 | `LlmError::Request` |
#### Step 30.2wiremock 流式测试(~200 行)
**文件操作**:追加到 `src/llm/provider/openai_response.rs` 内联测试
| 测试 | 场景 | 验证 |
|------|------|------|
| `response_api_stream_text` | 流式文本 | 完整 SSE 事件序列 |
| `response_api_stream_tool` | 流式工具调用 | `FunctionCallArgumentsDelta` 序列 |
| `response_api_stream_error` | 流中途失败 | `StreamEvent::Error` |
| `response_api_stream_multi_turn` | 流式多轮接续 | 第二轮携带历史消息时的完整 SSE 事件序列 |
#### Step 30.3CI 矩阵(~10 行)
**文件操作**:修改 `.github/workflows/ci.yml`
新增测试组合:
```yaml
- "chat,provider-openai,provider-openai-response"
```
#### Step 30.4:文档更新(~50 行)
**文件操作**:修改 `README.md` + `docs/roadmap.md`
- README feature 表新增 `provider-openai-response`
- `docs/roadmap.md``docs/roadmap-unsorted.md` 新增 v0.3.3 或下版本条目
#### Step 30.5Example~60 行)
**文件操作**:新建 `examples/response_api_demo.rs`
```toml
[[example]]
name = "response_api_demo"
required-features = ["llm", "provider-openai-response"]
```
基础对话示例,展示 Response API 的基本用法:
```
cargo run --example response_api_demo --features "full"
```
---
### 实施汇总
| Phase | 内容 | 代码行数估算 | 验证入口 |
|-------|------|------------|---------|
| 28 | Feature gate + Wire 类型 + Provider 骨架 | ~140 | `cargo build --features "provider-openai-response"` |
| 29 | 核心 Provider 实现(转换/HTTP/流式) | ~680 | 5 个单元测试 |
| 30 | 测试 + CI + 文档 | ~520 | 10 个 wiremock 测试 + CI 新组合 |
| **合计** | | **~1,340** | 全量 `cargo test --features "full"` |
---
## 5. 风险评估
| ID | 风险 | 影响 | 概率 | 缓解措施 |
|----|------|------|------|---------|
| R1 | Response API 协议快速迭代 | Wire 类型可能需更新 | 中 | Wire 类型集中在单个文件内,更新成本低 |
| R2 | `ContentBlock::Extension` 承载内置工具结果 | 下游消费方需适配 | 低 | 这是既有的逃生舱机制,已有消费模式 |
| R3 | 全量历史模式 token 开销 | 多轮时 input tokens 增长 | 低 | 功能正确,后续版本可优化为 `previous_response_id` 增量模式 |
| R4 | `instructions` 拼接多个 system 消息 | 语义可能与单 system 消息不同 | 低 | 已确认按 OpenAI 推荐方式全量拼接(`\n` 分隔),行为等价 |
| R5 | 与 `GenericOpenaiProvider` 的错误映射逻辑重复 | 维护两份相似逻辑 | 低 | 提取复用函数时需注意不影响现有 provider |
---
## 6. 验证标准
### 6.1 编译验证
| # | 检查项 | 命令 |
|---|--------|------|
| C1 | 独立 feature 编译 | `cargo build --features "provider-openai-response"` |
| C2 | full 组合编译 | `cargo build --features "full"` |
| C3 | light 组合不受影响 | `cargo build --features "light"`(不包含新 feature |
| C4 | Clippy 合规 | `cargo clippy --all-features --lib -- -D warnings` |
### 6.2 测试验证
| # | 检查项 | 通过条件 |
|---|--------|---------|
| T1 | 单元测试 | `cargo test --features "full"` 全部通过(+15 新增测试) |
| T2 | 非流式 wiremock | 7 个测试覆盖文本/工具/多轮/Vision/401/429/500 |
| T3 | 流式 wiremock | 3 个测试覆盖文本流/工具流/错误流 |
| T4 | 现有测试无回归 | 使用 `--features "full"` 时已有 427 测试全部通过 |
### 6.3 CI 验证
| # | 检查项 | 通过条件 |
|---|--------|---------|
| I1 | 新增 CI 组合 | 包含新 feature 的组合编译通过 |
| I2 | clippy + format | `cargo clippy` + `cargo fmt --check` 通过 |
### 6.4 Example 验证
| # | 检查项 | 通过条件 |
|---|--------|---------|
| E1 | Example 编译 | `cargo build --example response_api_demo --features "full"` 通过 |
| E2 | Example 运行 | `cargo run --example response_api_demo --features "full"` 可执行(需 API key |