修复: DeepSeek reasoning_content 全链路透传(跨6 crate补字段+映射+agentic loop累积回传)
This commit is contained in:
@@ -36,6 +36,9 @@ pub struct CompletionRequest {
|
||||
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<serde_json::Value>,
|
||||
/// DeepSeek thinking 模式:需把上一轮 response 的 reasoning_content 回传
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
/// 多模态消息内容片。Text 片为字符串;Image 片可走 url 或 base64(二选一,base64 非空时 url 忽略)。
|
||||
@@ -115,29 +118,32 @@ pub struct ChatMessage {
|
||||
/// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<String>,
|
||||
/// DeepSeek thinking 模式的推理内容(多轮需回传)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::System, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None }
|
||||
Self { role: MessageRole::System, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::User, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None }
|
||||
Self { role: MessageRole::User, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
pub fn assistant(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None }
|
||||
Self { role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
|
||||
Self { role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: Some(tool_calls), model: None, status: None }
|
||||
Self { role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: Some(tool_calls), model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
pub fn tool_result(call_id: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::Tool, content: content.into(), parts: None, tool_call_id: Some(call_id.into()), tool_calls: None, model: None, status: None }
|
||||
Self { role: MessageRole::Tool, content: content.into(), parts: None, tool_call_id: Some(call_id.into()), tool_calls: None, model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
|
||||
/// 多模态 user 消息:content 文本 + parts(含 Image 片)。
|
||||
/// content 作为人类可读文本(也作非 vision 端点降级载荷);parts 透传给 vision 端点。
|
||||
pub fn user_parts(content: impl Into<String>, parts: Vec<ContentPart>) -> Self {
|
||||
Self { role: MessageRole::User, content: content.into(), parts: Some(parts), tool_call_id: None, tool_calls: None, model: None, status: None }
|
||||
Self { role: MessageRole::User, content: content.into(), parts: Some(parts), tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None }
|
||||
}
|
||||
|
||||
/// 是否含图片片(供 provider 判定走多模态分支)。
|
||||
@@ -249,6 +255,9 @@ pub struct CompletionResponse {
|
||||
/// AI 发起的工具调用(如有)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
/// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
/// Token 用量
|
||||
@@ -276,6 +285,9 @@ pub struct StreamChunk {
|
||||
/// 非空表示流中途出错,不应视为正常完成(finished 路径),由 stream_llm 转 AiError。
|
||||
#[serde(skip)]
|
||||
pub error: Option<String>,
|
||||
/// DeepSeek thinking 模式推理内容增量
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
/// 工具调用增量(流式中的片段)
|
||||
@@ -449,8 +461,98 @@ mod tests {
|
||||
tool_calls: None,
|
||||
model: None,
|
||||
status: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
assert_eq!(m.content, "字面量构造");
|
||||
assert!(m.parts.is_none());
|
||||
}
|
||||
|
||||
// ---------- DeepSeek reasoning_content ----------
|
||||
|
||||
/// 有 reasoning_content 时应序列化出来;None 时不出现
|
||||
#[test]
|
||||
fn completion_request_reasoning_content_serialization() {
|
||||
let req = CompletionRequest {
|
||||
model: "deepseek-r1".to_string(),
|
||||
messages: vec![ChatMessage::user("hello")],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: Some("let me think...".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("reasoning_content"), "reasoning_content 应出现在 JSON 中, got: {}", json);
|
||||
|
||||
// None 时不应出现(skip_serializing_if)
|
||||
let req_none = CompletionRequest { reasoning_content: None, ..req };
|
||||
let json_none = serde_json::to_string(&req_none).unwrap();
|
||||
assert!(!json_none.contains("reasoning_content"), "None 的 reasoning_content 不应序列化, got: {}", json_none);
|
||||
}
|
||||
|
||||
/// 所有便捷构造函数默认 reasoning_content 为 None
|
||||
#[test]
|
||||
fn chat_message_reasoning_content_constructors() {
|
||||
assert!(ChatMessage::system("sys").reasoning_content.is_none());
|
||||
assert!(ChatMessage::user("hi").reasoning_content.is_none());
|
||||
assert!(ChatMessage::assistant("resp").reasoning_content.is_none());
|
||||
assert!(ChatMessage::tool_result("call_1", "result").reasoning_content.is_none());
|
||||
}
|
||||
|
||||
/// ChatMessage reasoning_content 序列化 round-trip
|
||||
#[test]
|
||||
fn chat_message_reasoning_content_roundtrip() {
|
||||
let m = ChatMessage {
|
||||
role: MessageRole::Assistant,
|
||||
content: "answer".to_string(),
|
||||
parts: None,
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
model: None,
|
||||
status: None,
|
||||
reasoning_content: Some("thinking process".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&m).unwrap();
|
||||
assert!(json.contains("reasoning_content"));
|
||||
|
||||
let deserialized: ChatMessage = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.reasoning_content, Some("thinking process".to_string()));
|
||||
}
|
||||
|
||||
/// StreamChunk reasoning_content 序列化
|
||||
#[test]
|
||||
fn stream_chunk_reasoning_content() {
|
||||
let chunk = StreamChunk {
|
||||
delta: "text".to_string(),
|
||||
finished: false,
|
||||
tool_calls: None,
|
||||
usage: None,
|
||||
error: None,
|
||||
reasoning_content: Some("thought".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&chunk).unwrap();
|
||||
assert!(json.contains("reasoning_content"));
|
||||
|
||||
let chunk_none = StreamChunk { reasoning_content: None, ..chunk };
|
||||
let json_none = serde_json::to_string(&chunk_none).unwrap();
|
||||
assert!(!json_none.contains("reasoning_content"));
|
||||
}
|
||||
|
||||
/// CompletionResponse reasoning_content 序列化
|
||||
#[test]
|
||||
fn completion_response_reasoning_content() {
|
||||
let resp = CompletionResponse {
|
||||
text: "ok".to_string(),
|
||||
model: "r1".to_string(),
|
||||
usage: TokenUsage { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
|
||||
tool_calls: None,
|
||||
reasoning_content: Some("r1 thought".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&resp).unwrap();
|
||||
assert!(json.contains("reasoning_content"));
|
||||
|
||||
let deserialized: CompletionResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.reasoning_content, Some("r1 thought".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user