重构: audit第三批cache + anthropic_compat拆分(strategy并行)

- audit 第三批: 新建 audit/cache.rs(151行 find_cached_high_risk_result/canonical_args_key/sort_object_keys/PENDING_APPROVAL_PLACEHOLDER), audit/mod.rs 586→451(-135); pub(crate) re-export, F-09 per_conv保留
- anthropic_compat 拆分: 新建 df-ai/anthropic_helpers.rs(223行 apply_anthropic_event + 5 struct + 2常量), anthropic_compat.rs 1036→842; Provider impl保留(impl块约束), 字段pub(crate)
主代兜底: cargo check --workspace 0 + test df-ai 119 + test devflow 98
strategy: 核心库自底向上, 单批1-2文件原子, impl块约束(Provider方法保留只抽纯函数/类型)
git add指定(audit/* + df-ai/*, 不含其他会话)
This commit is contained in:
2026-06-19 04:31:19 +08:00
parent ed35c51a47
commit 55bc1e1b23
5 changed files with 391 additions and 345 deletions

View File

@@ -5,18 +5,20 @@
//!
//! 与 OpenAI 协议的关键差异由本模块内部完成转换,对外仍暴露统一的 LlmProvider trait
//! 上层 (Agentic Loop / AiNode) 无需感知协议。
//!
//! 协议数据结构(请求/响应 struct与 SSE 事件解析纯函数已抽至 `anthropic_helpers`
//! 本模块仅保留 Provider struct + implHTTP 调用Rust impl 块不可跨文件故作此切分。
use async_trait::async_trait;
use eventsource_stream::Eventsource;
use futures::StreamExt;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{debug, error, warn};
use crate::provider::{
CompletionRequest, CompletionResponse, LlmProvider, MessageRole,
StreamChunk, StreamResult, TokenUsage, ToolCall, ToolCallDelta,
StreamResult, TokenUsage, ToolCall,
};
// ChatMessage 仅单测构造 CompletionRequest 用,避免非 test 构建的 unused import 警告。
#[cfg(test)]
@@ -25,204 +27,12 @@ use crate::retry::{
retry_with_backoff, AttemptOutcome, is_reqwest_error_retryable, is_status_retryable,
};
// ============================================================
// Anthropic API 请求/响应结构体
// ============================================================
/// Anthropic 请求体
#[derive(Debug, Clone, 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, Clone, 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 {
// SW-260618-25: Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
#[allow(dead_code)]
id: String,
model: String,
content: Vec<AnthropicContentBlock>,
// SW-260618-25: Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
#[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,
}
// ============================================================
// SSE 解析纯函数(与 HTTP 解耦,便于单测)
// ============================================================
/// 将一条 Anthropic Messages SSE 事件 data 解析为 StreamChunk并按需更新 usage 累加器。
///
/// 按 `type` 字段分发:
/// - `message_start` → 用 `message.usage.input_tokens` 初始化累加器output 置 0
/// - `message_delta` → **output_tokens 是累计值(非增量)**,直接覆盖 `completion_tokens` 并重算 `total`。
/// - `content_block_delta` (text_delta/input_json_delta) → 文本/工具入参增量。
/// - `content_block_start` (tool_use) → 工具块开始,带 id+name。
/// - `message_stop` → 返回 `finished=true` 终态 chunk`usage` 取自累加器(`take()`)。
/// - `error` → 返回 `finished=true` 终态空 chunk。
/// - 其它content_block_stop / ping 等)→ 空 chunk。
///
/// 等价性content_block / message_stop / error 等事件分支与原 stream() 闭包逐字一致;
/// usage 透传message_stop 终态 take() 带出、message_delta 的 output_tokens 按累计值覆盖)
/// 为本次新增能力,对应 StreamChunk 新增的 usage 字段。
pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk {
// 解析 data 中的 JSON按 type 字段决定如何转 StreamChunk
let v: serde_json::Value = match serde_json::from_str(data) {
Ok(v) => v,
Err(_) => {
return StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
};
let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
match ty {
// 消息开始:取 input_tokens 初始化累积器output 此时未知,置 0
"message_start" => {
if let Some(inp) = v
.get("message")
.and_then(|m| m.get("usage"))
.and_then(|u| u.get("input_tokens"))
.and_then(|t| t.as_u64())
{
*usage_accum = Some(TokenUsage {
prompt_tokens: inp as u32,
completion_tokens: 0,
total_tokens: inp as u32,
});
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 消息增量output_tokens 是累计值(非增量),直接覆盖 completion + 重算 total
"message_delta" => {
if let Some(out) = v.get("usage").and_then(|u| u.get("output_tokens")).and_then(|t| t.as_u64()) {
let acc = usage_accum
.get_or_insert(TokenUsage { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
acc.completion_tokens = out as u32;
acc.total_tokens = acc.prompt_tokens + acc.completion_tokens;
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 文本增量
"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 StreamChunk { delta: text, finished: false, tool_calls: None, usage: None, error: None, reasoning_content: 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 StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id: None,
function_name: None,
function_arguments: Some(partial),
}]),
usage: None,
error: None,
reasoning_content: None,
};
}
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: 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 name = cb.get("name").and_then(|t| t.as_str()).map(|s| s.to_string());
// id 缺失时用占位 id 兜底:流式后续 input_json_delta 按 index 累加,
// 中途无法整体跳过;占位 id 保证回传的 tool_use_id 非空,避免 GLM 500。
let id = match cb.get("id").and_then(|t| t.as_str()).map(|s| s.to_string()) {
Some(id) if !id.is_empty() => Some(id),
_ => {
let placeholder = format!("tool_missing_{}", idx);
warn!(%placeholder, name = ?name, "Anthropic 流式 tool_use 块缺少 id已填占位 id原样回传会触发 GLM 500");
Some(placeholder)
}
};
return StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id,
function_name: name,
function_arguments: None,
}]),
usage: None,
error: None,
reasoning_content: None,
};
}
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 消息结束:带出累积 usage
"message_stop" => StreamChunk {
delta: String::new(),
finished: true,
tool_calls: None,
usage: usage_accum.take(),
error: None,
reasoning_content: None,
},
// 错误事件:流中途出错。不走 finished 完成路径(避免残缺响应被当正常完成入库),
// 改由 stream_llm 识别 error 非空 → 发 AiError + 丢弃残缺(与 OpenAI 路径 Err 一致)。
"error" => {
let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error").to_string();
error!(%msg, "Anthropic 流式错误事件");
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: Some(msg), reasoning_content: None }
}
// content_block_stop / ping 等不产出 chunk
_ => StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None },
}
}
// 协议数据结构 + SSE 纯解析apply_anthropic_event / AnthropicRequest 等)抽至 anthropic_helpers
// 此处 use 引入以保持本模块内引用路径不变(零行为变更搬迁)。
use crate::anthropic_helpers::{
apply_anthropic_event, AnthropicRequest, AnthropicResponse, AnthropicToolDef,
ANTHROPIC_VERSION, DEFAULT_MAX_TOKENS,
};
// ============================================================
// Provider 实现
@@ -236,11 +46,6 @@ pub struct AnthropicCompatProvider {
default_model: String,
}
/// Anthropic 流式协议版本头
const ANTHROPIC_VERSION: &str = "2023-06-01";
/// Anthropic max_tokens 必填,缺省时的兜底值
const DEFAULT_MAX_TOKENS: u32 = 4096;
impl AnthropicCompatProvider {
/// 创建 Provider
///
@@ -788,6 +593,7 @@ impl LlmProvider for AnthropicCompatProvider {
#[cfg(test)]
mod tests {
use super::*;
// apply_anthropic_event / TokenUsage 经 super::*(含 anthropic_helpers::apply_anthropic_event 的 use可见。
/// 辅助:构造 message_start 事件
fn message_start(input_tokens: u32) -> String {

View File

@@ -0,0 +1,223 @@
//! Anthropic 协议适配 — 协议数据结构与 SSE 事件解析(纯函数)。
//!
//! 本模块从 `anthropic_compat.rs` 抽离,承载与 HTTP 无关的纯协议逻辑:
//! - 请求/响应结构体(`AnthropicRequest` 等)
//! - 协议常量(`ANTHROPIC_VERSION` / `DEFAULT_MAX_TOKENS`
//! - SSE 事件 → `StreamChunk` 转换纯函数(`apply_anthropic_event`
//!
//! Provider struct + impl含 HTTP 调用)仍留在 `anthropic_compat.rs`
//! Rust impl 块不可跨文件,故仅搬迁 impl 块外部的类型/常量/纯函数。
//! 零行为变更(纯搬迁)。
use serde::{Deserialize, Serialize};
use tracing::{error, warn};
use crate::provider::{StreamChunk, TokenUsage, ToolCallDelta};
// ============================================================
// Anthropic API 请求/响应结构体
// ============================================================
/// Anthropic 请求体
#[derive(Debug, Clone, Serialize)]
pub(crate) struct AnthropicRequest {
pub model: String,
pub messages: Vec<serde_json::Value>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AnthropicToolDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
}
/// Anthropic 工具定义input_schema 对应 OpenAI 的 parameters
#[derive(Debug, Clone, Serialize)]
pub(crate) struct AnthropicToolDef {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
/// Anthropic 同步响应
#[derive(Debug, Deserialize)]
pub(crate) struct AnthropicResponse {
// SW-260618-25: Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
#[allow(dead_code)]
id: String,
pub model: String,
pub content: Vec<AnthropicContentBlock>,
// SW-260618-25: Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
#[allow(dead_code)]
stop_reason: Option<String>,
pub usage: AnthropicUsage,
}
/// 响应 content 块text 或 tool_use
#[derive(Debug, Deserialize)]
pub(crate) struct AnthropicContentBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(default)]
pub text: Option<String>,
/// tool_use 块字段
pub id: Option<String>,
pub name: Option<String>,
pub input: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AnthropicUsage {
pub input_tokens: u32,
pub output_tokens: u32,
}
// ============================================================
// 协议常量
// ============================================================
/// Anthropic 流式协议版本头
pub(crate) const ANTHROPIC_VERSION: &str = "2023-06-01";
/// Anthropic max_tokens 必填,缺省时的兜底值
pub(crate) const DEFAULT_MAX_TOKENS: u32 = 4096;
// ============================================================
// SSE 解析纯函数(与 HTTP 解耦,便于单测)
// ============================================================
/// 将一条 Anthropic Messages SSE 事件 data 解析为 StreamChunk并按需更新 usage 累加器。
///
/// 按 `type` 字段分发:
/// - `message_start` → 用 `message.usage.input_tokens` 初始化累加器output 置 0
/// - `message_delta` → **output_tokens 是累计值(非增量)**,直接覆盖 `completion_tokens` 并重算 `total`。
/// - `content_block_delta` (text_delta/input_json_delta) → 文本/工具入参增量。
/// - `content_block_start` (tool_use) → 工具块开始,带 id+name。
/// - `message_stop` → 返回 `finished=true` 终态 chunk`usage` 取自累加器(`take()`)。
/// - `error` → 返回 `finished=true` 终态空 chunk。
/// - 其它content_block_stop / ping 等)→ 空 chunk。
///
/// 等价性content_block / message_stop / error 等事件分支与原 stream() 闭包逐字一致;
/// usage 透传message_stop 终态 take() 带出、message_delta 的 output_tokens 按累计值覆盖)
/// 为本次新增能力,对应 StreamChunk 新增的 usage 字段。
pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk {
// 解析 data 中的 JSON按 type 字段决定如何转 StreamChunk
let v: serde_json::Value = match serde_json::from_str(data) {
Ok(v) => v,
Err(_) => {
return StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
};
let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
match ty {
// 消息开始:取 input_tokens 初始化累积器output 此时未知,置 0
"message_start" => {
if let Some(inp) = v
.get("message")
.and_then(|m| m.get("usage"))
.and_then(|u| u.get("input_tokens"))
.and_then(|t| t.as_u64())
{
*usage_accum = Some(TokenUsage {
prompt_tokens: inp as u32,
completion_tokens: 0,
total_tokens: inp as u32,
});
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 消息增量output_tokens 是累计值(非增量),直接覆盖 completion + 重算 total
"message_delta" => {
if let Some(out) = v.get("usage").and_then(|u| u.get("output_tokens")).and_then(|t| t.as_u64()) {
let acc = usage_accum
.get_or_insert(TokenUsage { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
acc.completion_tokens = out as u32;
acc.total_tokens = acc.prompt_tokens + acc.completion_tokens;
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 文本增量
"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 StreamChunk { delta: text, finished: false, tool_calls: None, usage: None, error: None, reasoning_content: 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 StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id: None,
function_name: None,
function_arguments: Some(partial),
}]),
usage: None,
error: None,
reasoning_content: None,
};
}
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: 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 name = cb.get("name").and_then(|t| t.as_str()).map(|s| s.to_string());
// id 缺失时用占位 id 兜底:流式后续 input_json_delta 按 index 累加,
// 中途无法整体跳过;占位 id 保证回传的 tool_use_id 非空,避免 GLM 500。
let id = match cb.get("id").and_then(|t| t.as_str()).map(|s| s.to_string()) {
Some(id) if !id.is_empty() => Some(id),
_ => {
let placeholder = format!("tool_missing_{}", idx);
warn!(%placeholder, name = ?name, "Anthropic 流式 tool_use 块缺少 id已填占位 id原样回传会触发 GLM 500");
Some(placeholder)
}
};
return StreamChunk {
delta: String::new(),
finished: false,
tool_calls: Some(vec![ToolCallDelta {
index: idx,
id,
function_name: name,
function_arguments: None,
}]),
usage: None,
error: None,
reasoning_content: None,
};
}
}
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
}
// 消息结束:带出累积 usage
"message_stop" => StreamChunk {
delta: String::new(),
finished: true,
tool_calls: None,
usage: usage_accum.take(),
error: None,
reasoning_content: None,
},
// 错误事件:流中途出错。不走 finished 完成路径(避免残缺响应被当正常完成入库),
// 改由 stream_llm 识别 error 非空 → 发 AiError + 丢弃残缺(与 OpenAI 路径 Err 一致)。
"error" => {
let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error").to_string();
error!(%msg, "Anthropic 流式错误事件");
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: Some(msg), reasoning_content: None }
}
// content_block_stop / ping 等不产出 chunk
_ => StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None },
}
}

View File

@@ -2,6 +2,7 @@
pub mod ai_tools;
pub mod anthropic_compat;
pub mod anthropic_helpers;
pub mod context;
pub mod context_helpers;
pub mod coordinator;