11 KiB
Provider 实现策略
本文档从
9-llm-provider-unified-interface.md拆分而来,包含 §5 Provider 实现策略。相关文件:
- 9b-ir-type-system.md — IR 类型定义(ContentBlock、Message、MessageRequest/Response 等)
- 9c-llm-provider-trait.md — LlmProvider trait 定义(chat、chat_stream 签名)
- 9e-llm-cycle-and-upstream.md — LlmCycle 改造(system prompt 冲突等与 Provider 相关)
- 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 的映射,没有实现细节。 实际实现中需要解决以下问题:
- SSE 字节流解析器:当前
SseChunkStream从字节流解析 SSE 行(data: ...),新设计中需要改写为 直接产出 StreamEvent 的转换器。是否可以复用现有SseChunkStream的字节流解析逻辑?- Tool call 索引跟踪:OpenAI 的
delta.tool_calls[i].index是局部索引, 需要累积状态来推导全局 tool call 序号。当同一 chunk 出现多个delta.tool_calls条目时, 是逐个发出ToolCallStart/ToolCallArgumentsDelta还是合并?- 多 Choice 的处理:当前代码只处理
choices[0]。新设计中是继续忽略其他 choice 还是 通过某种机制暴露(如extra中携带)?- usage 的时机:OpenAI 的 usage 通常在最后一个 chunk 中携带,与 finish_reason 在同一 chunk。 是先发
CostUpdate再发MessageComplete,还是反过来? 需要推演:
OpenaiStreamToEvents<S>转换器的状态机设计(跟踪的局部状态、事件产出规则)- 边界情况: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_start中单独下发。 需要推演:
- 状态机状态设计:
PendingStart→ 等待message_startInBlock { block_index, block_type, tool_acc: Option<ToolAccumulator> }→ 在某个 content block 中BetweenBlocks→ 等待下一个content_block_start或message_deltaCompleted→ 收到message_stopErrored- 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 可能已部分累积)。- 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 }。- block 嵌套的边界情况:Anthropic 的响应中 block 不会嵌套,但允许多个 block 连续出现。 需要确保状态机正确处理 block 间切换(前一个
content_block_stop→ 后一个content_block_start)。 需要推演:
- 完整的状态转移图(当前状态 + 输入事件 → 新状态 + 产出的 StreamEvent)
- SSE 字节流解析器(与 OpenAI 共享字节流层,差异化事件解析层)
- 错误恢复:收到
error事件时的状态机行为 优先级:高(Phase 4 AnthropicProvider 实现的前提条件)
5.3 OpenAI Response API(草案)
// 核心思路: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 只有注释级别的草案,缺乏完整映射。 需要推演:
input字段支持三种模式:字符串、Vec<Message>(IR 消息列表)、response_id(前序响应)。 IR 的MessageRequest.messages能否同时覆盖这三种?previous_response_id通过 extra 传递后,messages是否还需要存在?output中的每种类型(message,web_search_call,file_search_call,code_interpreter_call,computer_call,reasoning)如何映射到ContentBlock? 当前Extension逃生舱能否承载?是否需要新增 ContentBlock variant?- Response API 的
tools参数除了定义 function 工具外,还支持配置内置工具的参数 (如web_search的search_context_size)。ToolDefinition能否表达?- 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 格式基础上做了扩展/修改(如自定义参数、不同的错误格式), 可独立实现LlmProvidertrait,复用 IR 类型,仅在 IR ↔ 原生格式映射层做差异处理。 需要推演:
ProviderType枚举在新的注册体系中是否还有存在的必要(工厂函数模式 vs 直接 new Provider)OpenaiProvider是否要重命名为更通用的OpenaiCompatibleProvider? 优先级:低(Phase 4 后梳理)