优化: AI模块(provider重试兼容+aichat交互+工具卡片可读化+diff高亮)
This commit is contained in:
@@ -11,19 +11,23 @@ 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,
|
||||
};
|
||||
use crate::retry::{
|
||||
retry_with_backoff, AttemptOutcome, is_reqwest_error_retryable, is_status_retryable,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Anthropic API 请求/响应结构体
|
||||
// ============================================================
|
||||
|
||||
/// Anthropic 请求体
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct AnthropicRequest {
|
||||
model: String,
|
||||
messages: Vec<serde_json::Value>,
|
||||
@@ -40,7 +44,7 @@ struct AnthropicRequest {
|
||||
}
|
||||
|
||||
/// Anthropic 工具定义(input_schema 对应 OpenAI 的 parameters)
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct AnthropicToolDef {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -406,68 +410,93 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
|
||||
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);
|
||||
// 指数退避重试(B-260616-07): 包裹 send + 状态码判定。
|
||||
// 同时补 FR-R4 遗漏: Anthropic 同步路径此前无单请求 timeout(建连后挂起会无限 hang),
|
||||
// 此处加 60s timeout,与 OpenAI 路径对齐。
|
||||
let label = format!("Anthropic[{}]", body.model);
|
||||
retry_with_backoff(&label, move |_| {
|
||||
let client = self.client.clone();
|
||||
let url = self.messages_url();
|
||||
let api_key = self.api_key.clone();
|
||||
let body = body.clone();
|
||||
async move {
|
||||
// send: 补 60s 单请求 timeout(此前缺失,FR-R4 遗漏)
|
||||
let rb = client
|
||||
.post(url)
|
||||
.header("x-api-key", &api_key)
|
||||
.header("anthropic-version", ANTHROPIC_VERSION)
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration::from_secs(60))
|
||||
.json(&body);
|
||||
let resp = match rb.send().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
if is_reqwest_error_retryable(&e) {
|
||||
return AttemptOutcome::Retryable(format!("请求失败(可重试): {}", e));
|
||||
}
|
||||
return AttemptOutcome::Fatal(format!("请求失败(不可重试): {}", e));
|
||||
}
|
||||
};
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status().as_u16();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
let msg = format!("Anthropic API 错误 {}: {}", status, text);
|
||||
if is_status_retryable(status) {
|
||||
warn!(%status, "Anthropic 同步调用可重试状态码");
|
||||
return AttemptOutcome::Retryable(msg);
|
||||
}
|
||||
error!(%status, %text, "Anthropic API 调用失败(不可重试)");
|
||||
return AttemptOutcome::Fatal(msg);
|
||||
}
|
||||
let resp: AnthropicResponse = match resp.json().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return AttemptOutcome::Fatal(format!("响应解析失败: {}", e)),
|
||||
};
|
||||
// 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 = match block.id {
|
||||
Some(id) if !id.is_empty() => id,
|
||||
_ => {
|
||||
warn!(
|
||||
name = ?block.name,
|
||||
"Anthropic tool_use 块缺少 id,已跳过(空 id 会回传空 tool_use_id 触发 500)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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 块类型,已忽略"),
|
||||
}
|
||||
}
|
||||
"tool_use" => {
|
||||
// tool_use 必须带 id:后续 tool_result 用它回引,缺 id 的 tool_call
|
||||
// 会拖出空 tool_use_id 的 tool_result,触发 GLM 500 卡死会话。
|
||||
// id 缺失/为空时跳过该 tool_use 解析并告警。
|
||||
let id = match block.id {
|
||||
Some(id) if !id.is_empty() => id,
|
||||
_ => {
|
||||
warn!(
|
||||
name = ?block.name,
|
||||
"Anthropic tool_use 块缺少 id,已跳过(空 id 会回传空 tool_use_id 触发 500)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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,
|
||||
};
|
||||
AttemptOutcome::Ok(CompletionResponse {
|
||||
text,
|
||||
model: resp.model,
|
||||
usage,
|
||||
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) },
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream(&self, request: CompletionRequest) -> anyhow::Result<StreamResult> {
|
||||
|
||||
Reference in New Issue
Block a user