From 2d6eff66a7dca1014afc1b088b4b1b3fbf62ca4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Fri, 19 Jun 2026 04:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E6=8B=86provider.rs=20df?= =?UTF-8?q?-ai-core=E6=A0=B8=E5=BF=83=E7=B1=BB=E5=9E=8B(strategy=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=BA=93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 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 八大文件 --- crates/df-ai-core/src/lib.rs | 1 + crates/df-ai-core/src/provider.rs | 225 ++---------------------------- crates/df-ai-core/src/types.rs | 221 +++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 214 deletions(-) create mode 100644 crates/df-ai-core/src/types.rs diff --git a/crates/df-ai-core/src/lib.rs b/crates/df-ai-core/src/lib.rs index a925443..7746939 100644 --- a/crates/df-ai-core/src/lib.rs +++ b/crates/df-ai-core/src/lib.rs @@ -7,6 +7,7 @@ pub mod model; pub mod provider; +pub mod types; pub use model::*; pub use provider::*; diff --git a/crates/df-ai-core/src/provider.rs b/crates/df-ai-core/src/provider.rs index 6ee8cb8..f7ed120 100644 --- a/crates/df-ai-core/src/provider.rs +++ b/crates/df-ai-core/src/provider.rs @@ -3,70 +3,20 @@ //! 支持 OpenAI 兼容 API(覆盖 OpenAI / GLM / DeepSeek / Claude 兼容模式), //! 含 function calling / tool use 能力。 //! -//! 本文件仅含 trait + 数据结构定义(零 IO)。HTTP impl(OpenAICompatProvider / -//! 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 impl(OpenAICompatProvider / 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, - /// 温度(0.0 ~ 2.0) - pub temperature: Option, - /// 最大生成 token 数 - pub max_tokens: Option, - /// 是否流式输出 - pub stream: bool, - /// 可调用的工具定义 - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option>, - /// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"} - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - /// DeepSeek thinking 模式:需把上一轮 response 的 reasoning_content 回传 - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_content: Option, -} - -/// 多模态消息内容片。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 }, - /// 图片片。 - /// - url:http(s) 可达 URL(provider 直接转发,不读字节)。 - /// - base64:data URI 之外的纯 base64 字符串 + media_type(provider 内嵌转发)。 - /// base64 非空时 url 忽略,便于前端上行无网络回拉的本地粘贴图。 - Image { - url: Option, - base64: Option, - /// base64 模式必填(image/png | image/jpeg | image/webp | image/gif);url 模式可空。 - media_type: Option, - /// 可选 alt(vision 模型/降级文本时用) - #[serde(skip_serializing_if = "Option::is_none")] - alt: Option, - }, -} +// 透明 re-export:types.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) -> 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>, - /// 工具调用 ID(role=Tool 时必填) - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, - /// AI 发起的工具调用列表(role=Assistant 时可能有) - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_calls: Option>, - /// 生成该消息的 model(仅 assistant 消息有,消息级 model 追溯) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - /// 消息状态(UX-09 编辑重生成):None/"active" 正常可见; - /// "truncated" 软删(编辑某条 user 消息后其后续消息标记,保留 DB 可追溯但不进 LLM 上下文、前端视图过滤) - /// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, - /// DeepSeek thinking 模式的推理内容(多轮需回传) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_content: Option, - /// 消息创建时间(Unix 毫秒)。仅前端时间戳展示用,序列化进 messages JSON 持久化; - /// provider 请求映射不读此字段(构造器打戳→映射忽略,不进 LLM 请求),老数据反序列化为 None。 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub timestamp: Option, -} - -/// 当前 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) -> 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, description: impl Into, 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, name: impl Into, arguments: impl Into) -> 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>, - /// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_content: Option, -} - -/// 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>, - /// Token 用量(流末 chunk 携带,由 provider 解析自 SSE usage 事件) - #[serde(skip_serializing_if = "Option::is_none")] - pub usage: Option, - /// provider 流式错误事件(如 Anthropic SSE `type=="error"`)。 - /// 非空表示流中途出错,不应视为正常完成(finished 路径),由 stream_llm 转 AiError。 - #[serde(skip)] - pub error: Option, - /// DeepSeek thinking 模式推理内容增量 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_content: Option, -} - -/// 工具调用增量(流式中的片段) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolCallDelta { - /// 索引 - pub index: u32, - /// 工具调用 ID(仅第一个 chunk 有) - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - /// 函数名片段 - #[serde(skip_serializing_if = "Option::is_none")] - pub function_name: Option, - /// 函数参数片段 - #[serde(skip_serializing_if = "Option::is_none")] - pub function_arguments: Option, -} - -/// 异步流类型别名 -pub type StreamResult = Pin> + Send>>; - /// LLM Provider trait #[async_trait] pub trait LlmProvider: Send + Sync { diff --git a/crates/df-ai-core/src/types.rs b/crates/df-ai-core/src/types.rs new file mode 100644 index 0000000..9fcbb6b --- /dev/null +++ b/crates/df-ai-core/src/types.rs @@ -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, + /// 温度(0.0 ~ 2.0) + pub temperature: Option, + /// 最大生成 token 数 + pub max_tokens: Option, + /// 是否流式输出 + pub stream: bool, + /// 可调用的工具定义 + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"} + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, + /// DeepSeek thinking 模式:需把上一轮 response 的 reasoning_content 回传 + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, +} + +/// 多模态消息内容片。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 }, + /// 图片片。 + /// - url:http(s) 可达 URL(provider 直接转发,不读字节)。 + /// - base64:data URI 之外的纯 base64 字符串 + media_type(provider 内嵌转发)。 + /// base64 非空时 url 忽略,便于前端上行无网络回拉的本地粘贴图。 + Image { + url: Option, + base64: Option, + /// base64 模式必填(image/png | image/jpeg | image/webp | image/gif);url 模式可空。 + media_type: Option, + /// 可选 alt(vision 模型/降级文本时用) + #[serde(skip_serializing_if = "Option::is_none")] + alt: Option, + }, +} + +/// 聊天消息 +#[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>, + /// 工具调用 ID(role=Tool 时必填) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + /// AI 发起的工具调用列表(role=Assistant 时可能有) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + /// 生成该消息的 model(仅 assistant 消息有,消息级 model 追溯) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// 消息状态(UX-09 编辑重生成):None/"active" 正常可见; + /// "truncated" 软删(编辑某条 user 消息后其后续消息标记,保留 DB 可追溯但不进 LLM 上下文、前端视图过滤) + /// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + /// DeepSeek thinking 模式的推理内容(多轮需回传) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, + /// 消息创建时间(Unix 毫秒)。仅前端时间戳展示用,序列化进 messages JSON 持久化; + /// provider 请求映射不读此字段(构造器打戳→映射忽略,不进 LLM 请求),老数据反序列化为 None。 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +/// 当前 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>, + /// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, +} + +/// 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>, + /// Token 用量(流末 chunk 携带,由 provider 解析自 SSE usage 事件) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage: Option, + /// provider 流式错误事件(如 Anthropic SSE `type=="error"`)。 + /// 非空表示流中途出错,不应视为正常完成(finished 路径),由 stream_llm 转 AiError。 + #[serde(skip)] + pub error: Option, + /// DeepSeek thinking 模式推理内容增量 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, +} + +/// 工具调用增量(流式中的片段) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCallDelta { + /// 索引 + pub index: u32, + /// 工具调用 ID(仅第一个 chunk 有) + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// 函数名片段 + #[serde(skip_serializing_if = "Option::is_none")] + pub function_name: Option, + /// 函数参数片段 + #[serde(skip_serializing_if = "Option::is_none")] + pub function_arguments: Option, +} + +/// 异步流类型别名 +pub type StreamResult = Pin> + Send>>;