重构: 拆provider.rs df-ai-core核心类型(strategy核心库)

- 新建 df-ai-core/types.rs(221行): 13类型(CompletionRequest/ContentPart/ChatMessage/MessageRole/ToolDefinition/ToolCall/TokenUsage/StreamChunk/ToolCallDelta等) + now_millis_i64
- provider.rs 572→369: pub use crate::types::* re-export透明 + impl/LlmProvider trait保留
- lib.rs: pub mod types
主代兜底(独立 -p df-ai-core 避 df-ai anthropic_compat 其他会话E0599): cargo check 0 + test 24
strategy: 核心库自底向上, 纯类型抽离, re-export稳定(df_ai::provider::* 与 df_ai_core::provider::限定路径双通, df-ideas印证)
git add指定(df-ai-core/*)
核心库拆分完整: context/anthropic/openai/scan/model_fetch/migrations/executor/model_probe/provider 八大文件
This commit is contained in:
2026-06-19 04:57:13 +08:00
parent 4dee706631
commit 2d6eff66a7
3 changed files with 233 additions and 214 deletions

View File

@@ -7,6 +7,7 @@
pub mod model;
pub mod provider;
pub mod types;
pub use model::*;
pub use provider::*;

View File

@@ -3,70 +3,20 @@
//! 支持 OpenAI 兼容 API覆盖 OpenAI / GLM / DeepSeek / Claude 兼容模式),
//! 含 function calling / tool use 能力。
//!
//! 本文件含 trait + 数据结构定义(零 IO。HTTP implOpenAICompatProvider /
//! AnthropicCompatProvider+ 业务逻辑ContextManager / AiToolRegistry /
//! build_provider 工厂)留在 df-ai crate。
use std::pin::Pin;
//! 本文件含 trait + 类型 impl 块(零 IO。纯类型定义CompletionRequest /
//! ChatMessage / ContentPart 等 `#[derive]` struct/enum已拆分到 `types.rs`
//! 经下方 `pub use crate::types::*` re-export 保持外部路径
//! `df_ai_core::provider::*` / `df_ai::provider::*` 不变(编译期验证)。
//! HTTP implOpenAICompatProvider / AnthropicCompatProvider+ 业务逻辑
//! ContextManager / AiToolRegistry / build_provider 工厂)留在 df-ai crate。
use async_trait::async_trait;
use futures::Stream;
use serde::{Deserialize, Serialize};
// ============================================================
// 核心数据结构
// ============================================================
/// LLM 调用请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionRequest {
/// 模型名称
pub model: String,
/// 提示消息列表
pub messages: Vec<ChatMessage>,
/// 温度0.0 ~ 2.0
pub temperature: Option<f32>,
/// 最大生成 token 数
pub max_tokens: Option<u32>,
/// 是否流式输出
pub stream: bool,
/// 可调用的工具定义
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDefinition>>,
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
/// DeepSeek thinking 模式:需把上一轮 response 的 reasoning_content 回传
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// 多模态消息内容片。Text 片为字符串Image 片可走 url 或 base64二选一base64 非空时 url 忽略)。
///
/// F-260614-05 Phase 2a 后端ContentPart 作为 `ChatMessage.parts` 的元素类型。
/// 设计上 `content: String`(纯文本主载荷)保持不变,多模态片挂在 `parts`
/// 这样未接入多模态的调用方audit/title/commands/knowledge_inject 等读 content 当字符串)
/// 零回归避免一次性改全仓。provider 转换层在 `has_image()` 为真时把 parts 透传给
/// vision 端点,否则只用 content 文本(保持纯文本端点兼容)。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
/// 文本片
Text { text: String },
/// 图片片。
/// - urlhttp(s) 可达 URLprovider 直接转发,不读字节)。
/// - base64data URI 之外的纯 base64 字符串 + media_typeprovider 内嵌转发)。
/// base64 非空时 url 忽略,便于前端上行无网络回拉的本地粘贴图。
Image {
url: Option<String>,
base64: Option<String>,
/// base64 模式必填image/png | image/jpeg | image/webp | image/gifurl 模式可空。
media_type: Option<String>,
/// 可选 altvision 模型/降级文本时用)
#[serde(skip_serializing_if = "Option::is_none")]
alt: Option<String>,
},
}
// 透明 re-exporttypes.rs 中所有 pub 类型经此回流到 provider::* glob,
// 使 df_ai::provider::*df-ai/src/provider.rs:15 的 `pub use df_ai_core::provider::*`
// 与直接 `df_ai_core::provider::Type` 限定路径全部继续可用。
pub use crate::types::*;
use crate::types::now_millis_i64;
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
@@ -90,51 +40,6 @@ impl ContentPart {
}
}
/// 聊天消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
/// 多模态内容片F-260614-05 Phase 2a
///
/// `None`/空 → 纯文本消息绝大多数场景content 即全部载荷)。
/// `Some(含 Image 片)` → 多模态消息provider 在 `has_image()` 为真时把 parts
/// 连同 content作为前置 Text 片)一起转成 OpenAI/Anthropic 的 content blocks。
/// content 字段始终保留人类可读文本(文本消息的 Text 片内容与 content 一致),
/// 保证 audit/title/export 等读 content 当字符串的调用方零回归。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parts: Option<Vec<ContentPart>>,
/// 工具调用 IDrole=Tool 时必填)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
/// AI 发起的工具调用列表role=Assistant 时可能有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
/// 生成该消息的 model仅 assistant 消息有,消息级 model 追溯)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// 消息状态UX-09 编辑重生成None/"active" 正常可见;
/// "truncated" 软删(编辑某条 user 消息后其后续消息标记,保留 DB 可追溯但不进 LLM 上下文、前端视图过滤)
/// 默认 None向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
/// DeepSeek thinking 模式的推理内容(多轮需回传)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
/// 消息创建时间(Unix 毫秒)。仅前端时间戳展示用,序列化进 messages JSON 持久化;
/// provider 请求映射不读此字段(构造器打戳→映射忽略,不进 LLM 请求),老数据反序列化为 None。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
}
/// 当前 Unix 毫秒(ChatMessage 打戳用;df-ai-core 不依赖 df-types,内联避免新增依赖)。
fn now_millis_i64() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self { role: MessageRole::System, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
@@ -194,24 +99,6 @@ impl ChatMessage {
}
}
/// 消息角色
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
/// 工具定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
#[serde(rename = "type")]
pub tool_type: String,
pub function: ToolFunction,
}
impl ToolDefinition {
pub fn function(name: impl Into<String>, description: impl Into<String>, parameters: serde_json::Value) -> Self {
Self {
@@ -221,23 +108,6 @@ impl ToolDefinition {
}
}
/// 函数定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// 工具调用AI 发起)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: ToolCallFunction,
}
impl ToolCall {
pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: impl Into<String>) -> Self {
Self {
@@ -248,79 +118,6 @@ impl ToolCall {
}
}
/// 工具调用函数部分
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
/// LLM 调用响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionResponse {
/// 生成的文本
pub text: String,
/// 使用的模型
pub model: String,
/// 消耗的 token 数
pub usage: TokenUsage,
/// AI 发起的工具调用(如有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
/// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// Token 用量
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
/// 流式输出的 chunk
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChunk {
/// 增量文本
pub delta: String,
/// 是否结束
pub finished: bool,
/// 工具调用增量(如有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
/// Token 用量(流末 chunk 携带,由 provider 解析自 SSE usage 事件)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
/// provider 流式错误事件(如 Anthropic SSE `type=="error"`)。
/// 非空表示流中途出错不应视为正常完成finished 路径),由 stream_llm 转 AiError。
#[serde(skip)]
pub error: Option<String>,
/// DeepSeek thinking 模式推理内容增量
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// 工具调用增量(流式中的片段)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallDelta {
/// 索引
pub index: u32,
/// 工具调用 ID仅第一个 chunk 有)
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// 函数名片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_name: Option<String>,
/// 函数参数片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_arguments: Option<String>,
}
/// 异步流类型别名
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
/// LLM Provider trait
#[async_trait]
pub trait LlmProvider: Send + Sync {

View File

@@ -0,0 +1,221 @@
//! LLM Provider 数据结构定义(纯类型,零 IO
//!
//! 从 provider.rs 拆分的核心数据类型层:请求/响应/消息/工具/Token 等
//! `#[derive]` struct 与 enum + 一个内联时间戳辅助函数。所有 impl 块与
//! `LlmProvider` trait 留在 provider.rs本文件只承载类型定义。
//!
//! 经 `provider.rs` 的 `pub use crate::types::*` re-export外部路径
//! `df_ai_core::provider::*` / `df_ai::provider::*` 保持不变(编译期验证)。
use std::pin::Pin;
use futures::Stream;
use serde::{Deserialize, Serialize};
// ============================================================
// 核心数据结构
// ============================================================
/// LLM 调用请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionRequest {
/// 模型名称
pub model: String,
/// 提示消息列表
pub messages: Vec<ChatMessage>,
/// 温度0.0 ~ 2.0
pub temperature: Option<f32>,
/// 最大生成 token 数
pub max_tokens: Option<u32>,
/// 是否流式输出
pub stream: bool,
/// 可调用的工具定义
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDefinition>>,
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
/// DeepSeek thinking 模式:需把上一轮 response 的 reasoning_content 回传
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// 多模态消息内容片。Text 片为字符串Image 片可走 url 或 base64二选一base64 非空时 url 忽略)。
///
/// F-260614-05 Phase 2a 后端ContentPart 作为 `ChatMessage.parts` 的元素类型。
/// 设计上 `content: String`(纯文本主载荷)保持不变,多模态片挂在 `parts`
/// 这样未接入多模态的调用方audit/title/commands/knowledge_inject 等读 content 当字符串)
/// 零回归避免一次性改全仓。provider 转换层在 `has_image()` 为真时把 parts 透传给
/// vision 端点,否则只用 content 文本(保持纯文本端点兼容)。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
/// 文本片
Text { text: String },
/// 图片片。
/// - urlhttp(s) 可达 URLprovider 直接转发,不读字节)。
/// - base64data URI 之外的纯 base64 字符串 + media_typeprovider 内嵌转发)。
/// base64 非空时 url 忽略,便于前端上行无网络回拉的本地粘贴图。
Image {
url: Option<String>,
base64: Option<String>,
/// base64 模式必填image/png | image/jpeg | image/webp | image/gifurl 模式可空。
media_type: Option<String>,
/// 可选 altvision 模型/降级文本时用)
#[serde(skip_serializing_if = "Option::is_none")]
alt: Option<String>,
},
}
/// 聊天消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
/// 多模态内容片F-260614-05 Phase 2a
///
/// `None`/空 → 纯文本消息绝大多数场景content 即全部载荷)。
/// `Some(含 Image 片)` → 多模态消息provider 在 `has_image()` 为真时把 parts
/// 连同 content作为前置 Text 片)一起转成 OpenAI/Anthropic 的 content blocks。
/// content 字段始终保留人类可读文本(文本消息的 Text 片内容与 content 一致),
/// 保证 audit/title/export 等读 content 当字符串的调用方零回归。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parts: Option<Vec<ContentPart>>,
/// 工具调用 IDrole=Tool 时必填)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
/// AI 发起的工具调用列表role=Assistant 时可能有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
/// 生成该消息的 model仅 assistant 消息有,消息级 model 追溯)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// 消息状态UX-09 编辑重生成None/"active" 正常可见;
/// "truncated" 软删(编辑某条 user 消息后其后续消息标记,保留 DB 可追溯但不进 LLM 上下文、前端视图过滤)
/// 默认 None向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
/// DeepSeek thinking 模式的推理内容(多轮需回传)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
/// 消息创建时间(Unix 毫秒)。仅前端时间戳展示用,序列化进 messages JSON 持久化;
/// provider 请求映射不读此字段(构造器打戳→映射忽略,不进 LLM 请求),老数据反序列化为 None。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
}
/// 当前 Unix 毫秒(ChatMessage 打戳用;df-ai-core 不依赖 df-types,内联避免新增依赖)。
pub(crate) fn now_millis_i64() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
/// 消息角色
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
/// 工具定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
#[serde(rename = "type")]
pub tool_type: String,
pub function: ToolFunction,
}
/// 函数定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// 工具调用AI 发起)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: ToolCallFunction,
}
/// 工具调用函数部分
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
/// LLM 调用响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionResponse {
/// 生成的文本
pub text: String,
/// 使用的模型
pub model: String,
/// 消耗的 token 数
pub usage: TokenUsage,
/// AI 发起的工具调用(如有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
/// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// Token 用量
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
/// 流式输出的 chunk
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChunk {
/// 增量文本
pub delta: String,
/// 是否结束
pub finished: bool,
/// 工具调用增量(如有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
/// Token 用量(流末 chunk 携带,由 provider 解析自 SSE usage 事件)
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
/// provider 流式错误事件(如 Anthropic SSE `type=="error"`)。
/// 非空表示流中途出错不应视为正常完成finished 路径),由 stream_llm 转 AiError。
#[serde(skip)]
pub error: Option<String>,
/// DeepSeek thinking 模式推理内容增量
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
/// 工具调用增量(流式中的片段)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallDelta {
/// 索引
pub index: u32,
/// 工具调用 ID仅第一个 chunk 有)
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// 函数名片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_name: Option<String>,
/// 函数参数片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_arguments: Option<String>,
}
/// 异步流类型别名
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;