//! 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), } /// Anthropic 请求体 #[derive(Debug, Clone, Serialize)] pub(crate) struct AnthropicRequest { pub model: String, pub messages: Vec, pub max_tokens: u32, #[serde(skip_serializing_if = "Option::is_none")] pub system: Option, #[serde(skip_serializing_if = "Option::is_none")] pub temperature: Option, pub stream: bool, #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_choice: Option, } /// 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, 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, // Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning #[allow(dead_code)] stop_reason: Option, 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, /// tool_use 块字段 pub id: Option, pub name: Option, pub input: Option, } #[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) -> 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 }, } }