新增: 初始化 DevFlow 项目仓库

Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate
(df-ai / df-storage / df-workflow / df-core / df-execute 等)。
核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、
任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
2026-06-12 01:31:05 +08:00
commit 98393b4908
178 changed files with 27859 additions and 0 deletions

18
crates/df-ai/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "df-ai"
version = "0.1.0"
edition = "2021"
[dependencies]
df-core = { path = "../df-core" }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
async-trait = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
# HTTP + 流式
reqwest = { version = "0.12", features = ["stream", "json"] }
futures = "0.3"
eventsource-stream = "0.2"

View File

@@ -0,0 +1,166 @@
//! AI 工具注册 — 将现有 CRUD 操作注册为 LLM 可调用的 Tool
//!
//! 风险分级:
//! - Low: 只读操作list/getAI 自动执行
//! - Medium: 创建操作create显示意图可配置自动批准
//! - High: 破坏性操作delete / run_workflow必须人工批准
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::provider::ToolDefinition;
// ============================================================
// 风险级别
// ============================================================
/// 工具调用风险级别
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RiskLevel {
/// 只读操作 — AI 自动执行
Low,
/// 创建操作 — 可配置自动批准
Medium,
/// 破坏性操作 — 必须人工批准
High,
}
// ============================================================
// 工具定义
// ============================================================
/// 已注册的 AI 工具
pub struct AiTool {
/// 工具定义(发送给 LLM 的 JSON Schema
pub definition: ToolDefinition,
/// 风险级别
pub risk_level: RiskLevel,
/// 执行处理器
pub handler: AiToolHandler,
}
/// 工具处理器类型 — 异步函数,接收 JSON 参数,返回 JSON 结果
pub type AiToolHandler =
Box<dyn Fn(Value) -> Pin<Box<dyn Future<Output = anyhow::Result<Value>> + Send>> + Send + Sync>;
// ============================================================
// 工具注册表
// ============================================================
/// AI 工具注册表
pub struct AiToolRegistry {
tools: HashMap<String, AiTool>,
}
impl AiToolRegistry {
/// 创建空注册表
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
/// 注册一个工具
pub fn register(
&mut self,
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
risk_level: RiskLevel,
handler: AiToolHandler,
) {
let name_str = name.into();
let definition = ToolDefinition::function(&name_str, description, parameters);
self.tools.insert(
name_str,
AiTool {
definition,
risk_level,
handler,
},
);
}
/// 获取所有工具定义(发送给 LLM 的 tools 参数)
pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
self.tools.values().map(|t| t.definition.clone()).collect()
}
/// 根据名称获取工具
pub fn get(&self, name: &str) -> Option<&AiTool> {
self.tools.get(name)
}
/// 获取所有已注册工具名称
pub fn tool_names(&self) -> Vec<String> {
self.tools.keys().cloned().collect()
}
/// 已注册工具数量
pub fn len(&self) -> usize {
self.tools.len()
}
/// 是否为空
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
}
impl Default for AiToolRegistry {
fn default() -> Self {
Self::new()
}
}
// ============================================================
// 工具执行结果
// ============================================================
/// 工具执行结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecutionResult {
/// 工具调用 ID
pub tool_call_id: String,
/// 工具名称
pub tool_name: String,
/// 执行参数
pub arguments: Value,
/// 执行结果
pub result: Value,
/// 是否需要人工批准
pub approval_required: bool,
/// 风险级别
pub risk_level: RiskLevel,
}
// ============================================================
// 辅助: 构建 JSON Schema 参数
// ============================================================
/// 构建一个简单的 object JSON Schema
pub fn object_schema(properties: Vec<(&str, &str, bool)>) -> Value {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for (name, type_str, is_required) in properties {
props.insert(
name.to_string(),
serde_json::json!({ "type": type_str, "description": "" }),
);
if is_required {
required.push(name.to_string());
}
}
serde_json::json!({
"type": "object",
"properties": props,
"required": required,
})
}

View File

@@ -0,0 +1,426 @@
//! Anthropic 兼容 Provider — 通过 /v1/messages 端点实现
//!
//! 覆盖: Claude 官方 / GLM 订阅端点 (open.bigmodel.cn/api/anthropic) / 任意 Messages API 网关
//! 支持: 同步调用 + SSE 流式 + Tool Use
//!
//! 与 OpenAI 协议的关键差异由本模块内部完成转换,对外仍暴露统一的 LlmProvider trait
//! 上层 (Agentic Loop / AiNode) 无需感知协议。
use async_trait::async_trait;
use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, error, warn};
use crate::provider::{
CompletionRequest, CompletionResponse, LlmProvider, MessageRole, ProviderFeatures,
StreamChunk, StreamResult, TokenUsage, ToolCall, ToolCallDelta,
};
// ============================================================
// Anthropic API 请求/响应结构体
// ============================================================
/// Anthropic 请求体
#[derive(Debug, Serialize)]
struct AnthropicRequest {
model: String,
messages: Vec<serde_json::Value>,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicToolDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<serde_json::Value>,
}
/// Anthropic 工具定义input_schema 对应 OpenAI 的 parameters
#[derive(Debug, Serialize)]
struct AnthropicToolDef {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
input_schema: serde_json::Value,
}
/// Anthropic 同步响应
#[derive(Debug, Deserialize)]
struct AnthropicResponse {
#[allow(dead_code)]
id: String,
model: String,
content: Vec<AnthropicContentBlock>,
#[allow(dead_code)]
stop_reason: Option<String>,
usage: AnthropicUsage,
}
/// 响应 content 块text 或 tool_use
#[derive(Debug, Deserialize)]
struct AnthropicContentBlock {
#[serde(rename = "type")]
block_type: String,
#[serde(default)]
text: Option<String>,
/// tool_use 块字段
id: Option<String>,
name: Option<String>,
input: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct AnthropicUsage {
input_tokens: u32,
output_tokens: u32,
}
// ============================================================
// Provider 实现
// ============================================================
/// Anthropic 兼容 LLM Provider
pub struct AnthropicCompatProvider {
client: Client,
api_key: String,
base_url: String,
default_model: String,
}
/// Anthropic 流式协议版本头
const ANTHROPIC_VERSION: &str = "2023-06-01";
/// Anthropic max_tokens 必填,缺省时的兜底值
const DEFAULT_MAX_TOKENS: u32 = 4096;
impl AnthropicCompatProvider {
/// 创建 Provider
///
/// - `base_url`: 如 `https://api.anthropic.com`、`https://open.bigmodel.cn/api/anthropic`
/// - `api_key`: API 密钥Anthropic 用 x-api-key 头,非 Bearer
/// - `default_model`: 默认模型名称
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
let client = Client::builder()
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|e| {
warn!("reqwest builder 失败,降级为默认 client: {}", e);
Client::new()
});
Self {
client,
api_key: api_key.into(),
base_url: base_url.into(),
default_model: default_model.into(),
}
}
/// 构建 messages 端点 URL
///
/// - 已含 `/v1/messages` → 直接用
/// - 以 `/v1` 结尾 → 补 `/messages`
/// - 否则(如 `.../api/anthropic`、`api.anthropic.com`)→ 补 `/v1/messages`
fn messages_url(&self) -> String {
let base = self.base_url.trim_end_matches('/');
if base.ends_with("/v1/messages") {
return base.to_string();
}
if base.ends_with("/v1") {
return format!("{}/messages", base);
}
format!("{}/v1/messages", base)
}
/// 将统一 CompletionRequest 转换为 Anthropic 请求体
///
/// 转换要点:
/// - system 消息从 messages 抽离到顶层 system 字段
/// - assistant 带 tool_calls → content 数组含 text + tool_use 块
/// - tool_resultrole=Tool→ user 消息含 tool_result 块;连续多个合并为一条 user
/// - tool_definitions 的 parameters → input_schema
fn convert_request(&self, req: CompletionRequest) -> AnthropicRequest {
let model = if req.model.is_empty() {
self.default_model.clone()
} else {
req.model
};
// 抽离 system 消息
let system: Option<String> = {
let sys: Vec<String> = req
.messages
.iter()
.filter(|m| matches!(m.role, MessageRole::System))
.map(|m| m.content.clone())
.collect();
if sys.is_empty() {
None
} else {
Some(sys.join("\n\n"))
}
};
// 构建非 system 消息(保留顺序,合并连续 tool_result
let mut messages: Vec<serde_json::Value> = Vec::new();
let mut pending_tool_results: Vec<serde_json::Value> = Vec::new();
for m in req.messages.iter() {
match m.role {
MessageRole::System => continue,
MessageRole::Tool => {
// 累积 tool_result 块,遇到非 Tool 消息时 flush
pending_tool_results.push(serde_json::json!({
"type": "tool_result",
"tool_use_id": m.tool_call_id,
"content": m.content,
}));
}
MessageRole::User => {
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
messages.push(serde_json::json!({ "role": "user", "content": m.content }));
}
MessageRole::Assistant => {
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
let mut content: Vec<serde_json::Value> = Vec::new();
if !m.content.is_empty() {
content.push(serde_json::json!({ "type": "text", "text": m.content }));
}
if let Some(calls) = &m.tool_calls {
for tc in calls {
let input: serde_json::Value =
serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null);
content.push(serde_json::json!({
"type": "tool_use",
"id": tc.id,
"name": tc.function.name,
"input": input,
}));
}
}
if content.is_empty() {
content.push(serde_json::json!({ "type": "text", "text": "" }));
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
}
}
}
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
let tools = req.tools.map(|defs| {
defs.into_iter()
.map(|d| AnthropicToolDef {
name: d.function.name,
description: Some(d.function.description).filter(|s| !s.is_empty()),
input_schema: d.function.parameters,
})
.collect()
});
AnthropicRequest {
model,
messages,
max_tokens: req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
system,
temperature: req.temperature,
stream: req.stream,
tools,
tool_choice: req.tool_choice,
}
}
/// 将累积的 tool_result 块作为一条 user 消息 flush 进消息列表
fn flush_tool_results(
messages: &mut Vec<serde_json::Value>,
pending: &mut Vec<serde_json::Value>,
) {
if pending.is_empty() {
return;
}
let blocks: Vec<serde_json::Value> = pending.drain(..).collect();
messages.push(serde_json::json!({ "role": "user", "content": blocks }));
}
/// 统一鉴权头x-api-key + anthropic-version
fn auth_headers(&self, rb: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
rb.header("x-api-key", &self.api_key)
.header("anthropic-version", ANTHROPIC_VERSION)
.header("Content-Type", "application/json")
}
}
#[async_trait]
impl LlmProvider for AnthropicCompatProvider {
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse> {
let mut req = request;
req.stream = false;
let body = self.convert_request(req);
debug!(model = %body.model, "Anthropic 同步调用");
let resp = self
.auth_headers(self.client.post(self.messages_url()))
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
error!(%status, %text, "Anthropic API 调用失败");
anyhow::bail!("Anthropic API 错误 {}: {}", status, text);
}
let resp: AnthropicResponse = resp.json().await?;
// content 块中拼接 text收集 tool_use
let mut text = String::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
for block in resp.content {
match block.block_type.as_str() {
"text" => {
if let Some(t) = block.text {
text.push_str(&t);
}
}
"tool_use" => {
let id = block.id.unwrap_or_default();
let name = block.name.unwrap_or_default();
let args = block
.input
.map(|v| serde_json::to_string(&v).unwrap_or_default())
.unwrap_or_default();
tool_calls.push(ToolCall::new(id, name, args));
}
other => warn!(block_type = other, "Anthropic 响应含未知 content 块类型,已忽略"),
}
}
let usage = TokenUsage {
prompt_tokens: resp.usage.input_tokens,
completion_tokens: resp.usage.output_tokens,
total_tokens: resp.usage.input_tokens + resp.usage.output_tokens,
};
Ok(CompletionResponse {
text,
model: resp.model,
usage,
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
})
}
async fn stream(&self, request: CompletionRequest) -> anyhow::Result<StreamResult> {
let mut req = request;
req.stream = true;
let body = self.convert_request(req);
debug!(model = %body.model, "Anthropic 流式调用");
let resp = self
.auth_headers(self.client.post(self.messages_url()))
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
error!(%status, %text, "Anthropic 流式 API 调用失败");
anyhow::bail!("Anthropic 流式 API 错误 {}: {}", status, text);
}
// 流式解析eventsource 逐事件处理,按 type 字段分发转 StreamChunk
let stream = resp
.bytes_stream()
.eventsource()
.map(move |event| match event {
Ok(ev) => {
// 解析 data 中的 JSON按 type 字段决定如何转 StreamChunk
let v: serde_json::Value = match serde_json::from_str(&ev.data) {
Ok(v) => v,
Err(_) => return Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }),
};
let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
match ty {
// 文本增量
"content_block_delta" => {
if let Some(delta) = v.get("delta") {
if delta.get("type").and_then(|t| t.as_str()) == Some("text_delta") {
let text = delta.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string();
return Ok(StreamChunk { delta: text, finished: false, tool_calls: None });
}
// 工具入参增量
if delta.get("type").and_then(|t| t.as_str()) == Some("input_json_delta") {
let partial = delta.get("partial_json").and_then(|t| t.as_str()).unwrap_or("").to_string();
let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32;
return Ok(StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id: None,
function_name: None,
function_arguments: Some(partial),
}]),
});
}
}
Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None })
}
// 工具块开始:带 id + name
"content_block_start" => {
if let Some(cb) = v.get("content_block") {
if cb.get("type").and_then(|t| t.as_str()) == Some("tool_use") {
let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32;
let id = cb.get("id").and_then(|t| t.as_str()).map(|s| s.to_string());
let name = cb.get("name").and_then(|t| t.as_str()).map(|s| s.to_string());
return Ok(StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id,
function_name: name,
function_arguments: None,
}]),
});
}
}
Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None })
}
// 消息结束
"message_stop" => Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None }),
// 错误事件
"error" => {
let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error");
error!(%msg, "Anthropic 流式错误事件");
Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None })
}
// message_start / content_block_stop / message_delta 等不产出 chunk
_ => Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }),
}
}
Err(e) => {
error!(error = %e, "Anthropic SSE 事件流错误");
Err(anyhow::anyhow!("Anthropic SSE 错误: {}", e))
}
});
Ok(Box::pin(stream))
}
fn name(&self) -> &str {
"anthropic-compat"
}
fn supported_features(&self) -> ProviderFeatures {
ProviderFeatures {
streaming: true,
function_calling: true,
vision: false,
}
}
}

View File

@@ -0,0 +1,59 @@
//! 上下文管理器 — 管理对话上下文和 token 预算
use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
use crate::provider::ChatMessage;
/// 上下文窗口配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
/// 最大 token 数
pub max_tokens: u32,
/// 保留的系统提示 token 数
pub system_reserve: u32,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_tokens: 128_000,
system_reserve: 4_000,
}
}
}
/// 上下文管理器
pub struct ContextManager {
/// 消息历史
messages: VecDeque<ChatMessage>,
/// 配置
config: ContextConfig,
}
impl ContextManager {
/// 创建上下文管理器
pub fn new(config: ContextConfig) -> Self {
Self {
messages: VecDeque::new(),
config,
}
}
/// 添加消息
pub fn push(&mut self, message: ChatMessage) {
self.messages.push_back(message);
// TODO: 当超过 token 预算时,淘汰旧消息
}
/// 获取当前消息列表
pub fn messages(&self) -> &VecDeque<ChatMessage> {
&self.messages
}
/// 清空上下文
pub fn clear(&mut self) {
self.messages.clear();
}
}

View File

@@ -0,0 +1,28 @@
//! Agent 协调器 — 管理多 Agent 协作
/// Agent 协调器
///
/// TODO: 实现多 Agent 协作逻辑
pub struct AgentCoordinator;
impl AgentCoordinator {
/// 创建协调器
pub fn new() -> Self {
Self
}
/// 启动 Agent 协作任务
///
/// TODO: 实现 Agent 间消息传递和任务分配
pub async fn run(&self, _task: &str) -> anyhow::Result<String> {
tracing::info!("AgentCoordinator: 协调任务开始");
// TODO: 实现多 Agent 协作
Ok("TODO: Agent 协作结果".to_string())
}
}
impl Default for AgentCoordinator {
fn default() -> Self {
Self::new()
}
}

10
crates/df-ai/src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
//! df-ai: AI 编排 — LLM Provider、模型路由、Agent 协调、上下文管理、流式处理、工具注册
pub mod ai_tools;
pub mod anthropic_compat;
pub mod context;
pub mod coordinator;
pub mod openai_compat;
pub mod provider;
pub mod router;
pub mod stream;

View File

@@ -0,0 +1,410 @@
//! OpenAI 兼容 Provider — 通过 /v1/chat/completions 端点实现
//!
//! 覆盖: OpenAI / GLM (open.bigmodel.cn) / DeepSeek / Claude OpenAI 兼容模式
//! 支持: 同步调用 + SSE 流式 + Function Calling / Tool Use
use std::pin::Pin;
use async_trait::async_trait;
use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, error, info, warn};
use crate::provider::{
CompletionRequest, CompletionResponse, LlmProvider, ProviderFeatures, StreamChunk, StreamResult,
TokenUsage, ToolCall, ToolCallDelta,
};
// ============================================================
// OpenAI API 请求/响应结构体
// ============================================================
/// OpenAI 兼容请求体
#[derive(Debug, Serialize)]
struct OpenAiRequest {
model: String,
messages: Vec<OpenAiMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<serde_json::Value>,
}
/// OpenAI 消息格式
#[derive(Debug, Serialize, Deserialize)]
struct OpenAiMessage {
role: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_calls: Option<Vec<serde_json::Value>>,
}
/// OpenAI 同步响应
#[derive(Debug, Deserialize)]
struct OpenAiResponse {
choices: Vec<OpenAiChoice>,
model: String,
usage: Option<OpenAiUsage>,
}
#[derive(Debug, Deserialize)]
struct OpenAiChoice {
message: OpenAiMessageResp,
finish_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OpenAiMessageResp {
content: Option<String>,
tool_calls: Option<Vec<OpenAiToolCallResp>>,
}
#[derive(Debug, Deserialize)]
struct OpenAiToolCallResp {
id: String,
#[serde(rename = "type")]
call_type: String,
function: OpenAiFunctionResp,
}
#[derive(Debug, Deserialize)]
struct OpenAiFunctionResp {
name: String,
arguments: String,
}
#[derive(Debug, Deserialize)]
struct OpenAiUsage {
prompt_tokens: u32,
completion_tokens: u32,
total_tokens: u32,
}
/// SSE 流式响应 chunk
#[derive(Debug, Deserialize)]
struct OpenAiStreamChunk {
choices: Vec<OpenAiStreamChoice>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamChoice {
delta: OpenAiStreamDelta,
finish_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamDelta {
content: Option<String>,
tool_calls: Option<Vec<OpenAiStreamToolCall>>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamToolCall {
index: u32,
id: Option<String>,
function: Option<OpenAiStreamFunction>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamFunction {
name: Option<String>,
arguments: Option<String>,
}
// ============================================================
// OpenAI Compat Provider
// ============================================================
/// OpenAI 兼容 LLM Provider
pub struct OpenAICompatProvider {
client: Client,
api_key: String,
base_url: String,
default_model: String,
}
impl OpenAICompatProvider {
/// 创建 Provider
///
/// - `base_url`: 如 "https://api.openai.com", "https://open.bigmodel.cn/api/paas", "https://api.deepseek.com"
/// - `api_key`: API 密钥
/// - `default_model`: 默认模型名称
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
// connect_timeout 防连接阶段无限 hang网络静默断不设总 timeout——
// reqwest 的 .timeout() 会限制整个响应 body 时长,流式长生成任务会被误砍。
// 读取阶段的中途静默由上层 stream_llm 的 idle timeout 兜底。
let client = Client::builder()
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|e| {
warn!("reqwest builder 失败,降级为默认 client无 connect_timeout: {}", e);
Client::new()
});
Self {
client,
api_key: api_key.into(),
base_url: base_url.into(),
default_model: default_model.into(),
}
}
/// 构建完整 API URL
///
/// 智能拼接,兼容三种 base_url 约定:
/// - 已含完整端点(…/chat/completions→ 直接用
/// - 已含版本段(…/v1 …/v4 等,如 GLM 的 /api/paas/v4→ 补 /chat/completions
/// - 仅域名无版本(如 api.openai.com / api.deepseek.com→ 补 /v1/chat/completionsOpenAI 约定)
fn chat_url(&self) -> String {
let base = self.base_url.trim_end_matches('/');
if base.ends_with("/chat/completions") {
return base.to_string();
}
if Self::ends_with_version(base) {
return format!("{}/chat/completions", base);
}
format!("{}/v1/chat/completions", base)
}
/// base_url 是否以 `/v<数字>` 结尾(如 /v1 /v4
fn ends_with_version(base: &str) -> bool {
match base.rsplit_once('/') {
Some((_, last)) if last.starts_with('v') && last.len() > 1 => {
last[1..].bytes().all(|b| b.is_ascii_digit())
}
_ => false,
}
}
/// 将通用请求转换为 OpenAI 格式
fn convert_request(&self, req: CompletionRequest) -> OpenAiRequest {
let model = if req.model.is_empty() {
self.default_model.clone()
} else {
req.model
};
let messages: Vec<OpenAiMessage> = req
.messages
.into_iter()
.map(|m| {
let role = match m.role {
crate::provider::MessageRole::System => "system",
crate::provider::MessageRole::User => "user",
crate::provider::MessageRole::Assistant => "assistant",
crate::provider::MessageRole::Tool => "tool",
};
let tool_calls = m.tool_calls.map(|calls| {
calls
.into_iter()
.map(|tc| {
serde_json::json!({
"id": tc.id,
"type": tc.call_type,
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
}
})
})
.collect()
});
OpenAiMessage {
role: role.to_string(),
content: m.content,
tool_call_id: m.tool_call_id,
tool_calls,
}
})
.collect();
let tools = req.tools.map(|defs| {
defs.into_iter()
.map(|d| serde_json::to_value(d).unwrap_or_default())
.collect()
});
OpenAiRequest {
model,
messages,
temperature: req.temperature,
max_tokens: req.max_tokens,
stream: req.stream,
tools,
tool_choice: req.tool_choice,
}
}
/// 解析同步响应中的工具调用
fn parse_tool_calls(calls: Vec<OpenAiToolCallResp>) -> Vec<ToolCall> {
calls
.into_iter()
.map(|c| ToolCall::new(c.id, c.function.name, c.function.arguments))
.collect()
}
}
#[async_trait]
impl LlmProvider for OpenAICompatProvider {
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse> {
let mut req = request;
req.stream = false;
let openai_req = self.convert_request(req);
debug!(model = %openai_req.model, "OpenAI 同步调用");
let resp = self
.client
.post(self.chat_url())
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&openai_req)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
error!(%status, %body, "LLM API 调用失败");
anyhow::bail!("LLM API 错误 {}: {}", status, body);
}
let body: OpenAiResponse = resp.json().await?;
let choice = body
.choices
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("LLM 响应无 choices"))?;
let text = choice.message.content.unwrap_or_default();
let tool_calls = choice.message.tool_calls.map(Self::parse_tool_calls);
let usage = body.usage.map(|u| TokenUsage {
prompt_tokens: u.prompt_tokens,
completion_tokens: u.completion_tokens,
total_tokens: u.total_tokens,
}).unwrap_or(TokenUsage {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
});
Ok(CompletionResponse {
text,
model: body.model,
usage,
tool_calls,
})
}
async fn stream(&self, request: CompletionRequest) -> anyhow::Result<StreamResult> {
let mut req = request;
req.stream = true;
let openai_req = self.convert_request(req);
debug!(model = %openai_req.model, "OpenAI 流式调用");
let resp = self
.client
.post(self.chat_url())
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&openai_req)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
error!(%status, %body, "LLM 流式 API 调用失败");
anyhow::bail!("LLM 流式 API 错误 {}: {}", status, body);
}
let stream = resp
.bytes_stream()
.eventsource()
.map(move |event| {
match event {
Ok(event) => {
// OpenAI 发送 "data: [DONE]" 表示流结束
if event.data == "[DONE]" {
return Ok(StreamChunk {
delta: String::new(),
finished: true,
tool_calls: None,
});
}
match serde_json::from_str::<OpenAiStreamChunk>(&event.data) {
Ok(chunk) => {
if let Some(choice) = chunk.choices.into_iter().next() {
let delta_text = choice.delta.content.unwrap_or_default();
// "length" = max_tokens 截断,属正常终止(非断连),纳入 finished
let finished = choice.finish_reason.as_deref() == Some("stop")
|| choice.finish_reason.as_deref() == Some("tool_calls")
|| choice.finish_reason.as_deref() == Some("length");
let tool_calls = choice.delta.tool_calls.map(|tcs| {
tcs.into_iter()
.map(|tc| ToolCallDelta {
index: tc.index,
id: tc.id,
function_name: tc.function.as_ref().and_then(|f| f.name.clone()),
function_arguments: tc.function.and_then(|f| f.arguments),
})
.collect()
});
Ok(StreamChunk {
delta: delta_text,
finished,
tool_calls,
})
} else {
Ok(StreamChunk {
delta: String::new(),
finished: false,
tool_calls: None,
})
}
}
Err(e) => {
debug!("SSE 数据解析失败: {} — data: {}", e, event.data);
Ok(StreamChunk {
delta: String::new(),
finished: false,
tool_calls: None,
})
}
}
}
Err(e) => {
error!("SSE 流错误: {}", e);
Err(anyhow::anyhow!("SSE 流错误: {}", e))
}
}
});
Ok(Box::pin(stream))
}
fn name(&self) -> &str {
&self.default_model
}
fn supported_features(&self) -> ProviderFeatures {
ProviderFeatures {
streaming: true,
function_calling: true,
vision: false,
}
}
}

View File

@@ -0,0 +1,207 @@
//! LLM Provider trait — 统一的 LLM 调用抽象
//!
//! 支持 OpenAI 兼容 API覆盖 OpenAI / GLM / DeepSeek / Claude 兼容模式),
//! 含 function calling / tool use 能力。
use std::pin::Pin;
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>,
}
/// 聊天消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
/// 工具调用 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>>,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self { role: MessageRole::System, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn user(content: impl Into<String>) -> Self {
Self { role: MessageRole::User, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn assistant(content: impl Into<String>) -> Self {
Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: Some(tool_calls) }
}
pub fn tool_result(call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self { role: MessageRole::Tool, content: content.into(), tool_call_id: Some(call_id.into()), tool_calls: None }
}
}
/// 消息角色
#[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 {
tool_type: "function".into(),
function: ToolFunction { name: name.into(), description: description.into(), parameters },
}
}
}
/// 函数定义
#[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 {
id: id.into(),
call_type: "function".into(),
function: ToolCallFunction { name: name.into(), arguments: arguments.into() },
}
}
}
/// 工具调用函数部分
#[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>>,
}
/// Token 用量
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
/// Provider 支持的特性标志
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProviderFeatures {
pub streaming: bool,
pub function_calling: bool,
pub vision: bool,
}
/// 流式输出的 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>>,
}
/// 工具调用增量(流式中的片段)
#[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 {
/// 同步调用
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse>;
/// 流式调用(返回异步流)
async fn stream(
&self,
request: CompletionRequest,
) -> anyhow::Result<StreamResult>;
/// Provider 名称
fn name(&self) -> &str;
/// 支持的特性
fn supported_features(&self) -> ProviderFeatures;
}

View File

@@ -0,0 +1,51 @@
//! 模型路由 — 根据任务类型选择最优模型
use serde::{Deserialize, Serialize};
/// 任务类型
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskType {
/// 代码生成
CodeGeneration,
/// 代码审查
CodeReview,
/// 文档生成
Documentation,
/// 分析推理
Analysis,
/// 摘要总结
Summarization,
/// 通用对话
Chat,
}
/// 模型路由器
///
/// 根据任务类型、成本、延迟等选择最优模型
pub struct ModelRouter {
/// 默认模型
default_model: String,
}
impl ModelRouter {
/// 创建路由器
pub fn new(default_model: &str) -> Self {
Self {
default_model: default_model.to_string(),
}
}
/// 根据任务类型路由到合适的模型
pub fn route(&self, task_type: &TaskType) -> String {
// TODO: 实现基于规则的模型路由
match task_type {
TaskType::CodeGeneration => self.default_model.clone(),
TaskType::CodeReview => self.default_model.clone(),
TaskType::Documentation => self.default_model.clone(),
TaskType::Analysis => self.default_model.clone(),
TaskType::Summarization => self.default_model.clone(),
TaskType::Chat => self.default_model.clone(),
}
}
}

View File

@@ -0,0 +1,45 @@
//! 流式处理 — LLM 响应的流式输出管理
use crate::provider::StreamChunk;
/// 流式响应收集器
pub struct StreamCollector {
/// 已收集的文本
text: String,
/// 是否完成
finished: bool,
}
impl StreamCollector {
/// 创建收集器
pub fn new() -> Self {
Self {
text: String::new(),
finished: false,
}
}
/// 追加一个 chunk
pub fn push(&mut self, chunk: &StreamChunk) {
self.text.push_str(&chunk.delta);
if chunk.finished {
self.finished = true;
}
}
/// 获取已收集的文本
pub fn text(&self) -> &str {
&self.text
}
/// 是否已完成
pub fn is_finished(&self) -> bool {
self.finished
}
}
impl Default for StreamCollector {
fn default() -> Self {
Self::new()
}
}