Files
agcore/docs/28-phase28-openai-response-api-provider.md
T
徐涛 b895616dd0
CI / test (chat,provider-openai) (push) Has been cancelled
CI / test (chat,provider-openai,provider-openai-response) (push) Has been cancelled
CI / test (chat,provider-openai,tools-mcp) (push) Has been cancelled
CI / test (full) (push) Has been cancelled
CI / test (light) (push) Has been cancelled
CI / test (multi,provider-openai,tools-mcp) (push) Has been cancelled
CI / clippy (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / examples (push) Has been cancelled
CI / test (multi,provider-openai) (push) Has been cancelled
feat(llm): 实现 OpenAI Response API Provider
- 新增独立 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
2026-07-20 09:05:04 +08:00

32 KiB
Raw Blame History

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 APIPOST /responses),作为 Chat Completions APIPOST /chat/completions)的下一代接口。Response API 不仅提供了更简洁的请求/响应结构,还将 web_searchfile_searchcomputer_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 UserImageinput_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,此处补充 reqwestHTTP 客户端)和 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 映射

  • completedStopReason::Stop
  • incompleteStopReason::Length
  • failedStopReason::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 错误映射

复用 GenericOpenaiProviderhandle_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

  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

# 在 [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
  • ResponseInputItemuntagged 枚举:Message / FunctionCall / FunctionCallOutput
  • ResponseInputContenttagged 枚举: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_builderhandle_error_responsemap_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 条件:

#[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.rscreate_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.toolstools 数组(ToolDefResponseTool::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. 其他 itemsfunction_callContentBlock::ToolUse,内置工具 → ContentBlock::Extension
  3. response.statusStopReason
  4. response.usageUsage

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

新增测试组合:

- "chat,provider-openai,provider-openai-response"

Step 30.4:文档更新(~50 行)

文件操作:修改 README.md + docs/roadmap.md

  • README feature 表新增 provider-openai-response
  • docs/roadmap.mddocs/roadmap-unsorted.md 新增 v0.3.3 或下版本条目

Step 30.5Example~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