修复: B-260626-01 首条 user 协议合规(sanitize 兜底 + ensure_leading_user)
- context.rs: 兜底全量也过 sanitize(对齐分支1/3,防主 loop 唯一 sanitize 漏洞——大体量 tool_result 致超预算且保护区满时,裸返 all_messages_clone 不过滤 truncated/中毒三元组/首条非法直送 provider) - anthropic_compat.rs + openai_compat.rs: ensure_leading_user 补 user 占位(Anthropic/OpenAI 协议要求首条非 assistant/tool;上游绕过 sanitize 的调用方——标题生成/知识注入/工作流 AI 节点——可能传入首条 assistant 序列,补占位保留上下文,tool_use/tool_result 配对完整无 orphan,远优于砍丢历史)
This commit is contained in:
@@ -105,7 +105,7 @@ impl OpenAICompatProvider {
|
||||
req.model
|
||||
};
|
||||
|
||||
let messages: Vec<OpenAiMessage> = req
|
||||
let mut messages: Vec<OpenAiMessage> = req
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
@@ -177,6 +177,12 @@ impl OpenAICompatProvider {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// B-260626-01: 保证首条 user/system(OpenAI 协议要求首条非 assistant/tool)。
|
||||
// 对齐 AnthropicCompatProvider::ensure_leading_user:上游绕过 sanitize 的调用方
|
||||
// (标题生成/知识注入/工作流 AI 节点等直构造 CompletionRequest 的路径)可能传入首条
|
||||
// assistant 的序列(会话恢复/续发/片段截取),补 user 占位保留上下文,首条合法。
|
||||
Self::ensure_leading_user(&mut messages);
|
||||
|
||||
let tools = req.tools.map(|defs| {
|
||||
defs.into_iter()
|
||||
.map(|d| serde_json::to_value(d).unwrap_or_default())
|
||||
@@ -201,6 +207,36 @@ impl OpenAICompatProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// B-260626-01: 保证 messages 首条为 user/system(OpenAI 协议要求首条非 assistant/tool)。
|
||||
///
|
||||
/// 对齐 `AnthropicCompatProvider::ensure_leading_user`。上游绕过 `ContextManager::sanitize_messages`
|
||||
/// 的调用方(标题生成/知识注入/工作流 AI 节点等直构造 CompletionRequest 的路径)可能传入首条
|
||||
/// assistant 的序列——会话恢复、续发或历史片段截取时,真正的首条 user 已被裁剪/压缩掉。
|
||||
///
|
||||
/// **用"补"而非"砍"**:开头插一条 user 占位,保留全部上下文(砍会丢工具调用历史,多轮砍到空)。
|
||||
/// 占位 user 紧贴原首条,不破坏 user/assistant 交替;仅异常路径触发(正常首条本就是 user)。
|
||||
fn ensure_leading_user(messages: &mut Vec<OpenAiMessage>) {
|
||||
let first_role = messages.first().map(|m| m.role.as_str()).unwrap_or("");
|
||||
if first_role == "user" || first_role == "system" {
|
||||
return;
|
||||
}
|
||||
warn!(
|
||||
first_role,
|
||||
msg_count = messages.len(),
|
||||
"ensure_leading_user: 首条非 user/system,补 user 占位(保留上下文,防 OpenAI 首条 assistant/tool 非法)"
|
||||
);
|
||||
messages.insert(
|
||||
0,
|
||||
OpenAiMessage {
|
||||
role: "user".into(),
|
||||
content: serde_json::Value::String("(continued from previous context)".into()),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
reasoning_content: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析同步响应中的工具调用
|
||||
fn parse_tool_calls(calls: Vec<OpenAiToolCallResp>) -> Vec<ToolCall> {
|
||||
calls
|
||||
@@ -594,4 +630,53 @@ mod tests {
|
||||
let msg = &out.messages[0];
|
||||
assert_eq!(msg.content, serde_json::Value::String("hello".into()));
|
||||
}
|
||||
|
||||
// ---------- B-260626-01: ensure_leading_user(首条非 user/system → 补 user 占位,OpenAI 对称 Anthropic)----------
|
||||
|
||||
/// B-260626-01: 首条 assistant → 补 user 占位(对齐 Anthropic)。上游绕过 sanitize 的
|
||||
/// 调用方(title/knowledge_inject/工作流节点)可能传入首条 assistant 序列,补占位保留上下文。
|
||||
#[test]
|
||||
fn openai_ensure_leading_user_first_assistant_gets_placeholder() {
|
||||
let provider = OpenAICompatProvider::new("https://api.openai.com", "k", "gpt-4o");
|
||||
let req = CompletionRequest {
|
||||
model: "gpt-4o".into(),
|
||||
messages: vec![
|
||||
ChatMessage::assistant("我来帮你"),
|
||||
ChatMessage::user("继续"),
|
||||
],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let out = provider.convert_request(req);
|
||||
assert_eq!(out.messages.len(), 3, "占位 + 原 2 条");
|
||||
assert_eq!(out.messages[0].role.as_str(), "user", "首条应为 user(补占位)");
|
||||
assert_eq!(out.messages[1].role.as_str(), "assistant");
|
||||
assert_eq!(out.messages[2].role.as_str(), "user");
|
||||
}
|
||||
|
||||
/// B-260626-01: 正常序列(user 开头)不补占位——零回归。
|
||||
#[test]
|
||||
fn openai_ensure_leading_user_normal_unchanged() {
|
||||
let provider = OpenAICompatProvider::new("https://api.openai.com", "k", "gpt-4o");
|
||||
let req = CompletionRequest {
|
||||
model: "gpt-4o".into(),
|
||||
messages: vec![
|
||||
ChatMessage::user("hello"),
|
||||
ChatMessage::assistant("hi"),
|
||||
],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let out = provider.convert_request(req);
|
||||
assert_eq!(out.messages.len(), 2, "正常序列不补占位");
|
||||
assert_eq!(out.messages[0].role.as_str(), "user");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user