修复: OpenAI SSE error 识别 + 条件引擎 warn + MidStream stop_flag 优先 + 双窗口 listener 时序加固
This commit is contained in:
@@ -231,6 +231,49 @@ impl OpenAICompatProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成 messages 诊断摘要(每条 role + content 形态 + tool 标记),不含敏感数据。
|
||||
/// 流中途 error 时附摘要定位哪条非法(对齐 `AnthropicCompatProvider::summarize_messages`)。
|
||||
fn summarize_openai_messages(messages: &[OpenAiMessage]) -> String {
|
||||
let lines: Vec<String> = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let role = m.role.as_str();
|
||||
let desc = match &m.content {
|
||||
serde_json::Value::String(s) => format!("text({}B)", s.len()),
|
||||
serde_json::Value::Array(blocks) => {
|
||||
let parts: Vec<String> = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let ty = b.get("type").and_then(|t| t.as_str()).unwrap_or("?");
|
||||
match ty {
|
||||
"text" => format!(
|
||||
"text({}B)",
|
||||
b.get("text")
|
||||
.and_then(|t| t.as_str())
|
||||
.map(|s| s.len())
|
||||
.unwrap_or(0)
|
||||
),
|
||||
"image_url" => "image".to_string(),
|
||||
_ => ty.to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("[{}]", parts.join(","))
|
||||
}
|
||||
_ => "?".to_string(),
|
||||
};
|
||||
let tool_mark = match (&m.tool_calls, &m.tool_call_id) {
|
||||
(Some(tcs), _) => format!(" tool_calls={}", tcs.len()),
|
||||
(None, Some(tid)) => format!(" tool_result[tid={}]", tid),
|
||||
(None, None) => String::new(),
|
||||
};
|
||||
format!("#{}:{} {}{}", i, role, desc, tool_mark)
|
||||
})
|
||||
.collect();
|
||||
format!("{} msgs: {}", lines.len(), lines.join(" | "))
|
||||
}
|
||||
|
||||
/// 保证 messages 首条为 user/system(OpenAI 协议要求首条非 assistant/tool)。
|
||||
///
|
||||
/// 对齐 `AnthropicCompatProvider::ensure_leading_user`。上游绕过 `ContextManager::sanitize_messages`
|
||||
@@ -419,6 +462,9 @@ impl LlmProvider for OpenAICompatProvider {
|
||||
// (严格 UTF-8 + SSE 协议校验,跨 chunk 字符/不完整事件均报错且不可恢复)。
|
||||
// 原生解析器:bytes 累积 + from_utf8_lossy 宽松处理 + \n\n 分隔,容错不中断流。
|
||||
let mut last_usage: Option<TokenUsage> = None;
|
||||
// MidStream error(中转站按 OpenAI 协议在流中途发 error 帧)时附 messages 摘要定位哪条非法
|
||||
// (对齐 anthropic_compat 672)。
|
||||
let messages_summary = Self::summarize_openai_messages(&openai_req.messages);
|
||||
|
||||
let sse = crate::sse_parser::SseStream::new(resp.bytes_stream());
|
||||
let stream = sse.flat_map(move |result: Result<Vec<String>, String>| {
|
||||
@@ -426,7 +472,10 @@ impl LlmProvider for OpenAICompatProvider {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
for data in events {
|
||||
let chunk = apply_openai_sse(&data, &mut last_usage);
|
||||
let mut chunk = apply_openai_sse(&data, &mut last_usage);
|
||||
if let Some(err) = chunk.error.as_mut() {
|
||||
*err = format!("{} | messages 摘要: {}", err, messages_summary);
|
||||
}
|
||||
chunks.push(Ok(chunk));
|
||||
}
|
||||
}
|
||||
@@ -616,6 +665,38 @@ mod tests {
|
||||
assert!(!c.finished);
|
||||
}
|
||||
|
||||
/// 流中途 error 事件 → error 为 Some(msg),finished=false(避免残缺被当正常完成入库),不污染 usage 累加
|
||||
#[test]
|
||||
fn openai_sse_midstream_error_event() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
// 先累积一段 usage,验证 error 分支不污染累加器
|
||||
apply_openai_sse(&usage_only_chunk(10, 20), &mut acc);
|
||||
|
||||
let data = r#"{"choices":[],"error":{"message":"context length exceeded","type":"invalid_request_error"}}"#;
|
||||
let c = apply_openai_sse(data, &mut acc);
|
||||
assert!(!c.finished, "error 帧不应走 finished 完成路径");
|
||||
assert_eq!(c.delta, "");
|
||||
assert!(c.tool_calls.is_none());
|
||||
assert!(c.usage.is_none(), "error 帧不应带出 usage");
|
||||
let err = c.error.expect("error 帧应映射为 Some(msg)");
|
||||
assert_eq!(err, "context length exceeded");
|
||||
// 累加器保持原值(未被覆盖/清空)
|
||||
let acc = acc.expect("累加器应保留先前 usage 不受 error 影响");
|
||||
assert_eq!(acc.prompt_tokens, 10);
|
||||
assert_eq!(acc.completion_tokens, 20);
|
||||
}
|
||||
|
||||
/// error 无 message 字段 → 兜底 "stream error" 字符串
|
||||
#[test]
|
||||
fn openai_sse_midstream_error_without_message_falls_back() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
// error 形态异常(只有 type,无 message)
|
||||
let data = r#"{"choices":[],"error":{"type":"server_error"}}"#;
|
||||
let c = apply_openai_sse(data, &mut acc);
|
||||
assert!(!c.finished);
|
||||
assert_eq!(c.error.as_deref(), Some("stream error"), "无 message 字段应兜底");
|
||||
}
|
||||
|
||||
// ---------- 多模态 convert_request ----------
|
||||
|
||||
/// 含图消息 → content 数组(text + image_url data URI);纯文本 → 字符串简写
|
||||
|
||||
Reference in New Issue
Block a user