Files
agcore/docs/9d-provider-implementations.md
T

205 lines
11 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)
]
}
```
> **🔄 待深入推演:OpenAI 流式转换的具体实现**
> 当前只有一行从 OpenAI SSE chunk 到 StreamEvent 的映射,没有实现细节。
> 实际实现中需要解决以下问题:
> 1. **SSE 字节流解析器**:当前 `SseChunkStream` 从字节流解析 SSE 行(`data: ...`),新设计中需要改写为
> 直接产出 StreamEvent 的转换器。是否可以复用现有 `SseChunkStream` 的字节流解析逻辑?
> 2. **Tool call 索引跟踪**OpenAI 的 `delta.tool_calls[i].index` 是局部索引,
> 需要累积状态来推导全局 tool call 序号。当同一 chunk 出现多个 `delta.tool_calls` 条目时,
> 是逐个发出 `ToolCallStart`/`ToolCallArgumentsDelta` 还是合并?
> 3. **多 Choice 的处理**:当前代码只处理 `choices[0]`。新设计中是继续忽略其他 choice 还是
> 通过某种机制暴露(如 `extra` 中携带)?
> 4. **usage 的时机**OpenAI 的 usage 通常在最后一个 chunk 中携带,与 finish_reason 在同一 chunk。
> 是先发 `CostUpdate` 再发 `MessageComplete`,还是反过来?
> **需要推演:**
> - `OpenaiStreamToEvents<S>` 转换器的状态机设计(跟踪的局部状态、事件产出规则)
> - 边界情况:SSE 行乱序、`[DONE]` 标记的处理、网络断开重连
> 优先级:高(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
```
> **🔄 待深入推演:Anthropic 流式状态机设计**
> Anthropic 的流式比 OpenAI 复杂得多——7 种事件类型、需要维护 block index 状态、
> thinking 有 signature 在 `content_block_start` 中单独下发。
> **需要推演:**
> 1. **状态机状态设计**
> - `PendingStart` → 等待 `message_start`
> - `InBlock { block_index, block_type, tool_acc: Option<ToolAccumulator> }` → 在某个 content block 中
> - `BetweenBlocks` → 等待下一个 `content_block_start` 或 `message_delta`
> - `Completed` → 收到 `message_stop`
> - `Errored`
> 2. **block index 追踪**`content_block_start` 中的 `index` 是全局 content block 序号,
> 直接对应最终 content 数组中的位置。但 tool_use block 的 `id` 和 `name` 在 `content_block_start`
> 中下发,而 `input` 通过后续的 `content_block_delta`(含 `input_json_delta`)增量到达。
> 实现时需要按 index 累积 tool call 状态,在 `content_block_stop` 时发出完整的
> `ToolCallStart{ index, id, name }`(此时 arguments 可能已部分累积)。
> 3. **Thinking signature 的附着时机**
> Anthropic 中,thinking block 的 `signature` 在 `content_block_delta`(或 `message_delta`)中下发,
> 而不是在 `content_block_start`。这意味着 `StreamEvent::ThinkingDelta` 需要额外的字段或机制来携带
> signature。当前 `ThinkingDelta { text }` 缺少 `signature` 字段,需要重新设计。
> 备选方案:在 `MessageComplete` 中携带 `final_thinking_signature` 字段,或添加独立事件
> `ThinkingSignature { signature: String }`。
> 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<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 后梳理)