新增: anthropic 思维链解析入库与思考卡展示
- 流式与非流式 thinking 块接入 reasoning 通道 - 签名与密文块合规静默,请求侧零改动 - 历史映射透传 reasoning_content - 思考卡数据源真推理文本优先
This commit is contained in:
@@ -636,8 +636,9 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
return AttemptOutcome::Fatal(format!("响应解析失败: {}", e));
|
||||
}
|
||||
};
|
||||
// content 块中拼接 text,收集 tool_use
|
||||
// content 块中拼接 text,收集 tool_use,累积 thinking
|
||||
let mut text = String::new();
|
||||
let mut reasoning = String::new();
|
||||
let mut tool_calls: Vec<ToolCall> = Vec::new();
|
||||
// CR-空 id:按 tool_use 块在数组中的顺序计数(仅 tool_use 递增),用于 fallback index。
|
||||
// 用独立计数器而非 for enumerate,避免 text/unknown 块占用 index 致 fallback 编号跳号。
|
||||
@@ -649,6 +650,15 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
"thinking" => {
|
||||
// thinking 块正文回填 reasoning_content 通道(与 DeepSeek 同路入库展示)。
|
||||
// 仅入库与展示,绝不回传 anthropic 历史(丢弃 signature 的伪造回传必 400)。
|
||||
if let Some(t) = block.thinking {
|
||||
reasoning.push_str(&t);
|
||||
}
|
||||
}
|
||||
// redacted_thinking 为端点侧密文块,不可解析也不可伪造回传,合规丢弃不告警
|
||||
"redacted_thinking" => {}
|
||||
"tool_use" => {
|
||||
// CR-空 id:原逻辑空 id 直接 continue 跳过整个块(丢工具调用)。
|
||||
// 改为兜底:id 非空原样,空 → `gen_anthropic_{idx}` fallback(DRY 共用
|
||||
@@ -688,7 +698,8 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
model: resp.model,
|
||||
usage,
|
||||
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
|
||||
reasoning_content: None,
|
||||
// 无 thinking 块时保持 None(语义与 DeepSeek 路径一致,空串不落库)
|
||||
reasoning_content: if reasoning.is_empty() { None } else { Some(reasoning) },
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -963,6 +974,31 @@ mod tests {
|
||||
assert!(!c.finished);
|
||||
}
|
||||
|
||||
/// thinking_delta → reasoning_content 增量(不混入正文 delta);signature_delta 安静忽略。
|
||||
/// 推理正文走 reasoning_content 通道与 DeepSeek 同路入库展示。
|
||||
#[test]
|
||||
fn anthropic_thinking_delta_accumulates_reasoning() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
let think1 = r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"先查"}}"#;
|
||||
let c1 = apply_anthropic_event(think1, &mut acc);
|
||||
assert_eq!(c1.reasoning_content.as_deref(), Some("先查"), "thinking_delta 应产出 reasoning_content");
|
||||
assert_eq!(c1.delta, "", "思考增量不得混入正文 delta");
|
||||
assert!(!c1.finished);
|
||||
let think2 = r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"用户意图"}}"#;
|
||||
let c2 = apply_anthropic_event(think2, &mut acc);
|
||||
assert_eq!(c2.reasoning_content.as_deref(), Some("用户意图"));
|
||||
// signature_delta:thinking 块签名字段,本侧无处消费,安静跳过(空 chunk 不告警)
|
||||
let sig = r#"{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig-abc"}}"#;
|
||||
let c3 = apply_anthropic_event(sig, &mut acc);
|
||||
assert!(c3.reasoning_content.is_none());
|
||||
assert!(c3.delta.is_empty());
|
||||
assert!(!c3.finished);
|
||||
// 思考块之后正文照常
|
||||
let c4 = apply_anthropic_event(&text_delta("答案"), &mut acc);
|
||||
assert_eq!(c4.delta, "答案");
|
||||
assert!(c4.reasoning_content.is_none());
|
||||
}
|
||||
|
||||
/// error 事件 → error=Some + finished=false避免残缺响应被当正常完成入库,
|
||||
/// 由 stream_llm 识别 error 非空发 AiError + 丢弃残缺,与 OpenAI 路径 Err 一致)
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user