重写 OpenaiChatProvider:移除 Phase 0 临时桥接,实现真实流式状态机 (ContentBlockStart / *Delta / ContentBlockEnd / ToolCallEnd), MessageComplete.full_response 由 PartialMessageResponse::finalize 产出。 新增 AnthropicProvider:实现 Messages API + SSE 事件序列 (message_start → content_block_start → content_block_delta → content_block_stop → message_delta → message_stop), 529(overloaded)映射为 RateLimit;thinking signature 由 partial.set_thinking_signature 内部写入。 新增 DeepSeekProvider / QwenProvider:OpenAI-compatible 协议的 newtype 包装,共享 GenericOpenaiProvider 的 HTTP / SSE / 转换逻辑; Qwen 通过 extra_headers 注入 X-DashScope-SSE: enable。 新增 convert.rs 公共转换模块:从 Phase 0 cycle.rs / Phase 0 OpenaiProvider 桥接层提取 from_openai / to_openai / content_to_blocks / blocks_to_content,避免跨 Provider 重复逻辑。 新增 wiremock dev-dependency + 14 个集成测试: - OpenaiChatProvider:基础文本 / 401 / 500 / 流式 MessageComplete - AnthropicProvider:基础 / 401 / 529 / 流式 SSE 序列 / max_tokens 默认值 - DeepSeek / Qwen:基础文本响应 171 个测试全部通过(之前 157,新增 14)。
145 lines
5.3 KiB
Rust
145 lines
5.3 KiB
Rust
pub mod anthropic;
|
||
pub mod openai;
|
||
pub mod openai_compat;
|
||
pub mod registry;
|
||
|
||
use std::pin::Pin;
|
||
|
||
use futures_core::Stream;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::llm::error::LlmError;
|
||
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 Chat Completions API(兼容 DeepSeek / Qwen 等 `/chat/completions` 端点)。
|
||
OpenaiChat,
|
||
/// OpenAI Response API。
|
||
OpenaiResponse,
|
||
/// Anthropic Messages API。
|
||
Anthropic,
|
||
/// DeepSeek(OpenAI-compatible `/chat/completions`)。
|
||
DeepSeek,
|
||
/// Qwen / 阿里云百炼(OpenAI-compatible `/chat/completions`)。
|
||
Qwen,
|
||
}
|
||
|
||
impl std::str::FromStr for ProviderType {
|
||
type Err = String;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
match s.to_lowercase().as_str() {
|
||
"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}")),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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::OpenaiChat => Ok(Box::new(openai::OpenaiChatProvider::new(
|
||
config.base_url,
|
||
config.api_key,
|
||
config.model,
|
||
))),
|
||
ProviderType::OpenaiResponse => Err(LlmError::Other(
|
||
"OpenaiResponse Provider 在 Phase 1 暂不实现;请使用 OpenaiChat".into(),
|
||
)),
|
||
ProviderType::Anthropic => Ok(Box::new(anthropic::AnthropicProvider::new(
|
||
config.base_url,
|
||
config.api_key,
|
||
config.model,
|
||
))),
|
||
ProviderType::DeepSeek => Ok(Box::new(openai_compat::DeepSeekProvider::new(
|
||
config.base_url,
|
||
config.api_key,
|
||
config.model,
|
||
))),
|
||
ProviderType::Qwen => Ok(Box::new(openai_compat::QwenProvider::new(
|
||
config.base_url,
|
||
config.api_key,
|
||
config.model,
|
||
))),
|
||
}
|
||
}
|
||
|
||
/// 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、DeepSeek、Qwen 等)
|
||
/// 均需实现此 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: MessageRequest) -> Result<MessageResponse, LlmError>;
|
||
|
||
/// 流式聊天请求 —— 返回新 IR `StreamEvent` 流。
|
||
async fn chat_stream(
|
||
&self,
|
||
request: MessageRequest,
|
||
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError>;
|
||
|
||
/// 返回 Provider 静态能力描述。
|
||
fn capabilities(&self) -> ProviderCapabilities;
|
||
}
|