修复: 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"}
|
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tool_choice: Option<serde_json::Value>,
|
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 忽略)。
|
/// 多模态消息内容片。Text 片为字符串;Image 片可走 url 或 base64(二选一,base64 非空时 url 忽略)。
|
||||||
@@ -115,29 +118,32 @@ pub struct ChatMessage {
|
|||||||
/// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。
|
/// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
|
/// DeepSeek thinking 模式的推理内容(多轮需回传)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatMessage {
|
impl ChatMessage {
|
||||||
pub fn system(content: impl Into<String>) -> Self {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 片)。
|
/// 多模态 user 消息:content 文本 + parts(含 Image 片)。
|
||||||
/// content 作为人类可读文本(也作非 vision 端点降级载荷);parts 透传给 vision 端点。
|
/// content 作为人类可读文本(也作非 vision 端点降级载荷);parts 透传给 vision 端点。
|
||||||
pub fn user_parts(content: impl Into<String>, parts: Vec<ContentPart>) -> Self {
|
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 判定走多模态分支)。
|
/// 是否含图片片(供 provider 判定走多模态分支)。
|
||||||
@@ -249,6 +255,9 @@ pub struct CompletionResponse {
|
|||||||
/// AI 发起的工具调用(如有)
|
/// AI 发起的工具调用(如有)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tool_calls: Option<Vec<ToolCall>>,
|
pub tool_calls: Option<Vec<ToolCall>>,
|
||||||
|
/// DeepSeek thinking 模式响应中的推理内容(需回传到下一轮请求)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token 用量
|
/// Token 用量
|
||||||
@@ -276,6 +285,9 @@ pub struct StreamChunk {
|
|||||||
/// 非空表示流中途出错,不应视为正常完成(finished 路径),由 stream_llm 转 AiError。
|
/// 非空表示流中途出错,不应视为正常完成(finished 路径),由 stream_llm 转 AiError。
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub error: Option<String>,
|
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,
|
tool_calls: None,
|
||||||
model: None,
|
model: None,
|
||||||
status: None,
|
status: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
assert_eq!(m.content, "字面量构造");
|
assert_eq!(m.content, "字面量构造");
|
||||||
assert!(m.parts.is_none());
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
let v: serde_json::Value = match serde_json::from_str(data) {
|
let v: serde_json::Value = match serde_json::from_str(data) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None }
|
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("");
|
let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||||
@@ -128,7 +128,7 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
total_tokens: inp as u32,
|
total_tokens: inp as u32,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None }
|
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
|
||||||
}
|
}
|
||||||
// 消息增量:output_tokens 是累计值(非增量),直接覆盖 completion + 重算 total
|
// 消息增量:output_tokens 是累计值(非增量),直接覆盖 completion + 重算 total
|
||||||
"message_delta" => {
|
"message_delta" => {
|
||||||
@@ -138,14 +138,14 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
acc.completion_tokens = out as u32;
|
acc.completion_tokens = out as u32;
|
||||||
acc.total_tokens = acc.prompt_tokens + acc.completion_tokens;
|
acc.total_tokens = acc.prompt_tokens + acc.completion_tokens;
|
||||||
}
|
}
|
||||||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None }
|
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
|
||||||
}
|
}
|
||||||
// 文本增量
|
// 文本增量
|
||||||
"content_block_delta" => {
|
"content_block_delta" => {
|
||||||
if let Some(delta) = v.get("delta") {
|
if let Some(delta) = v.get("delta") {
|
||||||
if delta.get("type").and_then(|t| t.as_str()) == Some("text_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();
|
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 };
|
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") {
|
if delta.get("type").and_then(|t| t.as_str()) == Some("input_json_delta") {
|
||||||
@@ -162,10 +162,11 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
}]),
|
}]),
|
||||||
usage: None,
|
usage: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None }
|
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
|
||||||
}
|
}
|
||||||
// 工具块开始:带 id + name
|
// 工具块开始:带 id + name
|
||||||
"content_block_start" => {
|
"content_block_start" => {
|
||||||
@@ -194,10 +195,11 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
}]),
|
}]),
|
||||||
usage: None,
|
usage: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None }
|
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None }
|
||||||
}
|
}
|
||||||
// 消息结束:带出累积 usage
|
// 消息结束:带出累积 usage
|
||||||
"message_stop" => StreamChunk {
|
"message_stop" => StreamChunk {
|
||||||
@@ -206,16 +208,17 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
|||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
usage: usage_accum.take(),
|
usage: usage_accum.take(),
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
// 错误事件:流中途出错。不走 finished 完成路径(避免残缺响应被当正常完成入库),
|
// 错误事件:流中途出错。不走 finished 完成路径(避免残缺响应被当正常完成入库),
|
||||||
// 改由 stream_llm 识别 error 非空 → 发 AiError + 丢弃残缺(与 OpenAI 路径 Err 一致)。
|
// 改由 stream_llm 识别 error 非空 → 发 AiError + 丢弃残缺(与 OpenAI 路径 Err 一致)。
|
||||||
"error" => {
|
"error" => {
|
||||||
let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error").to_string();
|
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 流式错误事件");
|
error!(%msg, "Anthropic 流式错误事件");
|
||||||
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: Some(msg) }
|
StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: Some(msg), reasoning_content: None }
|
||||||
}
|
}
|
||||||
// content_block_stop / ping 等不产出 chunk
|
// content_block_stop / ping 等不产出 chunk
|
||||||
_ => StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None },
|
_ => StreamChunk { delta: String::new(), finished: false, tool_calls: None, usage: None, error: None, reasoning_content: None },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,6 +544,7 @@ impl LlmProvider for AnthropicCompatProvider {
|
|||||||
model: resp.model,
|
model: resp.model,
|
||||||
usage,
|
usage,
|
||||||
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
|
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
|
||||||
|
reasoning_content: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -808,6 +812,7 @@ mod tests {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
let body = provider.convert_request(req);
|
let body = provider.convert_request(req);
|
||||||
// user 消息 content 应为数组形态
|
// user 消息 content 应为数组形态
|
||||||
@@ -839,6 +844,7 @@ mod tests {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
let body = provider.convert_request(req);
|
let body = provider.convert_request(req);
|
||||||
let user_msg = body
|
let user_msg = body
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ struct OpenAiRequest {
|
|||||||
/// 流式时请求末 chunk 携带 usage(OpenAI 官方 + DeepSeek/GLM 兼容)
|
/// 流式时请求末 chunk 携带 usage(OpenAI 官方 + DeepSeek/GLM 兼容)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
stream_options: Option<serde_json::Value>,
|
stream_options: Option<serde_json::Value>,
|
||||||
|
/// DeepSeek thinking 模式
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OpenAI 消息格式
|
/// OpenAI 消息格式
|
||||||
@@ -58,6 +61,9 @@ struct OpenAiMessage {
|
|||||||
tool_call_id: Option<String>,
|
tool_call_id: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tool_calls: Option<Vec<serde_json::Value>>,
|
tool_calls: Option<Vec<serde_json::Value>>,
|
||||||
|
/// DeepSeek thinking 模式推理内容
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OpenAI 同步响应
|
/// OpenAI 同步响应
|
||||||
@@ -78,6 +84,9 @@ struct OpenAiChoice {
|
|||||||
struct OpenAiMessageResp {
|
struct OpenAiMessageResp {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
tool_calls: Option<Vec<OpenAiToolCallResp>>,
|
tool_calls: Option<Vec<OpenAiToolCallResp>>,
|
||||||
|
/// DeepSeek thinking 模式响应中的推理内容
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -120,6 +129,9 @@ struct OpenAiStreamChoice {
|
|||||||
struct OpenAiStreamDelta {
|
struct OpenAiStreamDelta {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
tool_calls: Option<Vec<OpenAiStreamToolCall>>,
|
tool_calls: Option<Vec<OpenAiStreamToolCall>>,
|
||||||
|
/// DeepSeek thinking 模式流式推理内容
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -158,6 +170,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
|||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
usage: usage_accum.take(),
|
usage: usage_accum.take(),
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +208,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
|||||||
tool_calls,
|
tool_calls,
|
||||||
usage: None,
|
usage: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: choice.delta.reasoning_content,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// choices 为空 = usage-only chunk,不输出文本(usage 已累积)
|
// choices 为空 = usage-only chunk,不输出文本(usage 已累积)
|
||||||
@@ -204,6 +218,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
|||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,6 +230,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
|||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
reasoning_content: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,6 +387,7 @@ impl OpenAICompatProvider {
|
|||||||
content,
|
content,
|
||||||
tool_call_id: m.tool_call_id,
|
tool_call_id: m.tool_call_id,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
|
reasoning_content: m.reasoning_content,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -389,6 +406,7 @@ impl OpenAICompatProvider {
|
|||||||
stream: req.stream,
|
stream: req.stream,
|
||||||
tools,
|
tools,
|
||||||
tool_choice: req.tool_choice,
|
tool_choice: req.tool_choice,
|
||||||
|
reasoning_content: req.reasoning_content,
|
||||||
// 流式请求末 chunk 带 usage(同步调用 complete 不需要)
|
// 流式请求末 chunk 带 usage(同步调用 complete 不需要)
|
||||||
stream_options: if req.stream {
|
stream_options: if req.stream {
|
||||||
Some(serde_json::json!({ "include_usage": true }))
|
Some(serde_json::json!({ "include_usage": true }))
|
||||||
@@ -509,6 +527,7 @@ impl LlmProvider for OpenAICompatProvider {
|
|||||||
model: body.model,
|
model: body.model,
|
||||||
usage,
|
usage,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
|
reasoning_content: choice.message.reasoning_content,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -755,6 +774,7 @@ mod tests {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
let out = provider.convert_request(req);
|
let out = provider.convert_request(req);
|
||||||
let msg = &out.messages[0];
|
let msg = &out.messages[0];
|
||||||
@@ -783,6 +803,7 @@ mod tests {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
let out = provider.convert_request(req);
|
let out = provider.convert_request(req);
|
||||||
let msg = &out.messages[0];
|
let msg = &out.messages[0];
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ impl Node for AiNode {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -513,6 +514,7 @@ impl Node for AiSelfReviewNode {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -924,6 +926,7 @@ mod tests {
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
let response = provider.complete(request).await.expect("GLM 调用失败");
|
let response = provider.complete(request).await.expect("GLM 调用失败");
|
||||||
assert!(!response.text.is_empty(), "GLM 返回空文本");
|
assert!(!response.text.is_empty(), "GLM 返回空文本");
|
||||||
|
|||||||
27
docs/todo.md
27
docs/todo.md
@@ -115,6 +115,33 @@
|
|||||||
**已审文件清单(本轮)**:crates/df-ai-core/src/provider.rs · crates/df-ai/src/anthropic_compat.rs · crates/df-ai/src/openai_compat.rs · src-tauri/src/commands/ai/conversation.rs · src-tauri/src/commands/ai/commands.rs · src/api/ai.ts · src/api/types.ts · src/composables/ai/useAiSend.ts · src/composables/ai/useAiConversations.ts · src/stores/ai.ts · src/components/AiChat.vue(图片构建+渲染) · src/components/settings/ProviderPanel.vue
|
**已审文件清单(本轮)**:crates/df-ai-core/src/provider.rs · crates/df-ai/src/anthropic_compat.rs · crates/df-ai/src/openai_compat.rs · src-tauri/src/commands/ai/conversation.rs · src-tauri/src/commands/ai/commands.rs · src/api/ai.ts · src/api/types.ts · src/composables/ai/useAiSend.ts · src/composables/ai/useAiConversations.ts · src/stores/ai.ts · src/components/AiChat.vue(图片构建+渲染) · src/components/settings/ProviderPanel.vue
|
||||||
**新登记 todo**:B-260617-07 · B-260617-08
|
**新登记 todo**:B-260617-07 · B-260617-08
|
||||||
|
|
||||||
|
### 🔧 2026-06-17 Tasks.vue i18n 缺失(仅分析·未实施)
|
||||||
|
|
||||||
|
> Tasks.vue:72 报 `Not found 'completed' key in 'zh' locale messages`。session-role-diagnose-only,仅分析+记todo。
|
||||||
|
|
||||||
|
**现象**:Tasks.vue:72 `$t(statusLabel(task.status))` → `taskStatusLabel()` → `TASK_STATUS_LABELS[status]` 返回 `'tasks.status.done'` → `$t('tasks.status.done')` 在 en/zh-CN 均未定义 → intlify fallback 到 en 仍缺失。
|
||||||
|
|
||||||
|
**根因**:`src/constants/project.ts:61-68` 定义了 `TASK_STATUS_LABELS`(值如 `'tasks.status.done'`/`'tasks.status.todo'` 等 7 个 status + `'tasks.statusFilter.done'` 等 5 个 filter + priority 相关),但 **en/zh-CN 的 tasks 相关 i18n 文件完全不存在**。6b67214 大量新增功能时未同步补 tasks i18n key。
|
||||||
|
|
||||||
|
**影响范围**(需补全的 key):
|
||||||
|
| 组件 | 用到的 key 前缀 | 缺失的 key |
|
||||||
|
|---|---|---|
|
||||||
|
| Tasks.vue:72 | `tasks.status.*` (7个) | 全部 |
|
||||||
|
| Tasks.vue:148-155 | `tasks.statusFilter.*` (5个) | 全部 |
|
||||||
|
| Tasks.vue:96-103 | `tasks.priority.*` / `P0`/`P1`/`P2`/`P3` (5+4=9) | 全部 |
|
||||||
|
|
||||||
|
- [ ] B-260617-10 [P2] — **Tasks.vue i18n 缺失**。`$t('tasks.status.done')` 等 **21 个翻译键**在 en/zh-CN 均未定义(intlify fallback 也找不到),根因:6b67214 新增大量功能时未同步补 tasks i18n key(`TASK_STATUS_LABELS` 定义了值但无对应 `$t()` 条目)。**影响**:Tasks.vue 状态标签/筛选器/优先级标签全部显示原始 key 而非翻译文本。—— src/views/Tasks.vue(:72) · src/constants/project.ts(:61-82 TASK_STATUS_LABELS/TASK_STATUS_CLASS/PRIORITY_LABELS/PRIORITY_CLASSES)
|
||||||
|
|
||||||
|
> **2026-06-17 进展注记(1cd7652)**:提交 1cd7652 已做**数据层映射兜底**(project.ts 新增 `LEGACY_STATUS_MAP`:completed→done / review_ready→in_review / merged→done / abandoned→cancelled),`taskStatusLabel/taskStatusClass` 经映射后老 DB 脏态不再直接走 `$t()` 报 not found。**但 i18n key 补全(显示层)仍未做**——映射后 `$t('tasks.status.done')` 仍 not found(tasks.ts 不存在),用户从看到 "completed" 变成看到 "tasks.status.done" 原始 key。**B-260617-10 主问题(补全 21 个 i18n key)仍在**,1cd7652 是其前置的数据归一,待补 key 后老态自动正确显示。映射目标态(done/in_review/cancelled)均已核验在 TASK_STATUS_LABELS 7 态内 ✓。
|
||||||
|
|
||||||
|
### 🔧 2026-06-17 走查·tauri 打包目标收窄 + 状态映射 DRY(仅分析·未实施)
|
||||||
|
|
||||||
|
> 本轮 git diff 核验工作区未提交改动 + 最新提交 1cd7652。session-role-diagnose-only。
|
||||||
|
|
||||||
|
- [ ] B-260617-11 [P2] — **tauri.conf.json 打包目标收窄未提交,若误入库锁死非 Windows 构建**。工作区改动 `bundle.targets: "all" → ["nsis"]`(src-tauri/tauri.conf.json:28),收窄到仅 Windows NSIS 安装包。若意图为本地只打 Windows 包,合理;**但若随其他改动一并提交**,macOS(dmg/app)、Linux(deb/appimage)构建将不可用,影响其他开发者/CI。**确认点**:该改动是临时本地构建还是有意入库?临时则建议提交前 revert 此行;有意则建议改为按平台条件配置而非硬编码单一 target。—— src-tauri/tauri.conf.json(:28)
|
||||||
|
|
||||||
|
- [ ] B-260617-12 [P3·可选] — **LEGACY_STATUS_MAP 映射 DRY 重复**。1cd7652 中 `taskStatusLabel`(:96) 与 `taskStatusClass`(:101) 各写一遍 `const mapped = LEGACY_STATUS_MAP[status] ?? status`。两处一行重复,可抽 `mapLegacyStatus(status)` 收敛。极简优先可不动(仅 2 处 + 单行),登记备查。—— src/constants/project.ts(:96,:101)
|
||||||
|
|
||||||
### 🔧 2026-06-17 aichat 消息全量重叠(5角度深入分析·未实施)
|
### 🔧 2026-06-17 aichat 消息全量重叠(5角度深入分析·未实施)
|
||||||
|
|
||||||
> 用户报 aichat 对话「大量重叠」「不单间距问题」。session-role-diagnose-only,5 角度并行论证后记录。
|
> 用户报 aichat 对话「大量重叠」「不单间距问题」。session-role-diagnose-only,5 角度并行论证后记录。
|
||||||
|
|||||||
36
docs/待决策.md
36
docs/待决策.md
@@ -79,6 +79,42 @@
|
|||||||
- **待用户操作**:部分场景运行时实测(A 路线场景 2/3)。
|
- **待用户操作**:部分场景运行时实测(A 路线场景 2/3)。
|
||||||
- **状态**:🟡 待用户实测
|
- **状态**:🟡 待用户实测
|
||||||
|
|
||||||
|
### 🟡 待设计/架构演进(有决策点,需方案设计或长期规划)
|
||||||
|
|
||||||
|
#### ARC-260615-07 架构重构批(排期/优先级决策)
|
||||||
|
- **背景**:src-tauri IPC 编排层 5711 行成事实业务层,7 项架构债。df-core→df-types 已完成(CR-61),剩 6 项独立大改。
|
||||||
|
- **决策点**:6 项重构何时做/优先级/是否做(每项 ROI 与风险权衡,无法纯技术论证——做不做是资源/产品取舍)
|
||||||
|
- **选项(剩余 6 项)**:
|
||||||
|
- a: df-app 抽取(IPC 编排层 5711 行独立 crate)
|
||||||
|
- b: AI agent loop 从 IPC 下沉 df-ai(逻辑与 IO 分离)
|
||||||
|
- c: 类型契约 ts-rs 代码生成(替代手写 types.ts)
|
||||||
|
- d: AiSession 多会话(B 路线前置,关联 S-260614-01)
|
||||||
|
- e: AppState 分组(5711 行 state 拆分)
|
||||||
|
- f: IPC 命名统一
|
||||||
|
- **推荐**:**⏸️ 缓做**(重构低 ROI,当前功能优先;待稳定后按 d→b→a 顺序,d 是 B 路线前置可先)
|
||||||
|
- **关联**:todo ARC-260615-07 / memory aichat-arch-extensibility
|
||||||
|
- **状态**:🟡 待排期决策
|
||||||
|
|
||||||
|
#### UX-260617-01 aichat 消息全量重叠(待 DevTools 定位根因)
|
||||||
|
- **背景**:用户实测消息大量重叠堆叠。5 角度分析排除 CSS/scoped/DOM,核心嫌疑虚拟滚动 IO 半激活态不一致 + height=0 未设 minHeight → slot 塌 0 → 重叠。
|
||||||
|
- **决策点**:根因确切触发路径(需 DevTools 实测,静态分析已到极限)
|
||||||
|
- **选项**:
|
||||||
|
- a: 虚拟滚动 IO/RO 时序竞态(主嫌疑,unload 分支 minHeight 兜底修复)
|
||||||
|
- b: 其他(DOM 结构问题,需 DevTools 实查)
|
||||||
|
- **推荐**:**待用户 DevTools 实测**(shouldRenderMsg 卸载时检查 slot 实际高度 + IO unobserve/observe 时序),定位后修法明确(unload 分支 minHeight fallback)
|
||||||
|
- **关联**:todo UX-260617-01 / docs/05-代码审查/AI聊天组件极端数据场景分析-2026-06-17.md
|
||||||
|
- **状态**:🟡 待 DevTools 验证根因
|
||||||
|
|
||||||
|
#### UX-260617-28 双监听器同通道(长期架构演进)
|
||||||
|
- **背景**:useAiEvents + useAiContext 各自 listen('ai-chat-event'),人工 AiCompressing flag 协调防双重处理,非架构保证。未来新增事件处理可能触发双重 bug。
|
||||||
|
- **决策点**:是否升级单一分发器模式(架构保证)
|
||||||
|
- **选项**:
|
||||||
|
- a: 保持现状(AiCompressing flag 协调够用)
|
||||||
|
- b: 单一分发器(架构保证,但大改)
|
||||||
|
- **推荐**:**⏸️ 暂缓**(当前 flag 协调有效,未来新增事件处理触发双重 bug 时再升级)
|
||||||
|
- **关联**:todo UX-260617-28 [INFO]
|
||||||
|
- **状态**:⏸️ 长期(INFO,当前够用)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已决归档
|
## 已决归档
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ enum StreamOutcome {
|
|||||||
usage: df_ai::provider::TokenUsage,
|
usage: df_ai::provider::TokenUsage,
|
||||||
incomplete: bool,
|
incomplete: bool,
|
||||||
resolved_model: String,
|
resolved_model: String,
|
||||||
|
/// DeepSeek thinking 模式推理内容(需回传到下一轮请求)
|
||||||
|
reasoning_content: Option<String>,
|
||||||
},
|
},
|
||||||
InitFailedExhausted,
|
InitFailedExhausted,
|
||||||
Fatal,
|
Fatal,
|
||||||
@@ -164,6 +166,7 @@ async fn stream_one_provider(
|
|||||||
retry_deadline: tokio::time::Instant,
|
retry_deadline: tokio::time::Instant,
|
||||||
model_override: &Option<String>,
|
model_override: &Option<String>,
|
||||||
agentic_req: &TaskRequirements,
|
agentic_req: &TaskRequirements,
|
||||||
|
last_reasoning_content: &Option<String>,
|
||||||
) -> StreamOutcome {
|
) -> StreamOutcome {
|
||||||
// resolve→ensure→build 三步(复用 secret::build_provider_for,DRY)。
|
// resolve→ensure→build 三步(复用 secret::build_provider_for,DRY)。
|
||||||
// key 缺失/损坏 → Err → 归 Fatal(stream_llm Fatal 也是 key 类错,语义一致)。
|
// key 缺失/损坏 → Err → 归 Fatal(stream_llm Fatal 也是 key 类错,语义一致)。
|
||||||
@@ -209,17 +212,19 @@ async fn stream_one_provider(
|
|||||||
stream: true,
|
stream: true,
|
||||||
tools: if tool_defs.is_empty() { None } else { Some(tool_defs.to_vec()) },
|
tools: if tool_defs.is_empty() { None } else { Some(tool_defs.to_vec()) },
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: last_reasoning_content.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match stream_llm(&*provider, retry_request, app_handle, stop_flag, notify, conv_id).await {
|
match stream_llm(&*provider, retry_request, app_handle, stop_flag, notify, conv_id).await {
|
||||||
StreamResult::Complete { text, tool_calls, usage } => {
|
StreamResult::Complete { text, tool_calls, usage, reasoning_content } => {
|
||||||
return StreamOutcome::Success {
|
return StreamOutcome::Success {
|
||||||
text, tool_calls, usage,
|
text, tool_calls, usage,
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resolved_model,
|
resolved_model,
|
||||||
|
reasoning_content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
StreamResult::Partial { text, tool_calls, usage } => {
|
StreamResult::Partial { text, tool_calls, usage, reasoning_content } => {
|
||||||
// MidStream 保文不重试(决策 a1)。携带 incomplete=true 交调用方走保文路径。
|
// MidStream 保文不重试(决策 a1)。携带 incomplete=true 交调用方走保文路径。
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
conv_id = %conv_id,
|
conv_id = %conv_id,
|
||||||
@@ -231,6 +236,7 @@ async fn stream_one_provider(
|
|||||||
text, tool_calls, usage,
|
text, tool_calls, usage,
|
||||||
incomplete: true,
|
incomplete: true,
|
||||||
resolved_model,
|
resolved_model,
|
||||||
|
reasoning_content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
StreamResult::InitFailed { retryable } => {
|
StreamResult::InitFailed { retryable } => {
|
||||||
@@ -435,6 +441,9 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
|
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
|
||||||
let mut converged = false;
|
let mut converged = false;
|
||||||
|
|
||||||
|
// BUG-260617-12: DeepSeek thinking 模式推理内容跨轮透传
|
||||||
|
let mut last_reasoning_content: Option<String> = None;
|
||||||
|
|
||||||
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
|
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
|
||||||
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
||||||
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
||||||
@@ -656,9 +665,9 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
let mut candidate_chain: Vec<AiProviderRecord> = Vec::with_capacity(1 + candidates.len());
|
let mut candidate_chain: Vec<AiProviderRecord> = Vec::with_capacity(1 + candidates.len());
|
||||||
candidate_chain.push(provider_config.clone());
|
candidate_chain.push(provider_config.clone());
|
||||||
candidate_chain.extend(candidates.iter().cloned());
|
candidate_chain.extend(candidates.iter().cloned());
|
||||||
let (full_text, tool_calls_acc, round_usage, incomplete) = {
|
let (full_text, tool_calls_acc, round_usage, incomplete, round_reasoning_content) = {
|
||||||
// outcome 累积成功结果(Complete/Partial),InitFailed 不写入。
|
// outcome 累积成功结果(Complete/Partial),InitFailed 不写入。
|
||||||
let mut outcome: Option<(String, std::collections::HashMap<u32, super::ToolCallDraft>, df_ai::provider::TokenUsage, bool)> = None;
|
let mut outcome: Option<(String, std::collections::HashMap<u32, super::ToolCallDraft>, df_ai::provider::TokenUsage, bool, Option<String>)> = None;
|
||||||
|
|
||||||
'candidate: for candidate in &candidate_chain {
|
'candidate: for candidate in &candidate_chain {
|
||||||
// per-provider permit(可选):set_provider_caps 未配置时返回 None(单 provider 零变化);
|
// per-provider permit(可选):set_provider_caps 未配置时返回 None(单 provider 零变化);
|
||||||
@@ -678,14 +687,16 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
retry_deadline,
|
retry_deadline,
|
||||||
&model_override,
|
&model_override,
|
||||||
&agentic_req,
|
&agentic_req,
|
||||||
|
&last_reasoning_content,
|
||||||
).await {
|
).await {
|
||||||
StreamOutcome::Success { text, tool_calls, usage, incomplete, resolved_model: m } => {
|
StreamOutcome::Success { text, tool_calls, usage, incomplete, resolved_model: m, reasoning_content: rc } => {
|
||||||
// 成功:更新迭代级 resolved_model + provider_config(供后续 push/save/标题)。
|
// 成功:更新迭代级 resolved_model + provider_config(供后续 push/save/标题)。
|
||||||
// mut 解构(已在顶部声明 mut):本轮后续 save/push 用「实际成功所用 provider」
|
// mut 解构(已在顶部声明 mut):本轮后续 save/push 用「实际成功所用 provider」
|
||||||
// 而非主 candidate。compress 已在本轮 stream 之前用过 primary,不受影响。
|
// 而非主 candidate。compress 已在本轮 stream 之前用过 primary,不受影响。
|
||||||
resolved_model = m;
|
resolved_model = m;
|
||||||
provider_config = candidate.clone();
|
provider_config = candidate.clone();
|
||||||
outcome = Some((text, tool_calls, usage, incomplete));
|
last_reasoning_content = rc;
|
||||||
|
outcome = Some((text, tool_calls, usage, incomplete, last_reasoning_content.clone()));
|
||||||
break 'candidate;
|
break 'candidate;
|
||||||
}
|
}
|
||||||
StreamOutcome::InitFailedExhausted => {
|
StreamOutcome::InitFailedExhausted => {
|
||||||
@@ -750,6 +761,8 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
if !full_text.is_empty() {
|
if !full_text.is_empty() {
|
||||||
let mut msg = ChatMessage::assistant(&full_text);
|
let mut msg = ChatMessage::assistant(&full_text);
|
||||||
msg.model = Some(resolved_model.clone());
|
msg.model = Some(resolved_model.clone());
|
||||||
|
// BUG-260617-12: MidStream 保文也回填 reasoning_content
|
||||||
|
msg.reasoning_content = round_reasoning_content.clone();
|
||||||
session.messages.push(msg);
|
session.messages.push(msg);
|
||||||
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
|
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
|
||||||
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
|
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
|
||||||
@@ -807,10 +820,13 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
.collect();
|
.collect();
|
||||||
let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls);
|
let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls);
|
||||||
msg.model = Some(resolved_model.clone());
|
msg.model = Some(resolved_model.clone());
|
||||||
|
// BUG-260617-12: 回填 reasoning_content 供下一轮请求透传
|
||||||
|
msg.reasoning_content = last_reasoning_content.clone();
|
||||||
session.messages.push(msg);
|
session.messages.push(msg);
|
||||||
} else if !full_text.is_empty() {
|
} else if !full_text.is_empty() {
|
||||||
let mut msg = ChatMessage::assistant(&full_text);
|
let mut msg = ChatMessage::assistant(&full_text);
|
||||||
msg.model = Some(resolved_model.clone());
|
msg.model = Some(resolved_model.clone());
|
||||||
|
msg.reasoning_content = last_reasoning_content.clone();
|
||||||
session.messages.push(msg);
|
session.messages.push(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ pub(crate) async fn compress_via_llm(
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)
|
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ async fn extract_knowledge_from_conversation(
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {
|
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ pub(crate) enum StreamResult {
|
|||||||
text: String,
|
text: String,
|
||||||
tool_calls: HashMap<u32, ToolCallDraft>,
|
tool_calls: HashMap<u32, ToolCallDraft>,
|
||||||
usage: df_ai::provider::TokenUsage,
|
usage: df_ai::provider::TokenUsage,
|
||||||
|
/// DeepSeek thinking 模式推理内容(需回传到下一轮请求)
|
||||||
|
reasoning_content: Option<String>,
|
||||||
},
|
},
|
||||||
/// 流中途断(MidStream chunk Err / idle timeout / provider stream-error / 有 partial_text
|
/// 流中途断(MidStream chunk Err / idle timeout / provider stream-error / 有 partial_text
|
||||||
/// 但流尽未收到 finished)。已 emit 过 AiTextDelta(前端 currentText 已累积),
|
/// 但流尽未收到 finished)。已 emit 过 AiTextDelta(前端 currentText 已累积),
|
||||||
@@ -110,6 +112,8 @@ pub(crate) enum StreamResult {
|
|||||||
text: String,
|
text: String,
|
||||||
tool_calls: HashMap<u32, ToolCallDraft>,
|
tool_calls: HashMap<u32, ToolCallDraft>,
|
||||||
usage: df_ai::provider::TokenUsage,
|
usage: df_ai::provider::TokenUsage,
|
||||||
|
/// DeepSeek thinking 模式推理内容(部分累积)
|
||||||
|
reasoning_content: Option<String>,
|
||||||
},
|
},
|
||||||
/// Init 失败(provider.stream() Err:建连/鉴权/HTTP non-2xx)。已 emit AiError。
|
/// Init 失败(provider.stream() Err:建连/鉴权/HTTP non-2xx)。已 emit AiError。
|
||||||
/// `retryable`: 据状态码分类(retry::is_status_retryable 镜像):true=5xx/429/timeout/connect
|
/// `retryable`: 据状态码分类(retry::is_status_retryable 镜像):true=5xx/429/timeout/connect
|
||||||
@@ -154,6 +158,8 @@ pub(crate) async fn stream_llm(
|
|||||||
let mut finished_received = false;
|
let mut finished_received = false;
|
||||||
let mut stopped = false;
|
let mut stopped = false;
|
||||||
let mut final_usage: Option<df_ai::provider::TokenUsage> = None;
|
let mut final_usage: Option<df_ai::provider::TokenUsage> = None;
|
||||||
|
// BUG-260617-12: DeepSeek thinking 模式推理内容累积(多轮需回传)
|
||||||
|
let mut reasoning_content_acc: Option<String> = None;
|
||||||
|
|
||||||
// B-260615-15:heartbeat interval 提至 loop 外复用,避免每轮重建计时器
|
// B-260615-15:heartbeat interval 提至 loop 外复用,避免每轮重建计时器
|
||||||
// (每轮重建会丢已积累的节拍,且 interval 首次 tick 立即返回的特性会被误用)。
|
// (每轮重建会丢已积累的节拍,且 interval 首次 tick 立即返回的特性会被误用)。
|
||||||
@@ -208,6 +214,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(None) => break, // 流正常结束
|
Ok(None) => break, // 流正常结束
|
||||||
@@ -231,6 +238,10 @@ pub(crate) async fn stream_llm(
|
|||||||
if let Some(u) = &chunk.usage {
|
if let Some(u) = &chunk.usage {
|
||||||
final_usage = Some(u.clone());
|
final_usage = Some(u.clone());
|
||||||
}
|
}
|
||||||
|
// BUG-260617-12: DeepSeek thinking 模式推理内容累积
|
||||||
|
if let Some(ref rc) = chunk.reasoning_content {
|
||||||
|
reasoning_content_acc.get_or_insert_with(String::new).push_str(rc);
|
||||||
|
}
|
||||||
// provider 流式错误事件(Anthropic SSE `type=="error"` 等):
|
// provider 流式错误事件(Anthropic SSE `type=="error"` 等):
|
||||||
// UX-2025-04 / CR-30-2 / 决策 a1: MidStream 类失败——已有文本则保文(Partial),
|
// UX-2025-04 / CR-30-2 / 决策 a1: MidStream 类失败——已有文本则保文(Partial),
|
||||||
// 不重试。空文本无文可保,emit AiError + InitFailed(保守 retryable=true,
|
// 不重试。空文本无文可保,emit AiError + InitFailed(保守 retryable=true,
|
||||||
@@ -260,6 +271,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if chunk.finished {
|
if chunk.finished {
|
||||||
@@ -301,6 +313,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +346,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +372,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +381,7 @@ pub(crate) async fn stream_llm(
|
|||||||
text: full_text,
|
text: full_text,
|
||||||
tool_calls: tool_calls_acc,
|
tool_calls: tool_calls_acc,
|
||||||
usage: final_usage.unwrap_or_default(),
|
usage: final_usage.unwrap_or_default(),
|
||||||
|
reasoning_content: reasoning_content_acc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub(crate) async fn ensure_conversation_title(
|
|||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
model: None,
|
model: None,
|
||||||
status: None,
|
status: None,
|
||||||
|
reasoning_content: m.reasoning_content.clone(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(summary, session.messages.all_messages_clone())
|
(summary, session.messages.all_messages_clone())
|
||||||
@@ -134,6 +135,7 @@ async fn generate_title_via_llm(
|
|||||||
stream: false,
|
stream: false,
|
||||||
tools: None,
|
tools: None,
|
||||||
tool_choice: None,
|
tool_choice: None,
|
||||||
|
reasoning_content: None,
|
||||||
};
|
};
|
||||||
// LLM 并发限流(标题生成属独立调用,纳入双层 Semaphore)
|
// LLM 并发限流(标题生成属独立调用,纳入双层 Semaphore)
|
||||||
let _global_permit = llm_concurrency.acquire_global().await;
|
let _global_permit = llm_concurrency.acquire_global().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user