mirror of
https://github.com/deepseek-ai/deepseek-recipe.git
synced 2026-10-28 17:17:08 +08:00
Merge f835279dd53472bf715433be2c8b4bb4d1509bbf into 8cadfede7063c896b944e7bae05daa3549ae97ea
This commit is contained in:
commit
6291cb7b43
@ -68,4 +68,25 @@ impl StreamDecoder {
|
|||||||
self.ids.clear();
|
self.ids.clear();
|
||||||
Ok(Some((content, std::mem::take(&mut self.pending))))
|
Ok(Some((content, std::mem::take(&mut self.pending))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flush any still-buffered ids at the end of the stream.
|
||||||
|
///
|
||||||
|
/// The incompleteness heuristic cannot distinguish a multi-token character
|
||||||
|
/// still split across tokens from decoded text that legitimately ends with
|
||||||
|
/// U+FFFD (mojibake bytes decode to a real replacement character). At
|
||||||
|
/// end-of-stream there is no further token to disambiguate, so whatever
|
||||||
|
/// remains is emitted as-is — a genuinely truncated sequence yields its
|
||||||
|
/// replacement character, a legitimate one is no longer dropped.
|
||||||
|
pub fn finish(&mut self) -> Option<(String, usize)> {
|
||||||
|
if self.ids.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let content = self
|
||||||
|
.decoder
|
||||||
|
.decode_ids(&self.ids, false)
|
||||||
|
.ok()
|
||||||
|
.filter(|content| !content.is_empty())?;
|
||||||
|
self.ids.clear();
|
||||||
|
Some((content, std::mem::take(&mut self.pending)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,6 +140,19 @@ where
|
|||||||
yield Ok(out);
|
yield Ok(out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 流结束时冲刷仍在缓冲的 token:尾部的替换字符(真实 U+FFFD 文本)
|
||||||
|
// 或截断序列不应从输出与用量里消失。
|
||||||
|
if let Some(decoder) = decoder.as_mut() {
|
||||||
|
if let Some((content, content_tokens)) = decoder.finish() {
|
||||||
|
completion_usage.completion_tokens += content_tokens;
|
||||||
|
let actions = state_machine.feed(&content);
|
||||||
|
stashed.push(Some(content));
|
||||||
|
for out in stashed.apply_actions(actions, &mut generator).await {
|
||||||
|
yield Ok(out);
|
||||||
|
}
|
||||||
|
stop_sequence = stashed.take_stop_sequence();
|
||||||
|
}
|
||||||
|
}
|
||||||
let actions = state_machine.finish();
|
let actions = state_machine.finish();
|
||||||
for out in stashed.apply_actions(actions, &mut generator).await {
|
for out in stashed.apply_actions(actions, &mut generator).await {
|
||||||
yield Ok(out);
|
yield Ok(out);
|
||||||
@ -367,3 +380,66 @@ fn escape_json_string(content: String) -> String {
|
|||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod decoder_flush_tests {
|
||||||
|
use super::super::decoder::TokenizerDecoder;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct ScriptedDecoder;
|
||||||
|
|
||||||
|
/// id 0 -> "a";id 1 -> U+FFFD(真实文本里的替换字符,非多字节截断)。
|
||||||
|
impl TokenizerDecoder for ScriptedDecoder {
|
||||||
|
fn decode_ids(&self, ids: &[u32], _skip_special_tokens: bool) -> Result<String, String> {
|
||||||
|
Ok(ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| match id {
|
||||||
|
0 => "a",
|
||||||
|
_ => "\u{FFFD}",
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Collector;
|
||||||
|
|
||||||
|
impl ChunkGenerator for Collector {
|
||||||
|
type Chunk = OutputChunk;
|
||||||
|
|
||||||
|
async fn generate(&mut self, chunk: OutputChunk) -> Vec<OutputChunk> {
|
||||||
|
vec![chunk]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 流末尾缓冲的 token 必须冲刷:结尾恰好是真实 U+FFFD 的文本不能
|
||||||
|
/// 从输出与 completion 用量里消失(此前启发式把它当作未完成字符而丢弃)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn trailing_replacement_character_is_flushed_at_end_of_stream() {
|
||||||
|
let inference = tokio_stream::iter(vec![
|
||||||
|
InferenceChunk::Token { token_id: 0 },
|
||||||
|
InferenceChunk::Token { token_id: 1 },
|
||||||
|
InferenceChunk::Finish {
|
||||||
|
finish_reason: InferenceFinishReason::Stop,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let processor = StreamProcessor::new(Collector, ParsingOptions::default())
|
||||||
|
.with_tokenizer(ScriptedDecoder);
|
||||||
|
let mut content = String::new();
|
||||||
|
let mut usage = None;
|
||||||
|
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(OutputChunk::Finish {
|
||||||
|
usage: finish_usage,
|
||||||
|
..
|
||||||
|
}) => usage = Some(finish_usage),
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => panic!("stream error: {error:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(content, "a\u{FFFD}");
|
||||||
|
assert_eq!(usage.expect("finish event").completion_tokens, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user