- 新增独立 OpenaiResponseProvider(POST /responses 协议),独立 feature provider-openai-response - 覆盖文本对话/流式/Vision/Function Calling/多轮接续/结构化输出/内置工具逃生舱 - 内置工具(web_search/file_search)通过 extra 逃生舱透传 - 工厂注册 ProviderType::OpenaiResponse + src/llm 模块门控追加 - 新增 example response_api_demo + CI 矩阵新增组合 + README/roadmap 同步 - 测试覆盖:13 单元 + 15 wiremock(流式 + 非流式 + 错误路径) - 文档:docs/28-phase28-openai-response-api-provider.md
32 KiB
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 定义
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":
#[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 类型设计
请求体类型
#[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 枚举
#[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 类型
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ResponseTool {
Function {
name: String,
description: String,
parameters: Value,
},
}
响应体类型
#[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 事件类型
#[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::Stopincomplete→StopReason::Lengthfailed→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 结构体
pub(crate) struct OpenaiResponseProvider {
http_client: Client,
base_url: String,
api_key: String,
model: String,
timeout_secs: u64,
}
工厂方法
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 实现
#[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
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 工厂函数注册
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):
convert_request()把MessageRequest.messages全部转换为inputitems- System 消息拼接到
instructions - User / Assistant / ToolResult 消息转换为对应的 input items
- 如果
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
# 在 [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 类型:
OpenaiResponseRequestResponseInputItem(untagged 枚举:Message / FunctionCall / FunctionCallOutput)ResponseInputContent(tagged 枚举:InputText / InputImage)ResponseToolOpenaiResponseBodyResponseOutputItemResponseContentPartResponseSseEvent(完整时序事件枚举)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 注册 + 模块门控
文件操作:
- 修改
src/llm.rs— 在 cfg 条件中追加feature = "provider-openai-response" - 修改
src/llm/provider.rs— 注册 factory
在 src/llm.rs 中修改现有 Provider features cfg 条件:
#[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<OpenaiResponseRequest, LlmError>。
处理逻辑:
- 遍历
request.messages,按 §3.6 消息类型映射表转换 - Assistant 消息回传时设置
item_type: Some("message".to_string()),使序列化结果为{type: "message", role: "assistant", content: [...]};User 消息保持item_type: None,序列化为{role: "user", content: [...]}(无type字段) request.tools→tools数组(ToolDef→ResponseTool::Function)request.extra→ 解析previous_response_id/store/metadata/truncation/reasoning等- 标准字段映射(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<MessageResponse, LlmError>。
处理逻辑:
- 遍历
response.output,找到第一个type: "message"的 item,提取 text - 其他 items(
function_call→ContentBlock::ToolUse,内置工具 →ContentBlock::Extension) response.status→StopReasonresponse.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
新增测试组合:
- "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
[[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) |