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

460 lines
25 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.
# Provider 实现策略
> 本文档从 `9-llm-provider-unified-interface.md` 拆分而来,包含 §5 Provider 实现策略。
>
> **相关文件:**
> - [9b-ir-type-system.md](9b-ir-type-system.md) — IR 类型定义(ContentBlock、Message、MessageRequest/Response 等)
> - [9c-llm-provider-trait.md](9c-llm-provider-trait.md) — LlmProvider trait 定义(chat、chat_stream 签名)
> - [9e-llm-cycle-and-upstream.md](9e-llm-cycle-and-upstream.md) — LlmCycle 改造(system prompt 冲突等与 Provider 相关)
> - [9f-edge-cases.md](9f-edge-cases.md) — Thinking signature 端到端传递(与 Anthropic 流式强相关)
## 5. Provider 实现策略
### 5.1 OpenaiProvider(兼容 Chat API
```
MessageRequest
├── model → model
├── messages → messages (逐条映射,见下方)
├── system → 插入为首条 System message(如无 System message 时)
├── tools → tools (OpenaiTool::Function)
├── tool_choice → tool_choice
├── max_tokens → max_tokens
├── temperature → temperature
├── top_p → top_p
├── stop_sequences → stop (StopSequence::Multiple)
├── thinking → 忽略(OpenAI 不支持)
└── extra.* → 对应字段 / extra_body
Message → OpenaiChatMessage:
System { content } → System { content: to_openai_content(content) }
User { content } → User { content: to_openai_content(content) }
Assistant { content } → Assistant {
content: to_openai_content(text_blocks),
tool_calls: extract_tool_calls(content)
}
Tool { content, tool_call_id } → Tool { content, tool_call_id }
ContentBlock → OpenaiContentPart:
Text { text } → Text { text }
Image { source } → Image { image_url: { url, detail } }
Audio { source } → InputAudio { input_audio }
File { source } → File { file }
ToolUse { id, name, input } → 转为 OpenaiToolCall 放入 tool_calls 字段
ToolResult { .. } → 通过 tool_call_id 关联到 Tool 消息
Thinking { .. } → 忽略
Extension { .. } → 忽略
OpenaiChatResponse → MessageResponse:
id → id
model → model
usage → usage
choices[0].finish_reason → stop_reason
choices[0].message → Message::Assistant {
content: [
ContentBlock::Text { text },
... (msg.tool_calls → ContentBlock::ToolUse)
]
}
```
> **✅ 推演结论(2026-06-18):**
>
> ### 架构分层
>
> OpenAI 的流式转换拆为**两层**,字节解析层通用、事件转换层 OpenAI 特定:
>
> ```
> bytes_stream()
>
>
> SseByteStream [sse.rs — 通用层]
> │ 逐行分割、按空行分帧、解析 event:/data: 行前缀、
> │ 过滤 "[DONE]"/ping、缓冲拼接碎片、多行 data: 自动拼接
>
> Stream<Item=Result<SseEvent, LlmError>> ← 结构化 SSE 帧
> │ SseEvent { event_name: Option<String>, data: String }
> │ - OpenAI: event_name = None(未命名事件)
> │ - Anthropic: event_name = Some("message_start" | "content_block_delta" | ...)
>
> ├─ OpenAI ────→ OpenaiStreamToEvents [openai/stream.rs]
> │ 忽略 event_name,反序列化 data → OpenaiChatChunk → 状态机
>
> └─ Anthropic ──→ AnthropicStreamToEvents [anthropic/stream.rs]
> 按 event_name 分发事件类型 → 事件映射
> ```
>
> **Layer 1 — `SseByteStream<S>`(新增 `src/llm/provider/sse.rs`**
>
> 从当前 `SseChunkStream``openai.rs` 内联实现)提取字节解析逻辑为通用 SSE 解析器。
> 产出 `SseEvent` 结构体,携带 `event_name` + `data` 两部分信息。
> OpenAI 和 Anthropic 均直接复用此层,各自的事件转换器按需使用 `event_name`。
>
> **Layer 2 — `OpenaiStreamToEvents`(新增 `src/llm/provider/openai/stream.rs`**
>
> 接收 `SseEvent` 流,忽略 `event_name`OpenAI 为 `None`),
> 将 `data` 反序列化为 `OpenaiChatChunk`(保留作为内部格式),
> 通过状态机转换为 `StreamEvent` 事件流。
>
> ### 状态机设计
>
> ```rust
> pub struct OpenaiStreamToEvents<S> {
> inner: S, // Stream<Item=Result<String, LlmError>>
> // ── 状态 ──
> global_index: u32, // 下一个可用 content block 序号
> current_block: Option<(u32, CurrentBlockType)>, // 当前活跃 block
> tool_call_indices: HashMap<u32, u32>, // OpenAI tool_call.index → 全局序号
> is_complete: bool,
> }
>
> enum CurrentBlockType { Text, Refusal, ToolUse }
> ```
>
> **每条 JSON 行的处理流程:**
>
> ```
> 收到一行 JSON 字符串
> ├─ 解析为 OpenaiChatChunk
>
> ├─ Phase 1: 处理 delta 内容(先增量)
> │ ├─ delta.content → ensure_block(Text) → TextDelta
> │ ├─ delta.refusal → ensure_block(Refusal) → RefusalDelta
> │ └─ delta.tool_calls
> │ ├─ 新 tool callfunction.name 有值)
> │ │ → ensure_block(ToolUse, id, name) → (不产参数事件,等后续 arguments)
> │ └─ 已有 tool callfunction.arguments 有值)
> │ → ToolCallArgumentsDelta(index, arguments)
>
> └─ Phase 2: 处理汇总(后收束)
> ├─ finish_reason 存在 → close_current_block() + MessageComplete
> ├─ usage 存在 → CostUpdate
> └─ 同时存在 → CostUpdate → close_block → MessageComplete
> ```
>
> **核心抽象 `ensure_block`** 当新 chunk 的 delta 类型与当前活跃 block 不同时,
> 自动关闭当前 blockemit `ContentBlockEnd`)并开启新 blockemit `ContentBlockStart`)。
> 同类型继续时只发增量事件,不切换。
>
> **状态转移表:**
>
> | 当前状态 | 收到 delta.content | 收到 delta.tool_calls (new) | 收到 delta.tool_calls (延续) | 收到 finish_reason |
> |----------|-------------------|----------------------------|----------------------------|-------------------|
> | 无活跃 block | → ContentBlockStart(Text)<br>→ TextDelta | → ContentBlockStart(ToolUse) | (不应发生) | → MessageComplete |
> | Text 活跃中 | → TextDelta | → ContentBlockEnd<br>→ ContentBlockStart(ToolUse) | (不应发生,与 content 互斥) | → ContentBlockEnd<br>→ CostUpdate<br>→ MessageComplete |
> | ToolUse 活跃中 | → ContentBlockEnd<br>→ ContentBlockStart(Text) | → ContentBlockEnd<br>→ ContentBlockStart(new ToolUse) | → ToolCallArgumentsDelta | → ContentBlockEnd<br>→ CostUpdate<br>→ MessageComplete |
>
> ### 各议题结论
>
> | # | 议题 | 结论 | 理由 |
> |---|------|------|------|
> | 1 | **SSE 字节解析复用** | 提取为通用 `SseByteStream``sse.rs`),支持 `event:` + `data:` 双行解析;`OpenaiStreamToEvents` / `AnthropicStreamToEvents` 分别在其上封装 | Anthropic 复用相同字节协议,通过 `SseEvent.event_name` 区分事件类型;分层测试、职责清晰 |
> | 2 | **ContentBlockStart/End 合成** | "类型切换推断边界"策略——来什么类型就关旧开新,依赖 content/tool_calls 互斥保证 | 状态机 3 种当前类型覆盖全部场景,假设验证通过 |
> | 3 | **Tool call 序号映射** | `HashMap<openai_index, global_block_index>`,新 tool call 出现时分配全局序号 | OpenAI index 是 tool 数组级别全局的,但与 IR content block 体系不同,需映射 |
> | 4 | **多 Choice** | 忽略 choices[1..],不暴露 | 当前架构无多 choice 概念,80% 场景 n=1,非目标已明确 |
> | 5 | **Usage 时机** | 先发 `CostUpdate` → 再发 `MessageComplete`,同一 chunk 内串行 | 汇聚算法兼容两者顺序,但语义上先用量后完成更合理 |
> | 6 | **ToolCallEnd 发出** | OpenAI 层不显式发出 `ToolCallEnd`,依赖汇聚算法 `finalize()` 兜底 | `ToolCallEnd` 保留给 Anthropic`content_block_stop` 场景);`ContentBlockEnd` 已标 block 完成,`finalize` 时 `tool_call_args` 已累积完整 |
> | 7 | **Refusal 处理** | 检测 `delta.refusal`emit `ContentBlockStart(Refusal)` + `RefusalDelta` + `ContentBlockEnd` | OpenAI 特有字段,IR 已有 `ContentBlockType::Refusal` |
> | 8 | **代码消重** | 删除 `stream.rs::parse_chunk_stream`(无人调用);`cycle.rs::submit_stream` 直接消费 `Result<StreamEvent>` 流 | 转换逻辑统一到 Provider 层,LlmCycle 只负责编排和 hook |
>
> ### 边界情况
>
> | 场景 | 处理方式 |
> |------|---------|
> | **`[DONE]` 行** | `SseByteStream` 层过滤,不传递到事件层(当前已有逻辑) |
> | **delta 为空 + finish_reason** | 只处理 Phase 2,关闭当前 block 后 emit MessageComplete |
> | **同一 chunk 含 delta.content + finish_reason** | Phase 1 先处理 delta 发 TextDeltaPhase 2 关闭 block 发 Complete |
> | **同一 chunk 含 delta.tool_calls + finish_reason** | 先处理所有 tool calls(映射 + arguments 累积),再关 block |
> | **usage 单独 chunk 下发** | 触发 Phase 2 但 finish_reason 为 None → 只发 CostUpdate,不发 MessageComplete |
> | **网络断开** | reqwest 返回 Err → emit StreamEvent::Erroris_complete = true |
> | **JSON 解析失败** | emit StreamEvent::Error,终止流(防御性处理) |
>
> ### 文件变更清单
>
> | 操作 | 文件 | 说明 |
> |------|------|------|
> | 新增 | `src/llm/provider/sse.rs` | 通用 SSE 字节解析层,含 `SseEvent` 结构体;从当前 `openai.rs` 的 `SseChunkStream` 提取核心逻辑并增强为支持 `event:` + `data:` 双行解析 + 空行分帧 |
> | 新增 | `src/llm/provider/openai/stream.rs` | `OpenaiStreamToEvents` 转换器 + 状态机 |
> | 修改 | `src/llm/provider/openai.rs` | `chat_stream` 返回 `Result<StreamEvent>`,组合 `SseByteStream` + `OpenaiStreamToEvents` |
> | 修改 | `src/llm/provider.rs` | `LlmProvider::chat_stream` 签名改为 `Result<StreamEvent>` |
> | 修改 | `src/llm/cycle.rs` | `submit_stream` 直接消费 `StreamEvent` 流,移除内联 chunk→event 转换 |
> | 修改 | `src/llm/stream.rs` | 删除 `parse_chunk_stream`(无人调用) |
>
> 优先级:高(Phase 2 OpenAI Provider 重构的核心任务)
### 5.2 AnthropicProviderMessages API
```
MessageRequest → Anthropic Messages Request:
model → "anthropic-xxx"
messages → [只包含 User / Assistant / Tool role]
system → system (顶层参数)
tools → tools (Anthropic 原生格式)
tool_choice → tool_choice
max_tokens → max_tokens
temperature → temperature
top_p → top_p
stop_sequences → stop_sequences
thinking → thinking (原生支持)
extra.* → 忽略或不支持
Message → Anthropic Message:
System { } → 跳过(已在 system 参数中)
User { content } → { role: "user", content: to_anthropic_content(content) }
Assistant { content } → { role: "assistant", content: to_anthropic_content(content) }
Tool { content, tool_call_id } → { role: "user", content: [tool_result block] }
ContentBlock → Anthropic Content Block:
Text { text } → { type: "text", text }
Image { source } → { type: "image", source: { type: "base64", ... } }
ToolUse { id, name, input } → { type: "tool_use", id, name, input }
ToolResult { tool_use_id, content, is_error } → { type: "tool_result", ... }
Thinking { text, signature } → { type: "thinking", thinking: text, signature }
Audio / File / Extension → 忽略或不支持
Anthropic Response → MessageResponse:
id → id
model → model
content → Message::Assistant { content: map_blocks(content) }
usage.input_tokens → usage.prompt_tokens
usage.output_tokens → usage.completion_tokens
stop_reason → stop_reason
```
> **✅ 推演结论(2026-06-18):**
>
> ### 设计思路:轻量分发器
>
> Anthropic 的流式事件**自带语义块边界**`content_block_start/stop` 显式声明),
> 不像 OpenAI 需从扁平 delta 推断。因此 Anthropic 状态机采用**轻量分发器**模式——
> 每个事件自描述,状态机仅做顺序合法性校验,不做 block 边界推断或 index 映射。
>
> **与 OpenAI 流式转换的核心差异:**
>
> | 维度 | OpenaiStreamToEvents | AnthropicStreamToEvents |
> |------|---------------------|------------------------|
> | 核心复杂度 | 中——需从扁平 delta 推断 block 边界 | 低——事件自带语义边界 |
> | 状态数 | 3(无活跃、Text、ToolUse | 3PendingStart、Active、Terminated |
> | index 管理 | `HashMap<openai_idx, global_idx>` 映射 | 直接使用 Anthropic index1:1 |
> | Block 边界推断 | 类型切换推断 | 原生 content_block_start/stop |
> | Thinking 处理 | 无 | 通过 message_delta.thinking.signature |
>
> ### 架构分层
>
> 沿用 OpenAI 的两层架构,`SseByteStream` 共享(已增强为支持命名事件),
> `AnthropicStreamToEvents` 在事件层按 `event_name` 分发:
>
> ```
> bytes_stream()
>
>
> SseByteStream [sse.rs — 通用层(已增强)]
> │ 逐行分割、按空行分帧、解析 event:/data: 行前缀、过滤 "[DONE]"/ping
>
> Stream<Item=Result<SseEvent, LlmError>> ← SseEvent { event_name: Option<String>, data }
>
>
> AnthropicStreamToEvents [anthropic/stream.rs — Anthropic 特定]
> │ 按 SseEvent.event_name 分发事件类型 → 直接映射
>
> Stream<Item=Result<StreamEvent, LlmError>> ← IR 语义事件
> ```
>
> ### 结构体设计
>
> ```rust
> pub struct AnthropicStreamToEvents<S> {
> inner: S, // Stream<Item=Result<SseEvent, LlmError>>
> state: AnthropicStreamState, // 仅做顺序校验
> usage: PartialUsage, // 从 message_start + message_delta 累积
> pending_thinking_signature: Option<String>, // message_delta 中到达
> }
>
> /// 状态机状态 —— 仅做合法性校验,事件本身已自描述。
> enum AnthropicStreamState {
> PendingStart, // 等待 message_start
> Active, // 已收到 message_start,正在接收 content block 事件
> Terminated, // 已终结,不再处理后续事件
> }
> ```
>
> 状态足够简单的原因:Anthropic 每个事件自带完整语义——
> - `content_block_delta` 自带 `index`,不需要追踪"当前活跃 block"
> - `content_block_stop` 自带 `index`,不需要追踪"当前关闭哪个"
> - 状态只拒绝非法到达顺序的事件
>
> ### 事件映射表(完整)
>
> | Anthropic SSE 事件 | 产出的 StreamEvent | 说明 |
> |-------------------|-------------------|------|
> | `message_start` | `MessageStart { id, model }`<br>`CostUpdate { prompt_tokens }` | 从 `message.usage.input_tokens` 提取 |
> | `ping` | —(忽略) | Anthropic 心跳 |
> | `content_block_start`<br>`block.type="text"` | `ContentBlockStart { index, Text }` | |
> | `content_block_start`<br>`block.type="tool_use"` | `ContentBlockStart { index, ToolUse { id, name } }` | block 自带 id + name |
> | `content_block_start`<br>`block.type="thinking"` | `ContentBlockStart { index, Thinking }` | |
> | `content_block_delta`<br>`delta.type="text_delta"` | `TextDelta { text: delta.text }` | |
> | `content_block_delta`<br>`delta.type="thinking_delta"` | `ThinkingDelta { text: delta.thinking }` | |
> | `content_block_delta`<br>`delta.type="input_json_delta"` | `ToolCallArgumentsDelta { index, arguments: delta.partial_json }` | index 透传 |
> | `content_block_stop` | `ContentBlockEnd { index }` | |
> | `message_delta` | `CostUpdate { completion_tokens }`<br>`MessageComplete { stop_reason, thinking_signature }` | signature 从 `delta.thinking?.signature` 提取 |
> | `message_stop` | —(流结束标记,不产事件) | 仅切状态到 Terminated |
> | `error` | `Error { message: error.message }` | 切状态到 Terminated |
>
> ### 状态转移表
>
> **当前状态:`PendingStart`**
>
> | 输入事件 | 输出 StreamEvent | 新状态 | 备注 |
> |---------|----------------|--------|------|
> | `message_start` | → `MessageStart` + `CostUpdate` | `Active` | ✅ 正常流程入口 |
> | 其他任何事件 | — ⚠ warn | 不变 | 防御性跳过 |
> | `error` | → `Error` | `Terminated` | ❌ 错误路径 |
>
> **当前状态:`Active`**
>
> | 输入事件 | 输出 StreamEvent | 新状态 | 备注 |
> |---------|----------------|--------|------|
> | `ping` | —(忽略) | `Active` | ✅ 心跳 |
> | `content_block_start` | → `ContentBlockStart` | `Active` | ✅ 新 block 开始 |
> | `content_block_delta` | → `TextDelta` / `ThinkingDelta` / `ToolCallArgumentsDelta` | `Active` | ✅ 块内增量 |
> | `content_block_stop` | → `ContentBlockEnd` | `Active` | ✅ block 结束 |
> | `message_delta` | → `CostUpdate` + `MessageComplete` | `Active` | ✅ 消息完成信息 |
> | `message_stop` | — | `Terminated` | ✅ 正常结束 |
> | `error` | → `Error` | `Terminated` | ❌ 错误路径 |
> | 未知 delta type | — ⚠ warn | `Active` | 防御性忽略 |
>
> **当前状态:`Terminated`**
>
> | 输入事件 | 输出 StreamEvent | 新状态 | 备注 |
> |---------|----------------|--------|------|
> | 任何事件 | — ⚠ warn "已完结" | `Terminated` | 防御性忽略 |
>
> ### 各议题结论
>
> | # | 议题 | 结论 | 理由 |
> |---|------|------|------|
> | 1 | **状态机模式** | 轻量分发器(3 状态),不做 block 推断 | Anthropic 事件自带语义边界,不需要像 OpenAI 那样推断 |
> | 2 | **index 映射** | 无需映射,直接使用 Anthropic index1:1 | Anthropic 的 `index` 是全局 content block 序号,与 IR 完全对齐 |
> | 3 | **Thinking signature** | ✅ 已推演(方案 C):message_delta 提取 → MessageComplete 传递 → finalize 回填 | 已在 [9f-edge-cases.md](9f-edge-cases.md#92-thinking-的端到端流程) 中完成推演 |
> | 4 | **SSE 字节解析复用** | 通过增强的 `SseByteStream`(支持 `event:` 行解析)与 OpenAI 共享通用层 | 同一字节协议,仅在事件解析层差异化 |
> | 5 | **Usage 分次到达** | `message_start` 提取 `input_tokens``message_delta` 提取 `output_tokens``PartialUsage` 字段级合并 | 与 StreamEvent 汇聚算法兼容 |
> | 6 | **错误恢复** | `error` 事件 → `StreamEvent::Error` + state=Terminated,后续事件全部忽略 | 不同于 OpenAI 的 HTTP 错误路径,但 IR 层统一为 `StreamEvent::Error` |
> | 7 | **ContentBlockType::ToolUse 嵌入** | `content_block_start` 中的 `id` + `name` 直接填入 `ContentBlockType::ToolUse { id, name }` | 与已推演的 ContentBlockStart 设计一致 |
> | 8 | **block 切换** | content_block_stop(index) → content_block_start(index') 自然过渡,状态机不追踪 | 事件本身已确定边界,无需状态机参与 |
>
> ### 与已推演设计的对齐
>
> **与 Thinking signature 方案的对齐(方案 C):**
> ```
> content_block_start { type: "thinking" }
> → ContentBlockStart(Thinking) ← signature 未到达
> content_block_delta { thinking_delta }
> → ThinkingDelta(...)
> content_block_stop
> → ContentBlockEnd ← signature 仍未到达
> message_delta { delta.thinking.signature = "0x..." }
> → MessageComplete { thinking_signature: Some("0x...") }
> → finalize() 回填到最后一个 Thinking block
> ```
>
> **与 StreamEvent 汇聚算法的对齐:**
> 本状态机产出的 StreamEvent 可直接喂入已推演的 `PartialMessageResponse::apply_to()` 算法。
> 上述事件序列在汇聚算法中:
> 1. `MessageStart` → state.id/model
> 2. `ContentBlockStart/Delta/End` → `BTreeMap` 按 index 分桶组装
> 3. `CostUpdate` → PartialUsage 字段级合并
> 4. `MessageComplete` → stop_reason + thinking_signature + is_complete
> 5. `finalize()` → 回填 signature → `MessageResponse`
>
> ### 边界情况
>
> | 场景 | 处理方式 |
> |------|---------|
> | **message_start 前收 content_block_start** | ⚠ warn 忽略,不发射事件 |
> | **message_delta 前收 message_stop** | ⚠ warn,强制 Terminated |
> | **content_block_stop 无对应 start** | ⚠ warn 忽略(index 无对应) |
> | **index 跳跃(0 → 2** | 正常处理,index 透传,content 数组留空位 |
> | **delta index 与最新 start 不匹配** | ⚠ warn,仍然按 delta 自带 index 处理 |
> | **message_delta 缺 thinking.signature** | `thinking_signature`: None |
> | **message_delta 缺 usage** | 不发射 CostUpdate,仅发射 MessageComplete |
> | **两次 message_delta** | ⚠ warn,第二次忽略 |
> | **content_block_stop 后同 index 又来 delta** | ⚠ warn 忽略 |
> | **ping 事件** | 忽略,不发射任何事件 |
> | **网络断开** | emit `Error`state = Terminated |
> | **JSON 解析失败** | emit `Error`state = Terminated |
> | **stop_reason 映射** | `"end_turn"`→`Stop`, `"max_tokens"`→`MaxTokens`, `"tool_use"`→`ToolUse`, `"stop_sequence"`→`StopSequence`, 其他→`Other` |
>
> ### 文件变更清单
>
> | 操作 | 文件 | 说明 |
> |------|------|------|
> | 新增 | `src/llm/provider/anthropic.rs` | AnthropicProvider 实现(chat + chat_stream |
> | 新增 | `src/llm/provider/anthropic/` | 目录,按 2018 版风格组织 |
> | 新增 | `src/llm/provider/anthropic/stream.rs` | `AnthropicStreamToEvents` 转换器 + 事件分发器 |
> | 增强 | `src/llm/provider/sse.rs` | `SseByteStream` 增强为支持 `event:` 行 + 空行分帧(已在 §5.1 中描述) |
> | 修改 | `src/llm/provider.rs` | `ProviderType` 增加 `Anthropic``create_provider` 增加分支 |
> | 无变更 | `src/llm/cycle.rs` | StreamEvent 事件序列格式不变,无需改动 |
> | 无变更 | `src/llm/stream.rs` | 汇聚算法 `apply_to/finalize` 不变 |
>
> 优先级:高(Phase 4 AnthropicProvider 实现的前提条件)
### 5.3 OpenAI Response API(草案)
```rust
// 核心思路:Response API 的 "input as messages" 模式映射到 IR
// MessageRequest → Response API Request:
// model → model
// messages → input (作为 multi-turn conversation)
// tools → tools (tool 定义)
// extra.previous_response_id → previous_response_id
// extra.built_in_tools → tools (内置工具配置)
// extra.instructions → instructions
// Response → MessageResponse:
// output[0] (type="message") → message
// output[1..n] → ContentBlock::Extension
// 内置工具(web_search 等)需要额外的能力 trait:
#[async_trait]
pub trait BuiltInToolsCapable: LlmProvider {
fn available_builtin_tools(&self) -> Vec<(&'static str, serde_json::Value)>;
async fn execute_builtin_tool(&self, name: &str, input: Value) -> Result<Value, LlmError>;
}
```
> **🔄 待深入推演:OpenAI Response API 完整映射表**
> 当前 §5.3 只有注释级别的草案,缺乏完整映射。
> **需要推演:**
> 1. `input` 字段支持三种模式:字符串、`Vec<Message>`IR 消息列表)、`response_id`(前序响应)。
> IR 的 `MessageRequest.messages` 能否同时覆盖这三种?`previous_response_id` 通过 extra 传递后,
> `messages` 是否还需要存在?
> 2. `output` 中的每种类型(`message`, `web_search_call`, `file_search_call`,
> `code_interpreter_call`, `computer_call`, `reasoning`)如何映射到 `ContentBlock`
> 当前 `Extension` 逃生舱能否承载?是否需要新增 ContentBlock variant
> 3. Response API 的 `tools` 参数除了定义 function 工具外,还支持配置内置工具的参数
> (如 `web_search` 的 `search_context_size`)。`ToolDefinition` 能否表达?
> 4. Streaming 差异:Response API 的流式事件类型(`response.output_items.added` 等)与 Chat API
> 完全不同,如何映射到 StreamEvent
> 优先级:低(Phase 4 之后的远期计划)
### 5.4 DeepSeek / Qwen 等兼容 Provider 的落地策略
当前 `ProviderType` 枚举中已列出 DeepSeek 和 Qwen,但实现均标记为 `unimplemented!()`
> **🔄 待深入推演:DeepSeek/Qwen Provider 的落地策略**
> 这些 Provider 通常兼容 OpenAI Chat API 格式。在新 IR 设计下,有两种落地路径:
> **路径 A — 复用 OpenaiProvider(推荐):**
> 在 `ProviderRegistry` 中注册时直接使用 `OpenaiProvider::new(base_url, api_key, model)`
> 仅需换 base_url。适合 DeepSeek、Qwen、Groq、Azure 等 API 格式与 OpenAI 完全一致的场景。
> 此时 `ProviderType` 枚举可能不再需要(`OpenaiProvider` 通过 `capabilities().provider_name`
> 标识自身为 "openai-compatible" 或具体名称)。
> **路径 B — 独立 Provider 实现:**
> 如果某 Provider 在 OpenAI 格式基础上做了扩展/修改(如自定义参数、不同的错误格式),
> 可独立实现 `LlmProvider` trait,复用 IR 类型,仅在 IR ↔ 原生格式映射层做差异处理。
> **需要推演:**
> 1. `ProviderType` 枚举在新的注册体系中是否还有存在的必要(工厂函数模式 vs 直接 new Provider
> 2. `OpenaiProvider` 是否要重命名为更通用的 `OpenaiCompatibleProvider`
> 优先级:低(Phase 4 后梳理)