修复: 协议层与体验缺陷六件
- temperature 按模型适配,仅收 1 的模型不再 400 - 流式 error 帧不再被 choices 必填整帧丢弃 - 只读缓存写后失效 - git diff 中文截断撞 UTF-8 边界 panic - 非视觉模型纯图片直接拒绝 - 全局快捷键补输入焦点守卫
This commit is contained in:
@@ -274,6 +274,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
usage: usage_accum.take(),
|
||||
error: None,
|
||||
reasoning_content: None,
|
||||
truncated_by_max_tokens: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -296,6 +297,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
usage: None,
|
||||
error: Some(msg),
|
||||
reasoning_content: None,
|
||||
truncated_by_max_tokens: false,
|
||||
};
|
||||
}
|
||||
// 提取 usage(带 include_usage 时末段 chunk 携带,覆盖累积)
|
||||
@@ -334,6 +336,9 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
choice.finish_reason.as_deref(),
|
||||
Some("stop") | Some("tool_calls") | Some("length") | Some("content_filter")
|
||||
);
|
||||
// length = 输出被 max_tokens 截断:仍按正常终止收敛(finished=true),
|
||||
// 但截断标志随 chunk 带出,消费方据此把该轮产出标记截断,不冒充完整。
|
||||
let truncated_by_max_tokens = choice.finish_reason.as_deref() == Some("length");
|
||||
|
||||
let tool_calls = choice.delta.tool_calls.map(|tcs| {
|
||||
// 缺 index 的增量缺省 0(单工具调用语义等价);但同帧多 call 且 index
|
||||
@@ -379,6 +384,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
usage: usage_accum.clone(),
|
||||
error: None,
|
||||
reasoning_content: choice.delta.reasoning_content,
|
||||
truncated_by_max_tokens,
|
||||
}
|
||||
} else {
|
||||
// choices 为空 = usage-only chunk,不输出文本(usage 已累积),usage 一并带出
|
||||
@@ -389,6 +395,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
usage: usage_accum.clone(),
|
||||
error: None,
|
||||
reasoning_content: None,
|
||||
truncated_by_max_tokens: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,6 +408,7 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
usage: None,
|
||||
error: None,
|
||||
reasoning_content: None,
|
||||
truncated_by_max_tokens: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,4 +511,32 @@ mod tests {
|
||||
);
|
||||
assert!(!chunk2.finished, "无终态 finish_reason 的普通 delta 不应终结");
|
||||
}
|
||||
|
||||
/// finish_reason=length:max_tokens 截断仍按正常终止收敛(finished=true),
|
||||
/// 但截断标志必须随 chunk 带出(零信号会让腰斩报告冒充完整完成);
|
||||
/// stop 等正常终态不置标志。
|
||||
#[test]
|
||||
fn finish_reason_length_carries_truncation_flag() {
|
||||
let mut accum = None;
|
||||
let chunk = apply_openai_sse(
|
||||
r#"{"choices":[{"delta":{"content":"报告写到一半"},"finish_reason":"length"}]}"#,
|
||||
&mut accum,
|
||||
);
|
||||
assert!(chunk.finished, "length 属正常终止");
|
||||
assert!(chunk.truncated_by_max_tokens, "length 必须带截断标志");
|
||||
|
||||
let mut accum2 = None;
|
||||
let chunk2 = apply_openai_sse(
|
||||
r#"{"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}"#,
|
||||
&mut accum2,
|
||||
);
|
||||
assert!(chunk2.finished && !chunk2.truncated_by_max_tokens, "stop 不置截断标志");
|
||||
|
||||
let mut accum3 = None;
|
||||
let chunk3 = apply_openai_sse(
|
||||
r#"{"choices":[{"delta":{"content":"x"},"finish_reason":null}]}"#,
|
||||
&mut accum3,
|
||||
);
|
||||
assert!(!chunk3.truncated_by_max_tokens, "普通 delta 不置截断标志");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user