# 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.2(Phase 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, pub input: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_choice: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_output_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] pub temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] pub top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stop: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub stream: Option, #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub truncation: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, } ``` #### 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, // 可选,固定为 "message"(assistant 回传时使用) role: String, content: Vec, }, 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, #[serde(skip_serializing_if = "Option::is_none")] status: Option, }, 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, }, } ``` > **补充说明**: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, 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, pub role: Option, pub content: Option>, pub call_id: Option, pub name: Option, pub arguments: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ResponseContentPart { #[serde(rename = "type")] pub part_type: String, pub text: Option, } ``` #### 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(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(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 { self.chat_blocking(request).await } async fn chat_stream( &self, request: MessageRequest, ) -> Result> + 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 个 Phase,9 个 Step。 ### Phase 28:Feature gate + Wire 类型 + Provider 骨架(~140 行) #### Step 28.1:Cargo.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.2:Wire 类型定义 **文件操作**:新建 `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.3:Provider 结构体 + 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.4:Factory 注册 + 模块门控 **文件操作**: 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.1:convert_request(~200 行) **文件操作**:追加到 `src/llm/provider/openai_response.rs` 实现 `OpenaiResponseProvider::convert_request(&self, request: MessageRequest) -> Result`。 处理逻辑: 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.2:convert_response(~100 行) **文件操作**:追加到 `src/llm/provider/openai_response.rs` 实现 `OpenaiResponseProvider::convert_response(&self, response: OpenaiResponseBody) -> Result`。 处理逻辑: 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.4:SSE 事件类型 + 状态机(~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.6:LlmProvider 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.1:wiremock 非流式测试(~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.2:wiremock 流式测试(~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.3:CI 矩阵(~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.5:Example(~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) |