mirror of
https://github.com/deepseek-ai/deepseek-recipe.git
synced 2026-10-28 17:17:08 +08:00
Merge 444e05949df6e1c656b1828632a771b4c28c14fd into 8cadfede7063c896b944e7bae05daa3549ae97ea
This commit is contained in:
commit
e227949bfc
@ -222,9 +222,17 @@ impl StashedChunks {
|
|||||||
OutputAction::Reasoning => {
|
OutputAction::Reasoning => {
|
||||||
non_empty(front_chunk).map(|content| OutputChunk::Reasoning { content })
|
non_empty(front_chunk).map(|content| OutputChunk::Reasoning { content })
|
||||||
}
|
}
|
||||||
OutputAction::ToSpace => Some(OutputChunk::Raw {
|
OutputAction::ToSpace => {
|
||||||
content: " ".to_string(),
|
// 与 Label/ToolCall* 相同的去重:一段连续替换的文本无论被后端
|
||||||
}),
|
// 切成多少个 chunk,都只产出等宽的一处空白,输出不依赖分块边界。
|
||||||
|
if last_pop_action != OutputAction::ToSpace {
|
||||||
|
Some(OutputChunk::Raw {
|
||||||
|
content: " ".to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
OutputAction::Skip | OutputAction::SkipInvalid { .. } => None,
|
OutputAction::Skip | OutputAction::SkipInvalid { .. } => None,
|
||||||
OutputAction::StopSequence => {
|
OutputAction::StopSequence => {
|
||||||
let content = front_chunk.unwrap_or_default();
|
let content = front_chunk.unwrap_or_default();
|
||||||
@ -367,3 +375,55 @@ fn escape_json_string(content: String) -> String {
|
|||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tospace_tests {
|
||||||
|
use super::*;
|
||||||
|
/// 直接收集 OutputChunk 的最小生成器,便于在无协议层时驱动流处理器。
|
||||||
|
struct Collector;
|
||||||
|
|
||||||
|
impl ChunkGenerator for Collector {
|
||||||
|
type Chunk = OutputChunk;
|
||||||
|
|
||||||
|
async fn generate(&mut self, chunk: OutputChunk) -> Vec<OutputChunk> {
|
||||||
|
vec![chunk]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_raw_content(parts: &[&str]) -> String {
|
||||||
|
let inference = tokio_stream::iter(parts.iter().map(|content| InferenceChunk::Text {
|
||||||
|
content: (*content).to_string(),
|
||||||
|
content_tokens: 1,
|
||||||
|
}));
|
||||||
|
let processor = StreamProcessor::new(
|
||||||
|
Collector,
|
||||||
|
ParsingOptions {
|
||||||
|
parse_json_output: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut content = String::new();
|
||||||
|
let stream = processor.process(inference);
|
||||||
|
tokio::pin!(stream);
|
||||||
|
while let Some(event) = stream.next().await {
|
||||||
|
match event {
|
||||||
|
Ok(OutputChunk::Raw { content: text }) => content += &text,
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => panic!("stream error: {error:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON 模式下被替换为空白的文本段:无论后端如何分块,输出必须一致
|
||||||
|
/// (每个连续替换段恰好一个空格,而不是每个 chunk 一个空格)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tospace_output_is_chunk_boundary_independent() {
|
||||||
|
let whole = collect_raw_content(&["aaaaabbbb```json\n{}"]).await;
|
||||||
|
let split = collect_raw_content(&["aaaaa", "bbbb```json\n{}"]).await;
|
||||||
|
assert_eq!(whole, split);
|
||||||
|
assert!(whole.contains(' '));
|
||||||
|
assert!(!whole.contains('a'));
|
||||||
|
assert!(!whole.contains('b'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user