264 lines
13 KiB
Rust
264 lines
13 KiB
Rust
//! 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::{tool_call_id_or_fallback, StreamChunk, TokenUsage, ToolCallDelta};
|
||
|
||
// ============================================================
|
||
// Anthropic API 请求/响应结构体
|
||
// ============================================================
|
||
|
||
/// Anthropic `system` 字段形态(受 prompt caching 开关 `ANTHROPIC_CACHE_ENABLED` 控制):
|
||
/// - `Plain`:纯字符串。开关关时保持原形态,兼容非官方网关(GLM 订阅端点 / 任意 Messages API
|
||
/// 代理——部分网关会拒收 cache_control 字段)。
|
||
/// - `Cached`:blocks 数组 `[{type:text, text, cache_control:{type:ephemeral}}]`。开关开时启用
|
||
/// Anthropic prompt caching,让 system 稳定段在支持缓存的端点上命中。
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(untagged)]
|
||
pub(crate) enum SystemBlock {
|
||
/// 纯字符串形态(兼容网关,开关关)
|
||
Plain(String),
|
||
/// blocks 数组形态(含 cache_control,开关开)
|
||
Cached(Vec<serde_json::Value>),
|
||
}
|
||
|
||
/// 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<SystemBlock>,
|
||
#[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 {
|
||
// Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
|
||
#[allow(dead_code)]
|
||
id: String,
|
||
pub model: String,
|
||
pub content: Vec<AnthropicContentBlock>,
|
||
// 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 prompt caching 扩展:cache 写入 token(本次写入缓存,计费如输入但稍便宜)。
|
||
/// 映射到 TokenUsage.prompt_cache_miss_tokens(全价输入语义)。
|
||
/// 非 cache 场景无此字段 → serde default 0。
|
||
#[serde(default)]
|
||
pub cache_creation_input_tokens: u32,
|
||
/// Anthropic prompt caching 扩展:cache 命中读取 token(低价)。
|
||
/// 映射到 TokenUsage.prompt_cache_hit_tokens。
|
||
/// 非 cache 场景无此字段 → serde default 0。
|
||
#[serde(default)]
|
||
pub cache_read_input_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)。
|
||
// anthropic prompt caching:cache_creation/read 在 message_start.usage 携带。
|
||
"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())
|
||
{
|
||
let u_obj = v.get("message").and_then(|m| m.get("usage"));
|
||
let cache_read = u_obj
|
||
.and_then(|u| u.get("cache_read_input_tokens"))
|
||
.and_then(|t| t.as_u64())
|
||
.unwrap_or(0) as u32;
|
||
let cache_creation = u_obj
|
||
.and_then(|u| u.get("cache_creation_input_tokens"))
|
||
.and_then(|t| t.as_u64())
|
||
.unwrap_or(0) as u32;
|
||
*usage_accum = Some(TokenUsage {
|
||
prompt_tokens: inp as u32,
|
||
completion_tokens: 0,
|
||
total_tokens: inp as u32,
|
||
prompt_cache_hit_tokens: cache_read,
|
||
prompt_cache_miss_tokens: cache_creation,
|
||
reasoning_tokens: 0,
|
||
});
|
||
}
|
||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: usage_accum.clone(), 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::default());
|
||
acc.completion_tokens = out as u32;
|
||
acc.total_tokens = acc.prompt_tokens.saturating_add(acc.completion_tokens);
|
||
}
|
||
// 加固:usage 挂到本帧(而非仅 message_stop 带出),中途断连/端点不发 message_stop
|
||
// 时仍能拿到真实 usage,对称 openai_helpers 的修复。
|
||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: usage_accum.clone(), 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());
|
||
// CR-空 id:id 缺失/空时用 fallback 兜底(流式后续 input_json_delta 按 index 累加,
|
||
// 中途无法整体跳过)。与同步路径 + OpenAI 路径共用 tool_call_id_or_fallback(DRY),
|
||
// prefix=`gen_anthropic_stream` 区分来源。非空原样。
|
||
let raw_id = cb.get("id").and_then(|t| t.as_str()).unwrap_or("");
|
||
let id = if raw_id.is_empty() {
|
||
let fallback = tool_call_id_or_fallback(raw_id, idx as usize, "gen_anthropic_stream");
|
||
warn!(%fallback, name = ?name, "Anthropic 流式 tool_use 块 id 为空,已生成 fallback id(原样回传会触发 GLM 500)");
|
||
Some(fallback)
|
||
} else {
|
||
Some(raw_id.to_string())
|
||
};
|
||
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 },
|
||
}
|
||
}
|