feat(llm): 新增 IR 类型层 + 切换 LlmProvider trait 签名

引入跨 Provider 统一的新类型系统:
- Message 扁平大枚举(System/User/UserImage/Assistant/ToolResult)
- MessageRequest + MessageResponse + StreamEvent(高精度 IR)
- PartialMessageResponse 含 apply_to/finalize 汇聚算法
- ProviderType enum 扩展 + ProviderCapabilities

切换 LlmProvider trait 签名为 chat(MessageRequest) → MessageResponse,
chat_stream 返回 Stream<Item = Result<StreamEvent, LlmError>>,新增 capabilities()。

StreamEvent 命名冲突处理:旧变体迁移至 old_stream::LegacyStreamEvent,
stream.rs 通过 pub use 重新导出新高精度事件。

OpenaiProvider 添加 Phase 0 临时桥接(MessageRequest ↔ OpenaiChatRequest 转换),
标注 ponytail: Phase 0 临时桥接 标记,Phase 1 重写时移除。

测试 147 passed / 0 failed(新增 31 个新类型测试)。
This commit is contained in:
徐涛
2026-07-02 21:58:07 +08:00
parent 925c8f9729
commit 7c299f1cfd
18 changed files with 2605 additions and 441 deletions
+80 -24
View File
@@ -3,16 +3,29 @@ pub mod registry;
use std::pin::Pin;
use tokio_stream::Stream;
use futures_core::Stream;
use serde::{Deserialize, Serialize};
use crate::llm::error::LlmError;
use crate::llm::types::{ChatRequest, ChatResponse, OpenaiChatChunk};
use async_trait::async_trait;
use crate::llm::types::request_v2::MessageRequest;
use crate::llm::types::response_v2::{MessageResponse, StreamEvent};
/// Provider 类型枚举 —— `create_provider()` 在编译期 exhaustive match 中使用。
///
/// 设计依据(见 `docs/10-llm-provider-refinement.md` §2.5 Decision-05):
/// 当前协议数量(5 种以内)完全可控,enum 的编译期安全检查优于运行时的 `HashMap::get()`。
/// 未来如果扩展到 15+ 种以上,再改为注册表模式。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderType {
OpenAI,
/// OpenAI Chat Completions API(兼容 DeepSeek / Qwen 等 `/chat/completions` 端点)。
OpenaiChat,
/// OpenAI Response API。
OpenaiResponse,
/// Anthropic Messages API。
Anthropic,
/// DeepSeekOpenAI-compatible `/chat/completions`)。
DeepSeek,
/// Qwen / 阿里云百炼(OpenAI-compatible `/chat/completions`)。
Qwen,
}
@@ -21,60 +34,103 @@ impl std::str::FromStr for ProviderType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"openai" => Ok(ProviderType::OpenAI),
"openai" | "openai-chat" | "openai_chat" => Ok(ProviderType::OpenaiChat),
"openai-response" | "openai_response" | "response" => Ok(ProviderType::OpenaiResponse),
"anthropic" | "claude" => Ok(ProviderType::Anthropic),
"deepseek" => Ok(ProviderType::DeepSeek),
"qwen" | "dashscope" | "tongyi" => Ok(ProviderType::Qwen),
_ => Err(format!("未知的 Provider 类型: {}", s)),
_ => Err(format!("未知的 Provider 类型: {s}")),
}
}
}
/// Provider 构造参数 —— 通用 base_url + api_key + model。
pub struct ProviderConfig {
pub base_url: String,
pub api_key: String,
pub model: String,
}
/// Provider 工厂 —— exhaustive match 在编译期保证新 Provider 被注册。
pub fn create_provider(
provider_type: ProviderType,
config: ProviderConfig,
) -> Result<Box<dyn LlmProvider>, LlmError> {
match provider_type {
ProviderType::OpenAI => Ok(Box::new(openai::OpenaiProvider::new(
ProviderType::OpenaiChat => Ok(Box::new(openai::OpenaiProvider::new(
config.base_url,
config.api_key,
config.model,
))),
ProviderType::OpenaiResponse => {
unimplemented!("OpenAI Response Provider 将在 Phase 1 引入")
}
ProviderType::Anthropic => {
unimplemented!("Anthropic Provider 将在 Phase 1 引入")
}
ProviderType::DeepSeek => {
unimplemented!("DeepSeek Provider 尚未实现")
unimplemented!("DeepSeek Provider 将在 Phase 1 引入")
}
ProviderType::Qwen => {
unimplemented!("Qwen Provider 尚未实现")
unimplemented!("Qwen Provider 将在 Phase 1 引入")
}
}
}
/// Provider 能力描述 —— 静态元信息,调用方据此决定可用特性。
///
/// 设计依据(见 `docs/10-llm-provider-refinement.md` §4 任务 6 决策):
/// `ProviderCapabilities` 与 trait 同文件(`provider.rs`),不分散到类型目录。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderCapabilities {
/// 人类可读的 Provider 名(如 `"openai"` / `"anthropic"`)。
pub provider_name: &'static str,
/// 支持的模型列表(`None` 表示"未列举全部")。
pub supported_models: Option<Vec<String>>,
/// 详细功能开关。
pub features: ProviderFeatures,
}
/// Provider 功能开关集合。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProviderFeatures {
/// 是否支持流式响应。
pub streaming: bool,
/// 是否支持 thinking / 推理。
pub thinking: bool,
/// 是否支持图片输入。
pub vision: bool,
/// 是否支持音频输入。
pub audio_input: bool,
/// 是否支持工具调用。
pub tool_use: bool,
/// 是否支持并行工具调用。
pub parallel_tool_calls: bool,
/// system prompt 是否放在 messages 中(`true`)还是顶层 `system` 字段(`false`)。
pub system_prompt_in_messages: bool,
/// 模型上下文窗口(tokens);`0` 表示未知。
pub max_context_window: u32,
}
/// LLM Provider 抽象接口。
///
/// 所有具体的 LLM 后端实现(OpenAI、Anthropic、Azure 等)
/// 所有具体的 LLM 后端实现(OpenAI、Anthropic、DeepSeek、Qwen 等)
/// 均需实现此 trait,以实现可插拔替换。
#[async_trait]
///
/// 修订(Phase 0):签名由 `chat(ChatRequest) → ChatResponse` 切换为
/// `chat(MessageRequest) → MessageResponse``chat_stream` 返回新 `StreamEvent` 流,
/// 新增 `capabilities()` 方法。
#[async_trait::async_trait]
pub trait LlmProvider: Send + Sync {
/// 发送聊天请求并返回完整响应。
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, LlmError>;
async fn chat(&self, request: MessageRequest) -> Result<MessageResponse, LlmError>;
/// 流式聊天请求 —— 返回原始 SSE chunk 流。
///
/// 默认实现回退到非流式调用(包装为单元素流)。
/// 流式聊天请求 —— 返回新 IR `StreamEvent` 流。
async fn chat_stream(
&self,
request: ChatRequest,
) -> Result<
Pin<Box<dyn Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
LlmError,
> {
let response = self.chat(request).await?;
let chunk = OpenaiChatChunk::from(response);
Ok(Box::pin(tokio_stream::once(Ok(chunk))))
}
request: MessageRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError>;
/// 返回 Provider 静态能力描述。
fn capabilities(&self) -> ProviderCapabilities;
}