//! Thinking Signature 整流器 //! //! 用于自动修复 Anthropic API 中因签名校验失败导致的请求错误。 //! 当上游 API 返回签名相关错误时,系统会自动移除有问题的签名字段并重试请求。 use super::types::RectifierConfig; use serde_json::Value; /// 整流结果 #[derive(Debug, Clone, Default)] pub struct RectifyResult { /// 是否应用了整流 pub applied: bool, /// 移除的 thinking block 数量 pub removed_thinking_blocks: usize, /// 移除的 redacted_thinking block 数量 pub removed_redacted_thinking_blocks: usize, /// 移除的 signature 字段数量 pub removed_signature_fields: usize, } /// 检测是否需要触发 thinking 签名整流器 /// /// 返回 `true` 表示需要触发整流器,`false` 表示不需要。 /// 会检查配置开关。 pub fn should_rectify_thinking_signature( error_message: Option<&str>, config: &RectifierConfig, ) -> bool { // 检查总开关 if !config.enabled { return false; } // 检查子开关 if !config.request_thinking_signature { return false; } // 检测错误类型 let Some(msg) = error_message else { return false; }; let lower = msg.to_lowercase(); // 场景1: thinking block 中的签名无效 // 错误示例: "Invalid 'signature' in 'thinking' block" if lower.contains("invalid") && lower.contains("signature") && lower.contains("thinking") && lower.contains("block") { return true; } // 场景2: assistant 消息必须以 thinking block 开头 // 错误示例: "must start with a thinking block" if lower.contains("must start with a thinking block") { return true; } // 场景3: expected thinking or redacted_thinking, found tool_use // 错误示例: "Expected `thinking` or `redacted_thinking`, but found `tool_use`" if lower.contains("expected") && (lower.contains("thinking") || lower.contains("redacted_thinking")) && lower.contains("found") { return true; } // 场景4: signature 字段必需但缺失 // 错误示例: "signature: Field required" if lower.contains("signature") && lower.contains("field required") { return true; } false } /// 对 Anthropic 请求体做最小侵入整流 /// /// - 移除 messages[*].content 中的 thinking/redacted_thinking block /// - 移除非 thinking block 上遗留的 signature 字段 /// - 特定条件下删除顶层 thinking 字段 /// /// 注意:该函数会原地修改 body 对象 pub fn rectify_anthropic_request(body: &mut Value) -> RectifyResult { let mut result = RectifyResult::default(); let messages = match body.get_mut("messages").and_then(|m| m.as_array_mut()) { Some(m) => m, None => return result, }; // 遍历所有消息 for msg in messages.iter_mut() { let content = match msg.get_mut("content").and_then(|c| c.as_array_mut()) { Some(c) => c, None => continue, }; let mut new_content = Vec::with_capacity(content.len()); let mut content_modified = false; for block in content.iter() { let block_type = block.get("type").and_then(|t| t.as_str()); match block_type { Some("thinking") => { result.removed_thinking_blocks += 1; content_modified = true; continue; } Some("redacted_thinking") => { result.removed_redacted_thinking_blocks += 1; content_modified = true; continue; } _ => {} } // 移除非 thinking block 上的 signature 字段 if block.get("signature").is_some() { let mut block_clone = block.clone(); if let Some(obj) = block_clone.as_object_mut() { obj.remove("signature"); result.removed_signature_fields += 1; content_modified = true; new_content.push(Value::Object(obj.clone())); continue; } } new_content.push(block.clone()); } if content_modified { result.applied = true; *content = new_content; } } // 兜底处理:thinking 启用 + 工具调用链路中最后一条 assistant 消息未以 thinking 开头 let messages_snapshot: Vec = body .get("messages") .and_then(|m| m.as_array()) .map(|a| a.to_vec()) .unwrap_or_default(); if should_remove_top_level_thinking(body, &messages_snapshot) { if let Some(obj) = body.as_object_mut() { obj.remove("thinking"); result.applied = true; } } result } /// 判断是否需要删除顶层 thinking 字段 fn should_remove_top_level_thinking(body: &Value, messages: &[Value]) -> bool { // 检查 thinking 是否启用 let thinking_enabled = body .get("thinking") .and_then(|t| t.get("type")) .and_then(|t| t.as_str()) == Some("enabled"); if !thinking_enabled { return false; } // 找到最后一条 assistant 消息 let last_assistant = messages .iter() .rev() .find(|m| m.get("role").and_then(|r| r.as_str()) == Some("assistant")); let last_assistant_content = match last_assistant .and_then(|m| m.get("content")) .and_then(|c| c.as_array()) { Some(c) if !c.is_empty() => c, _ => return false, }; // 检查首块是否为 thinking/redacted_thinking let first_block_type = last_assistant_content .first() .and_then(|b| b.get("type")) .and_then(|t| t.as_str()); let missing_thinking_prefix = first_block_type != Some("thinking") && first_block_type != Some("redacted_thinking"); if !missing_thinking_prefix { return false; } // 检查是否存在 tool_use last_assistant_content .iter() .any(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_use")) } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn enabled_config() -> RectifierConfig { RectifierConfig { enabled: true, request_thinking_signature: true, } } fn disabled_config() -> RectifierConfig { RectifierConfig { enabled: true, request_thinking_signature: false, } } fn master_disabled_config() -> RectifierConfig { RectifierConfig { enabled: false, request_thinking_signature: true, } } // ==================== should_rectify_thinking_signature 测试 ==================== #[test] fn test_detect_invalid_signature() { assert!(should_rectify_thinking_signature( Some("messages.1.content.0: Invalid `signature` in `thinking` block"), &enabled_config() )); } #[test] fn test_detect_invalid_signature_no_backticks() { assert!(should_rectify_thinking_signature( Some("Messages.1.Content.0: invalid signature in thinking block"), &enabled_config() )); } #[test] fn test_detect_invalid_signature_nested_json() { // 测试嵌套 JSON 格式的错误消息(第三方渠道常见格式) let nested_error = r#"{"error":{"message":"{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"***.content.0: Invalid `signature` in `thinking` block\"},\"request_id\":\"req_xxx\"}"}}"#; assert!(should_rectify_thinking_signature( Some(nested_error), &enabled_config() )); } #[test] fn test_detect_thinking_expected() { assert!(should_rectify_thinking_signature( Some("messages.69.content.0.type: Expected `thinking` or `redacted_thinking`, but found `tool_use`."), &enabled_config() )); } #[test] fn test_detect_must_start_with_thinking() { assert!(should_rectify_thinking_signature( Some("a final `assistant` message must start with a thinking block"), &enabled_config() )); } #[test] fn test_no_trigger_for_unrelated_error() { assert!(!should_rectify_thinking_signature( Some("Request timeout"), &enabled_config() )); assert!(!should_rectify_thinking_signature( Some("Connection refused"), &enabled_config() )); assert!(!should_rectify_thinking_signature(None, &enabled_config())); } #[test] fn test_detect_signature_field_required() { // 场景4: signature 字段缺失 assert!(should_rectify_thinking_signature( Some("***.***.***.***.***.signature: Field required"), &enabled_config() )); // 嵌套 JSON 格式 let nested_error = r#"{"error":{"type":"","message":"{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"***.***.***.***.***.signature: Field required\"},\"request_id\":\"req_xxx\"}"}}"#; assert!(should_rectify_thinking_signature( Some(nested_error), &enabled_config() )); } #[test] fn test_disabled_config() { // 即使错误匹配,配置关闭时也不触发 assert!(!should_rectify_thinking_signature( Some("Invalid `signature` in `thinking` block"), &disabled_config() )); } #[test] fn test_master_disabled() { // 总开关关闭时,即使子开关开启也不触发 assert!(!should_rectify_thinking_signature( Some("Invalid `signature` in `thinking` block"), &master_disabled_config() )); } // ==================== rectify_anthropic_request 测试 ==================== #[test] fn test_rectify_removes_thinking_blocks() { let mut body = json!({ "model": "claude-test", "messages": [{ "role": "assistant", "content": [ { "type": "thinking", "thinking": "t", "signature": "sig" }, { "type": "text", "text": "hello", "signature": "sig_text" }, { "type": "tool_use", "id": "toolu_1", "name": "WebSearch", "input": {}, "signature": "sig_tool" }, { "type": "redacted_thinking", "data": "r", "signature": "sig_redacted" } ] }] }); let result = rectify_anthropic_request(&mut body); assert!(result.applied); assert_eq!(result.removed_thinking_blocks, 1); assert_eq!(result.removed_redacted_thinking_blocks, 1); assert_eq!(result.removed_signature_fields, 2); let content = body["messages"][0]["content"].as_array().unwrap(); assert_eq!(content.len(), 2); assert_eq!(content[0]["type"], "text"); assert!(content[0].get("signature").is_none()); assert_eq!(content[1]["type"], "tool_use"); assert!(content[1].get("signature").is_none()); } #[test] fn test_rectify_removes_top_level_thinking() { let mut body = json!({ "model": "claude-test", "thinking": { "type": "enabled", "budget_tokens": 1024 }, "messages": [{ "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_1", "name": "WebSearch", "input": {} } ] }, { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "toolu_1", "content": "ok" }] }] }); let result = rectify_anthropic_request(&mut body); assert!(result.applied); assert!(body.get("thinking").is_none()); } #[test] fn test_rectify_no_change_when_no_issues() { let mut body = json!({ "model": "claude-test", "messages": [{ "role": "user", "content": [{ "type": "text", "text": "hello" }] }] }); let result = rectify_anthropic_request(&mut body); assert!(!result.applied); assert_eq!(result.removed_thinking_blocks, 0); } #[test] fn test_rectify_no_messages() { let mut body = json!({ "model": "claude-test" }); let result = rectify_anthropic_request(&mut body); assert!(!result.applied); } #[test] fn test_rectify_preserves_thinking_when_prefix_exists() { let mut body = json!({ "model": "claude-test", "thinking": { "type": "enabled" }, "messages": [{ "role": "assistant", "content": [ { "type": "thinking", "thinking": "some thought" }, { "type": "tool_use", "id": "toolu_1", "name": "Test", "input": {} } ] }] }); let result = rectify_anthropic_request(&mut body); // thinking block 被移除,但顶层 thinking 不应被移除(因为原本有 thinking 前缀) assert!(result.applied); assert_eq!(result.removed_thinking_blocks, 1); // 注意:由于 thinking block 被移除后,首块变成了 tool_use, // 此时会触发删除顶层 thinking 的逻辑 // 这是预期行为:整流后如果仍然不符合要求,就删除顶层 thinking } }