# 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) ] } ``` > **🔄 待深入推演:OpenAI 流式转换的具体实现** > 当前只有一行从 OpenAI SSE chunk 到 StreamEvent 的映射,没有实现细节。 > 实际实现中需要解决以下问题: > 1. **SSE 字节流解析器**:当前 `SseChunkStream` 从字节流解析 SSE 行(`data: ...`),新设计中需要改写为 > 直接产出 StreamEvent 的转换器。是否可以复用现有 `SseChunkStream` 的字节流解析逻辑? > 2. **ContentBlockStart/End 合成**:OpenAI 的 SSE 流中没有原生 block 边界标记,只有一个 `delta.content` > 和 `delta.tool_calls`。转换器需**自行合成**:在第一次出现 `delta.content` 时发出 > `ContentBlockStart(0, Text)`,在 `delta.tool_calls` 出现时发出 > `ContentBlockStart(n, ToolUse { id, name })`。注意 content 和 tool_calls 在同一 chunk **互斥** > (需验证),合成规则为"来什么类型,合什么边界"。 > 3. **Tool call 全局序号映射**:OpenAI 的 `delta.tool_calls[i].index` 是**该 chunk 内的局部索引**, > 需要累积状态(`HashMap<局部index, 全局index>`)来推导全局 content block 序号。 > 对比:**已推演**的 IR 设计将 ToolCallStart 合并到 ContentBlockStart,index 在 IR 层面统一为 > 全局序号,差异完全封装在 Provider 层。 > 4. **多 Choice 的处理**:当前代码只处理 `choices[0]`。新设计中是继续忽略其他 choice 还是 > 通过某种机制暴露(如 `extra` 中携带)? > 5. **usage 的时机**:OpenAI 的 usage 通常在最后一个 chunk 中携带,与 finish_reason 在同一 chunk。 > 是先发 `CostUpdate(PartialUsage{...})` 再发 `MessageComplete`,还是反过来? > **需要推演:** > - `OpenaiStreamToEvents` 转换器的状态机设计(跟踪的局部状态、事件产出规则) > - 边界情况:SSE 行乱序、`[DONE]` 标记的处理、网络断开重连 > 优先级:高(Phase 2 OpenAI Provider 重构的核心任务) ### 5.2 AnthropicProvider(Messages 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 ``` > **🔄 待深入推演:Anthropic 流式状态机设计** > Anthropic 的流式比 OpenAI 复杂得多——7 种事件类型、需要维护 block index 状态、 > thinking 有 signature 晚于 content_block 下发。 > **需要推演:** > 1. **状态机状态设计**: > - `PendingStart` → 等待 `message_start` > - `InBlock { block_index, block_type, tool_acc: Option }` → 在某个 content block 中 > - `BetweenBlocks` → 等待下一个 `content_block_start` 或 `message_delta` > - `Completed` → 收到 `message_stop` > - `Errored` > 2. **block index 追踪**:`content_block_start` 中的 `index` 是全局 content block 序号, > 直接对应最终 content 数组中的位置。`ToolUse { id, name }` 嵌入在 `ContentBlockStart` 的 > `block_type` 中(已推演:ToolCallStart 已移除,合并到 ContentBlockType::ToolUse)。 > 而 `input` 通过后续的 `content_block_delta`(含 `input_json_delta`)增量到达。 > 实现时发出 `ContentBlockStart(index, ToolUse { id, name })` 后, > 通过后续的 `ToolCallArgumentsDelta { index, arguments }` 累积参数。 > 3. **Thinking signature 的附着时机**:**✅ 已推演(方案 C:MessageComplete 兜底)** > Anthropic 中,thinking block 的 `signature` 不在 `content_block_start` 或 `content_block_delta` > 中下发,而是在最后的 `message_delta`(与 stop_reason 一起)下发。 > **推演结论:** > - `StreamEvent::MessageComplete { stop_reason, thinking_signature: Option }` > 携带 signature,不在 ThinkingDelta 或 ContentBlockEnd 中传递 > - Anthropic 映射层在收到 `message_delta` 时,将 `message_delta.thinking.signature` > 填入 `MessageComplete.thinking_signature` > - 汇聚算法(`PartialMessageResponse::finalize()`)在遍历 Thinking block 时, > 如果 builder 中的 `signature` 为 None,用 `self.thinking_signature` 回填 > - 该方案不需新增独立事件,统一在 finalize 时处理,语义清晰 > 4. **block 嵌套的边界情况**:Anthropic 的响应中 block 不会嵌套,但允许多个 block 连续出现。 > 需要确保状态机正确处理 block 间切换(前一个 `content_block_stop` → 后一个 `content_block_start`)。 > **需要推演:** > - 完整的状态转移图(当前状态 + 输入事件 → 新状态 + 产出的 StreamEvent) > - SSE 字节流解析器(与 OpenAI 共享字节流层,差异化事件解析层) > - 错误恢复:收到 `error` 事件时的状态机行为 > 优先级:高(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; } ``` > **🔄 待深入推演:OpenAI Response API 完整映射表** > 当前 §5.3 只有注释级别的草案,缺乏完整映射。 > **需要推演:** > 1. `input` 字段支持三种模式:字符串、`Vec`(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 后梳理)