新增: 初始化 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:
426
crates/df-ai/src/anthropic_compat.rs
Normal file
426
crates/df-ai/src/anthropic_compat.rs
Normal 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_result(role=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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user