重构: tool_registry拆分及多批改进
This commit is contained in:
@@ -131,27 +131,6 @@ impl Default for AiToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具执行结果
|
||||
// ============================================================
|
||||
|
||||
/// 工具执行结果
|
||||
#[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 参数
|
||||
// ============================================================
|
||||
|
||||
@@ -451,6 +451,107 @@ impl AnthropicCompatProvider {
|
||||
messages.push(serde_json::json!({ "role": "user", "content": blocks }));
|
||||
}
|
||||
|
||||
/// 生成 messages 诊断摘要(每条 role + content 形态 + tool 标记),不含敏感数据。
|
||||
/// B-260618-27: 1214 类错误时随 bail 文案直达前端 raw,定位哪条/字段非法。
|
||||
fn summarize_messages(messages: &[serde_json::Value]) -> String {
|
||||
let lines: Vec<String> = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let role = m.get("role").and_then(|r| r.as_str()).unwrap_or("?");
|
||||
let desc = match m.get("content") {
|
||||
Some(serde_json::Value::String(s)) => format!("text({}B)", s.len()),
|
||||
Some(serde_json::Value::Array(blocks)) => {
|
||||
let parts: Vec<String> = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let ty = b.get("type").and_then(|t| t.as_str()).unwrap_or("?");
|
||||
match ty {
|
||||
"text" => format!(
|
||||
"text({}B)",
|
||||
b.get("text").and_then(|t| t.as_str()).map(|s| s.len()).unwrap_or(0)
|
||||
),
|
||||
"tool_use" => format!(
|
||||
"tool_use[id={},input_obj={}]",
|
||||
b.get("id").and_then(|t| t.as_str()).unwrap_or(""),
|
||||
b.get("input").map(|v| v.is_object()).unwrap_or(false)
|
||||
),
|
||||
"tool_result" => format!(
|
||||
"tool_result[tid={}]",
|
||||
b.get("tool_use_id").and_then(|t| t.as_str()).unwrap_or("")
|
||||
),
|
||||
"image" => "image".to_string(),
|
||||
_ => ty.to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("[{}]", parts.join(","))
|
||||
}
|
||||
_ => "?".to_string(),
|
||||
};
|
||||
format!("#{}:{} {}", i, role, desc)
|
||||
})
|
||||
.collect();
|
||||
format!("{} msgs: {}", lines.len(), lines.join(" | "))
|
||||
}
|
||||
|
||||
/// B-260618-27: 协议预检——扫 messages 发现确定非法形态,命中返回原因(仅诊断不修复)。
|
||||
/// 覆盖:首条非 user / 连续同 role / tool_use input 非 object / 空 content / orphan tool_result
|
||||
/// (tool_use_id 无前置 tool_use,常见于裁剪/过滤后 assistant 被删但 tool_result 留)。
|
||||
fn precheck_messages(messages: &[serde_json::Value]) -> Result<(), String> {
|
||||
if messages.is_empty() {
|
||||
return Err("messages 为空".into());
|
||||
}
|
||||
let first_role = messages[0].get("role").and_then(|r| r.as_str()).unwrap_or("");
|
||||
if first_role != "user" {
|
||||
return Err(format!("首条 role={} 非法(须 user)", first_role));
|
||||
}
|
||||
for w in messages.windows(2) {
|
||||
let r0 = w[0].get("role").and_then(|r| r.as_str()).unwrap_or("");
|
||||
let r1 = w[1].get("role").and_then(|r| r.as_str()).unwrap_or("");
|
||||
if r0 == r1 && (r0 == "user" || r0 == "assistant") {
|
||||
return Err(format!("连续同 role={}", r0));
|
||||
}
|
||||
}
|
||||
let mut tool_use_ids: Vec<&str> = Vec::new();
|
||||
for (i, m) in messages.iter().enumerate() {
|
||||
let role = m.get("role").and_then(|r| r.as_str()).unwrap_or("");
|
||||
match m.get("content") {
|
||||
Some(serde_json::Value::String(s)) if s.is_empty() && role == "user" => {
|
||||
return Err(format!("#{} user content 空", i));
|
||||
}
|
||||
Some(serde_json::Value::Array(blocks)) => {
|
||||
if blocks.is_empty() && role == "user" {
|
||||
return Err(format!("#{} user content 空数组", i));
|
||||
}
|
||||
for b in blocks {
|
||||
match b.get("type").and_then(|t| t.as_str()).unwrap_or("") {
|
||||
"tool_use" => {
|
||||
let id = b.get("id").and_then(|t| t.as_str()).unwrap_or("");
|
||||
tool_use_ids.push(id);
|
||||
if !b.get("input").map(|v| v.is_object()).unwrap_or(false) {
|
||||
return Err(format!("#{} tool_use input 非 object", i));
|
||||
}
|
||||
}
|
||||
"tool_result" => {
|
||||
let tid = b.get("tool_use_id").and_then(|t| t.as_str()).unwrap_or("");
|
||||
if !tid.is_empty() && !tool_use_ids.contains(&tid) {
|
||||
return Err(format!(
|
||||
"#{} orphan tool_result(tid={} 无前置 tool_use)",
|
||||
i, tid
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 统一鉴权头:x-api-key + anthropic-version
|
||||
fn auth_headers(&self, rb: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
rb.header("x-api-key", &self.api_key)
|
||||
@@ -466,6 +567,13 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
req.stream = false;
|
||||
let body = self.convert_request(req);
|
||||
|
||||
// B-260618-27: 协议预检——命中非法 bail 含 messages 摘要,把 GLM 模糊 1214 转明确诊断
|
||||
if let Err(reason) = Self::precheck_messages(&body.messages) {
|
||||
let summary = Self::summarize_messages(&body.messages);
|
||||
warn!(%reason, %summary, "Anthropic messages 协议预检失败");
|
||||
anyhow::bail!("messages 预检失败: {} | 摘要: {}", reason, summary);
|
||||
}
|
||||
|
||||
debug!(model = %body.model, "Anthropic 同步调用");
|
||||
|
||||
// 指数退避重试(B-260616-07): 包裹 send + 状态码判定。
|
||||
@@ -504,7 +612,14 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
if is_reqwest_error_retryable(&e) {
|
||||
return AttemptOutcome::Retryable(format!("请求失败(可重试): {}", e));
|
||||
}
|
||||
return AttemptOutcome::Fatal(format!("请求失败(不可重试): {}", e));
|
||||
return AttemptOutcome::Fatal(format!(
|
||||
"请求失败(不可重试): {} [timeout={} connect={} body={} src={:?}]",
|
||||
e,
|
||||
e.is_timeout(),
|
||||
e.is_connect(),
|
||||
e.is_body(),
|
||||
std::error::Error::source(&e)
|
||||
));
|
||||
}
|
||||
};
|
||||
if !resp.status().is_success() {
|
||||
@@ -575,6 +690,13 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
req.stream = true;
|
||||
let body = self.convert_request(req);
|
||||
|
||||
// B-260618-27: 协议预检——命中非法 bail 含 messages 摘要,把 GLM 模糊 1214 转明确诊断
|
||||
if let Err(reason) = Self::precheck_messages(&body.messages) {
|
||||
let summary = Self::summarize_messages(&body.messages);
|
||||
warn!(%reason, %summary, "Anthropic messages 协议预检失败");
|
||||
anyhow::bail!("messages 预检失败: {} | 摘要: {}", reason, summary);
|
||||
}
|
||||
|
||||
debug!(model = %body.model, "Anthropic 流式调用");
|
||||
|
||||
let resp = match self
|
||||
@@ -597,7 +719,15 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
source = ?std::error::Error::source(&e),
|
||||
"Anthropic 流式 send 失败"
|
||||
);
|
||||
anyhow::bail!("Anthropic 流式 send 失败: {}", e);
|
||||
anyhow::bail!(
|
||||
"send 失败: {} [timeout={} connect={} body={} request={} src={:?}]",
|
||||
e,
|
||||
e.is_timeout(),
|
||||
e.is_connect(),
|
||||
e.is_body(),
|
||||
e.is_request(),
|
||||
std::error::Error::source(&e)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -458,24 +458,44 @@ impl ContextManager {
|
||||
/// 致 over_budget_trims_old 等测试失败)。若运行时日志显示连续 role 也是 1214 来源,再补合并。
|
||||
fn ensure_sequence_legal(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
let mut skipped = 0u32;
|
||||
let result: Vec<ChatMessage> = messages
|
||||
.into_iter()
|
||||
.skip_while(|m| {
|
||||
if matches!(m.role, MessageRole::Assistant | MessageRole::Tool) {
|
||||
skipped += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
let mut merged = 0u32;
|
||||
let mut fixed: Vec<ChatMessage> = Vec::with_capacity(messages.len());
|
||||
for m in messages {
|
||||
// 首条必须 user:skip 开头 assistant/tool(无前置 user 的孤儿)
|
||||
if fixed.is_empty() && matches!(m.role, MessageRole::Assistant | MessageRole::Tool) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
// 连续同 role 合并(user content;assistant content+tool_calls;Tool 不合并——
|
||||
// 连续 tool_result 由 anthropic_compat flush_tool_results 合并成 user blocks,此处合会丢 id)
|
||||
if let Some(last) = fixed.last_mut() {
|
||||
let same_role = std::mem::discriminant(&last.role) == std::mem::discriminant(&m.role);
|
||||
if same_role && matches!(m.role, MessageRole::User | MessageRole::Assistant) {
|
||||
if !m.content.is_empty() {
|
||||
if !last.content.is_empty() {
|
||||
last.content.push('\n');
|
||||
}
|
||||
last.content.push_str(&m.content);
|
||||
}
|
||||
if matches!(m.role, MessageRole::Assistant) {
|
||||
if let Some(calls) = m.tool_calls {
|
||||
last.tool_calls.get_or_insert_with(Vec::new).extend(calls);
|
||||
}
|
||||
}
|
||||
merged += 1;
|
||||
continue;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if skipped > 0 {
|
||||
}
|
||||
fixed.push(m);
|
||||
}
|
||||
if skipped > 0 || merged > 0 {
|
||||
tracing::warn!(
|
||||
skipped,
|
||||
"序列修复:丢弃开头的 assistant/tool 消息(Anthropic 要求首条 user,避免 1214)"
|
||||
merged,
|
||||
"序列修复:skip 开头非 user + 合并连续同 role(view-only,避免 Anthropic/GLM 1214)"
|
||||
);
|
||||
}
|
||||
result
|
||||
fixed
|
||||
}
|
||||
|
||||
/// 全量克隆(持久化 save_conversation / build_for_request 未裁剪分支,不受裁剪影响)
|
||||
@@ -952,17 +972,22 @@ mod tests {
|
||||
fn over_budget_trims_old() {
|
||||
// 小预算强制裁剪:20 条超预算,触发裁剪且保留保护区
|
||||
let mut mgr = ContextManager::new(cfg(200));
|
||||
// user/assistant 交替(真实对话序列;连续 user 会被 ensure_sequence_legal 合并,无法测条数裁剪)
|
||||
for i in 0..20 {
|
||||
mgr.push(ChatMessage::user(&format!("这是第 {} 条较长的消息用于撑爆预算", i)));
|
||||
if i % 2 == 0 {
|
||||
mgr.push(ChatMessage::user(&format!("这是第 {} 条较长的消息用于撑爆预算", i)));
|
||||
} else {
|
||||
mgr.push(ChatMessage::assistant(&format!("第 {} 条较长的回复用于撑爆预算", i)));
|
||||
}
|
||||
}
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "超预算应触发裁剪");
|
||||
assert!(msgs.len() < 20, "应裁掉部分旧消息, 实际 {}", msgs.len());
|
||||
|
||||
// 保护区:最新一条必保留
|
||||
// 保护区:最新一条必保留(末条 i=19 是 assistant)
|
||||
assert_eq!(
|
||||
msgs.last().unwrap().content,
|
||||
"这是第 19 条较长的消息用于撑爆预算",
|
||||
"第 19 条较长的回复用于撑爆预算",
|
||||
"保护区最新消息被误裁"
|
||||
);
|
||||
|
||||
|
||||
@@ -213,9 +213,9 @@ pub fn is_non_chat_model(id: &str) -> bool {
|
||||
|| id.contains("transcription")
|
||||
// 内容审查
|
||||
|| id.contains("moderation")
|
||||
// 旧 GPT 搜索变种
|
||||
// 旧 GPT 搜索变种(davinci-search 覆盖旧 GPT 搜索;裸 -search 误伤合法 search-augmented chat
|
||||
// 模型如 gpt-4o-search-preview/qwen-search/glm-4-search,已移除)
|
||||
|| id.contains("davinci-search")
|
||||
|| id.contains("-search")
|
||||
}
|
||||
|
||||
/// 过滤噪音:剔除非 chat 模型 + 去重(中转站可能返回重复) + 去空名。
|
||||
@@ -277,6 +277,17 @@ impl ModelsList {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── is_non_chat_model:search-augmented chat 模型不应误剔(裸 -search 已移除) ──
|
||||
#[test]
|
||||
fn is_non_chat_model_keeps_search_augmented_chat() {
|
||||
// search-augmented chat 模型(对话端点可调)不应被 -search 子串误剔
|
||||
assert!(!is_non_chat_model("gpt-4o-search-preview"));
|
||||
assert!(!is_non_chat_model("qwen-search"));
|
||||
assert!(!is_non_chat_model("glm-4-search-preview"));
|
||||
// 旧 GPT 搜索变种仍剔(davinci-search 覆盖)
|
||||
assert!(is_non_chat_model("text-davinci-search-001"));
|
||||
}
|
||||
|
||||
// ── build_models_url:尾 / 处理 ──
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user