1use std::collections::HashMap;
4use std::sync::OnceLock;
5
6mod base_parser;
7mod gemma4_parser;
8mod gpt_oss_parser;
9mod granite_parser;
10mod inkling_parser;
11mod minimax_append_think_parser;
12
13pub(crate) use crate::tool_calling::config::MINIMAX_M3_TOOL_NAMESPACE;
15pub use base_parser::BasicReasoningParser;
16pub use gemma4_parser::Gemma4ReasoningParser;
17pub use gpt_oss_parser::{GptOssReasoningParser, harmony_terminator_token_ids};
18pub use granite_parser::GraniteReasoningParser;
19pub use inkling_parser::InklingReasoningParser;
20pub use minimax_append_think_parser::MiniMaxAppendThinkParser;
21
22pub(crate) const KIMI_K2_TOOL_SECTION_BEGIN: &str = "<|tool_calls_section_begin|>";
26pub(crate) const DEEPSEEK_TOOL_BLOCK_BEGIN: &str = "<|DSML|tool_calls>";
27pub(crate) const DEEPSEEK_TOOL_INVOKE_BEGIN: &str = "<|DSML|invoke name=";
28
29static REASONING_PARSER_MAP: OnceLock<HashMap<&'static str, ReasoningParserType>> = OnceLock::new();
30
31fn get_reasoning_parser_map() -> &'static HashMap<&'static str, ReasoningParserType> {
33 REASONING_PARSER_MAP.get_or_init(|| {
34 let mut map = HashMap::new();
35 map.insert("deepseek_r1", ReasoningParserType::DeepseekR1);
36 map.insert("deepseek_v3", ReasoningParserType::DeepseekR1);
42 map.insert("deepseek_v3_1", ReasoningParserType::DeepseekR1);
43 map.insert("deepseek_v3_2", ReasoningParserType::DeepseekR1);
44 map.insert("basic", ReasoningParserType::Basic);
45 map.insert("gpt_oss", ReasoningParserType::GptOss);
46 map.insert("qwen3", ReasoningParserType::Qwen);
47 map.insert("deepseek_v4", ReasoningParserType::DeepSeekV4);
61 map.insert("deepseek-v4", ReasoningParserType::DeepSeekV4);
62 map.insert("deepseekv4", ReasoningParserType::DeepSeekV4);
63 map.insert("nemotron_deci", ReasoningParserType::NemotronDeci);
64 map.insert("kimi", ReasoningParserType::Kimi);
65 map.insert("kimi_k25", ReasoningParserType::KimiK25);
66 map.insert("kimi_k3", ReasoningParserType::KimiK3);
70 map.insert("kimi-k3", ReasoningParserType::KimiK3);
71 map.insert("step3", ReasoningParserType::Step3);
72 map.insert("mistral", ReasoningParserType::Mistral);
73 map.insert("granite", ReasoningParserType::Granite);
74 map.insert("nemotron_nano", ReasoningParserType::DeepseekR1); map.insert("nemotron3", ReasoningParserType::DeepseekR1);
76 map.insert("nemotron_v3", ReasoningParserType::DeepseekR1);
77 map.insert("glm45", ReasoningParserType::NemotronDeci); map.insert(
79 "minimax_append_think",
80 ReasoningParserType::MiniMaxAppendThink,
81 );
82 map.insert("minimax_m2", ReasoningParserType::MiniMaxM2);
83 map.insert("minimax_m3", ReasoningParserType::MiniMaxM3);
87 map.insert("minimax-m3", ReasoningParserType::MiniMaxM3);
88 map.insert("gemma4", ReasoningParserType::Gemma4);
92 map.insert("gemma-4", ReasoningParserType::Gemma4);
93 map.insert("inkling", ReasoningParserType::Inkling);
95 map
96 })
97}
98
99pub fn get_available_reasoning_parsers() -> Vec<&'static str> {
101 get_reasoning_parser_map().keys().copied().collect()
102}
103
104#[derive(Debug, Clone, Default)]
105pub struct ParserResult {
106 pub normal_text: String,
108
109 pub reasoning_text: String,
111}
112
113impl ParserResult {
114 pub fn get_some_reasoning(&self) -> Option<String> {
115 if self.reasoning_text.is_empty() {
116 None
117 } else {
118 Some(self.reasoning_text.clone())
119 }
120 }
121
122 pub fn get_some_normal_text(&self) -> Option<String> {
123 if self.normal_text.is_empty() {
124 None
125 } else {
126 Some(self.normal_text.clone())
127 }
128 }
129}
130
131pub trait ReasoningParser: Send + std::fmt::Debug {
132 fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult;
136
137 fn parse_reasoning_streaming_incremental(
141 &mut self,
142 text: &str,
143 token_ids: &[u32],
144 ) -> ParserResult;
145
146 fn finish_reasoning_stream(&mut self) -> ParserResult {
154 ParserResult::default()
155 }
156
157 fn set_in_reasoning(&mut self, _in_reasoning: bool) {
162 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum ReasoningParserType {
169 DeepseekR1,
170 Step3,
171 Basic,
172 GptOss,
173 Qwen,
174 DeepSeekV4,
182 NemotronDeci,
183 Kimi,
184 KimiK25,
185 KimiK3,
188 Mistral,
189 Granite,
190 MiniMaxAppendThink,
191 MiniMaxM2,
194 MiniMaxM3,
197 Gemma4,
200 Inkling,
203}
204
205#[derive(std::fmt::Debug)]
206pub struct ReasoningParserWrapper {
207 parser: Box<dyn ReasoningParser>,
208}
209
210impl ReasoningParser for ReasoningParserWrapper {
211 fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult {
212 self.parser.detect_and_parse_reasoning(text, token_ids)
213 }
214
215 fn parse_reasoning_streaming_incremental(
216 &mut self,
217 text: &str,
218 token_ids: &[u32],
219 ) -> ParserResult {
220 self.parser
221 .parse_reasoning_streaming_incremental(text, token_ids)
222 }
223
224 fn finish_reasoning_stream(&mut self) -> ParserResult {
225 self.parser.finish_reasoning_stream()
226 }
227
228 fn set_in_reasoning(&mut self, in_reasoning: bool) {
229 self.parser.set_in_reasoning(in_reasoning)
230 }
231}
232
233impl ReasoningParserType {
234 pub fn get_reasoning_parser(self) -> ReasoningParserWrapper {
235 let basic_parser =
236 BasicReasoningParser::new("<think>".into(), "</think>".into(), false, true);
237 let force_reasoning_basic_parser =
238 BasicReasoningParser::new("<think>".into(), "</think>".into(), true, true);
239 match self {
240 ReasoningParserType::DeepseekR1 => ReasoningParserWrapper {
241 parser: Box::new(force_reasoning_basic_parser),
242 },
243 ReasoningParserType::Step3 => ReasoningParserWrapper {
244 parser: Box::new(force_reasoning_basic_parser),
245 },
246 ReasoningParserType::Basic => ReasoningParserWrapper {
247 parser: Box::new(basic_parser),
248 },
249 ReasoningParserType::Qwen => ReasoningParserWrapper {
250 parser: Box::new(basic_parser),
251 },
252 ReasoningParserType::DeepSeekV4 => ReasoningParserWrapper {
256 parser: Box::new(
257 BasicReasoningParser::new("<think>".into(), "</think>".into(), false, true)
258 .with_tool_start_token(DEEPSEEK_TOOL_BLOCK_BEGIN)
259 .with_tool_start_token(DEEPSEEK_TOOL_INVOKE_BEGIN),
260 ),
261 },
262 ReasoningParserType::NemotronDeci => ReasoningParserWrapper {
263 parser: Box::new(basic_parser),
264 },
265 ReasoningParserType::Kimi => ReasoningParserWrapper {
266 parser: Box::new(BasicReasoningParser::new(
267 "◁think▷".into(),
268 "◁/think▷".into(),
269 false,
270 true,
271 )),
272 },
273 ReasoningParserType::KimiK25 => ReasoningParserWrapper {
274 parser: Box::new(
275 BasicReasoningParser::new("<think>".into(), "</think>".into(), true, true)
276 .with_tool_start_token(KIMI_K2_TOOL_SECTION_BEGIN),
277 ),
278 },
279 ReasoningParserType::KimiK3 => {
280 let mut parser = BasicReasoningParser::new(
281 "<|open|>think<|sep|>".into(),
282 "<|close|>think<|sep|>".into(),
283 false,
284 true,
285 );
286 parser = parser
291 .with_tool_start_token("<|open|>")
292 .with_tool_start_token("<|close|>")
293 .with_tool_start_token("<|end_of_msg|>");
294 ReasoningParserWrapper {
295 parser: Box::new(
296 parser
297 .with_single_char_marker_buffering()
301 .with_dangling_end_recovery(),
303 ),
304 }
305 }
306 ReasoningParserType::Mistral => ReasoningParserWrapper {
307 parser: Box::new(BasicReasoningParser::new(
308 "[THINK]".into(),
309 "[/THINK]".into(),
310 true,
311 true,
312 )),
313 },
314 ReasoningParserType::GptOss => match GptOssReasoningParser::new() {
315 Ok(parser) => ReasoningParserWrapper {
316 parser: Box::new(parser),
317 },
318 Err(e) => {
319 tracing::warn!(
320 "GptOssReasoningParser could not be initialized, falling back to Basic Reasoning Parser: {e}"
321 );
322 ReasoningParserWrapper {
323 parser: Box::new(BasicReasoningParser::new(
324 "<think>".into(),
325 "</think>".into(),
326 false,
327 true,
328 )),
329 }
330 }
331 },
332 ReasoningParserType::Granite => ReasoningParserWrapper {
333 parser: Box::new(GraniteReasoningParser::new()),
334 },
335 ReasoningParserType::MiniMaxAppendThink => ReasoningParserWrapper {
336 parser: Box::new(MiniMaxAppendThinkParser::new()),
337 },
338 ReasoningParserType::MiniMaxM2 => ReasoningParserWrapper {
339 parser: Box::new(
340 BasicReasoningParser::new("<think>".into(), "</think>".into(), true, true)
341 .with_tool_start_token("<minimax:tool_call>")
342 .with_tool_start_token("<invoke name="),
343 ),
344 },
345 ReasoningParserType::MiniMaxM3 => ReasoningParserWrapper {
346 parser: Box::new(
347 BasicReasoningParser::new(
348 "<mm:think>".into(),
349 "</mm:think>".into(),
350 false,
351 true,
352 )
353 .with_dangling_end_recovery()
354 .with_implicit_tool_start_recovery()
355 .with_tool_start_token(MINIMAX_M3_TOOL_NAMESPACE),
357 ),
358 },
359 ReasoningParserType::Gemma4 => ReasoningParserWrapper {
360 parser: Box::new(Gemma4ReasoningParser::new()),
361 },
362 ReasoningParserType::Inkling => ReasoningParserWrapper {
363 parser: Box::new(InklingReasoningParser::new()),
364 },
365 }
366 }
367
368 pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper {
369 tracing::debug!("Selected reasoning parser: {}", name);
370
371 let parser_map = get_reasoning_parser_map();
372 let normalized_name = name.to_lowercase();
373
374 match parser_map.get(normalized_name.as_str()) {
375 Some(parser_type) => parser_type.get_reasoning_parser(),
376 None => {
377 tracing::warn!(
378 parser_name = name,
379 "Unknown reasoning parser type, falling back to Basic Reasoning Parser",
380 );
381 Self::Basic.get_reasoning_parser()
382 }
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test] fn test_get_available_reasoning_parsers() {
393 let parsers = get_available_reasoning_parsers();
394 assert!(!parsers.is_empty());
395 let available_parsers = [
397 "deepseek_r1",
398 "deepseek_v3",
399 "deepseek_v3_1",
400 "deepseek_v3_2",
401 "basic",
402 "gpt_oss",
403 "qwen3",
404 "deepseek_v4",
405 "deepseek-v4",
406 "deepseekv4",
407 "nemotron_deci",
408 "kimi",
409 "kimi_k25",
410 "kimi_k3",
411 "kimi-k3",
412 "step3",
413 "mistral",
414 "granite",
415 "nemotron_nano",
416 "nemotron3",
417 "nemotron_v3",
418 "glm45",
419 "minimax_append_think",
420 "minimax_m2",
421 "minimax_m3",
422 "minimax-m3",
423 "gemma4",
424 "gemma-4",
425 "inkling",
426 ];
427 for parser in available_parsers {
428 assert!(parsers.contains(&parser));
429 }
430 }
431
432 #[test]
433 fn test_deepseek_v4_detect_and_parse_exits_reasoning_on_dsml_tool_start() {
434 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
435 let result = parser.detect_and_parse_reasoning(
436 &format!(
437 "<think>need a tool{DEEPSEEK_TOOL_BLOCK_BEGIN}\n<|DSML|invoke name=\"bash\">"
438 ),
439 &[],
440 );
441
442 assert_eq!(result.reasoning_text, "need a tool");
443 assert_eq!(
444 result.normal_text,
445 "<|DSML|tool_calls>\n<|DSML|invoke name=\"bash\">"
446 );
447 }
448
449 #[test]
450 fn test_deepseek_v4_detect_and_parse_exits_reasoning_without_visible_opener_on_dsml_tool_start()
451 {
452 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
453 parser.set_in_reasoning(true);
454
455 let result = parser.detect_and_parse_reasoning(
456 &format!(
457 "What is the weather now?{DEEPSEEK_TOOL_BLOCK_BEGIN}\n<|DSML|invoke name=\"get_weather\">"
458 ),
459 &[],
460 );
461
462 assert_eq!(result.reasoning_text, "What is the weather now?");
463 assert_eq!(
464 result.normal_text,
465 "<|DSML|tool_calls>\n<|DSML|invoke name=\"get_weather\">"
466 );
467 }
468
469 #[test]
470 fn test_deepseek_v4_detect_and_parse_exits_reasoning_on_bare_dsml_invoke() {
471 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
472 let tool_call = format!(
473 "{DEEPSEEK_TOOL_INVOKE_BEGIN}\"bash\">\n<|DSML|parameter name=\"cmd\" string=\"true\">pwd</|DSML|parameter>\n</|DSML|invoke>"
474 );
475 let result =
476 parser.detect_and_parse_reasoning(&format!("<think>need a tool{tool_call}"), &[]);
477
478 assert_eq!(result.reasoning_text, "need a tool");
479 assert_eq!(result.normal_text, tool_call);
480 }
481
482 #[test]
483 fn test_deepseek_v4_detect_and_parse_keeps_partial_dsml_tool_prefix_at_eof() {
484 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
485 let result = parser.detect_and_parse_reasoning("<think>need a tool<|DSML|tool_", &[]);
486
487 assert_eq!(result.reasoning_text, "need a tool<|DSML|tool_");
488 assert_eq!(result.normal_text, "");
489 }
490
491 #[test]
492 fn test_deepseek_v4_streaming_exits_reasoning_on_split_dsml_tool_start() {
493 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
494 parser.set_in_reasoning(true);
495
496 let chunks = ["need a tool", "<", "|DSML|tool_calls>\nX"];
497 let mut reasoning = String::new();
498 let mut normal = String::new();
499
500 for chunk in chunks {
501 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
502 reasoning.push_str(&result.reasoning_text);
503 normal.push_str(&result.normal_text);
504 }
505
506 assert_eq!(reasoning, "need a tool");
507 assert_eq!(normal, "<|DSML|tool_calls>\nX");
508 }
509
510 #[test]
511 fn test_deepseek_v4_streaming_exits_reasoning_on_split_dsml_invoke() {
512 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
513 parser.set_in_reasoning(true);
514
515 let chunks = ["need a tool", "<|DSML|inv", "oke name=\"bash\">\nX"];
516 let mut reasoning = String::new();
517 let mut normal = String::new();
518
519 for chunk in chunks {
520 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
521 reasoning.push_str(&result.reasoning_text);
522 normal.push_str(&result.normal_text);
523 }
524
525 assert_eq!(reasoning, "need a tool");
526 assert_eq!(normal, "<|DSML|invoke name=\"bash\">\nX");
527 }
528
529 #[test]
530 fn test_deepseek_v4_streaming_keeps_non_tool_dsml_prefix_in_reasoning() {
531 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
532 parser.set_in_reasoning(true);
533
534 let chunks = ["literal", "<|DSML|", "not_a_tool"];
535 let mut reasoning = String::new();
536 let mut normal = String::new();
537
538 for chunk in chunks {
539 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
540 reasoning.push_str(&result.reasoning_text);
541 normal.push_str(&result.normal_text);
542 }
543
544 assert_eq!(reasoning, "literal<|DSML|not_a_tool");
545 assert_eq!(normal, "");
546 }
547
548 #[test]
549 fn test_deepseek_v4_streaming_does_not_drop_non_dsml_lone_angle() {
550 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
551 parser.set_in_reasoning(true);
552
553 let chunks = ["literal", "<", "not dsml"];
554 let mut reasoning = String::new();
555 let mut normal = String::new();
556
557 for chunk in chunks {
558 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
559 reasoning.push_str(&result.reasoning_text);
560 normal.push_str(&result.normal_text);
561 }
562
563 assert_eq!(reasoning, "literal<not dsml");
564 assert_eq!(normal, "");
565 }
566
567 #[test] fn test_inkling_detect_and_parse() {
569 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("inkling");
570 let result = parser.detect_and_parse_reasoning(
571 "<|message_model|><|content_thinking|>thinking<|end_message|><|message_model|><|content_text|>answer<|end_message|><|content_model_end_sampling|>",
572 &[],
573 );
574 assert_eq!(result.reasoning_text, "thinking");
575 assert_eq!(result.normal_text, "answer");
576 }
577
578 #[test] fn test_inkling_preserves_tool_block() {
580 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("inkling");
581 let result = parser.detect_and_parse_reasoning(
582 r#"<|message_model|><|content_thinking|>use weather</a><|end_message|><|message_model|>get_weather<|content_invoke_tool_json|>{"name":"get_weather","args":{"location":"Paris"}}<|end_message|>"#,
583 &[],
584 );
585 assert_eq!(result.reasoning_text, "use weather</a>");
586 assert_eq!(
587 result.normal_text,
588 r#"<|message_model|>get_weather<|content_invoke_tool_json|>{"name":"get_weather","args":{"location":"Paris"}}<|end_message|>"#
589 );
590 }
591
592 #[test] fn test_minimax_m2_force_reasoning_and_tool_transition() {
594 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m2");
595 let result = parser.detect_and_parse_reasoning(
596 "I should call weather.</think><minimax:tool_call><invoke name=\"get_weather\"></invoke></minimax:tool_call>",
597 &[],
598 );
599
600 assert_eq!(result.reasoning_text, "I should call weather.");
601 assert_eq!(
602 result.normal_text,
603 "<minimax:tool_call><invoke name=\"get_weather\"></invoke></minimax:tool_call>"
604 );
605 }
606
607 #[test] fn test_minimax_m2_tool_start_exits_force_reasoning() {
609 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m2");
610 let tool_call =
611 "<minimax:tool_call><invoke name=\"get_weather\"></invoke></minimax:tool_call>";
612 let result = parser.detect_and_parse_reasoning(tool_call, &[]);
613
614 assert_eq!(result.reasoning_text, "");
615 assert_eq!(result.normal_text, tool_call);
616 }
617
618 #[test] fn test_minimax_m2_bare_invoke_exits_force_reasoning() {
620 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m2");
621 let tool_call =
622 "<invoke name=\"get_weather\"><parameter name=\"city\">NYC</parameter></invoke>";
623 let result =
624 parser.detect_and_parse_reasoning(&format!("I should call weather.{tool_call}"), &[]);
625
626 assert_eq!(result.reasoning_text, "I should call weather.");
627 assert_eq!(result.normal_text, tool_call);
628 }
629
630 #[test] fn test_minimax_m2_bare_invoke_streaming_exits_force_reasoning() {
632 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m2");
633
634 let r1 = parser.parse_reasoning_streaming_incremental("I should call ", &[]);
635 assert_eq!(r1.reasoning_text, "I should call ");
636 assert_eq!(r1.normal_text, "");
637
638 let r2 = parser.parse_reasoning_streaming_incremental("weather.<invoke na", &[]);
639 assert_eq!(r2.reasoning_text, "weather.");
640 assert_eq!(r2.normal_text, "");
641
642 let tail = "me=\"get_weather\"><parameter name=\"city\">NYC</parameter></invoke>";
643 let r3 = parser.parse_reasoning_streaming_incremental(tail, &[]);
644 assert_eq!(r3.reasoning_text, "");
645 assert_eq!(
646 r3.normal_text,
647 "<invoke name=\"get_weather\"><parameter name=\"city\">NYC</parameter></invoke>"
648 );
649 }
650
651 #[test] fn test_minimax_m3_detect_and_parse() {
653 for parser_name in ["minimax_m3", "minimax-m3"] {
654 let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
655 let result =
656 parser.detect_and_parse_reasoning("<mm:think>thinking</mm:think>answer", &[]);
657 assert_eq!(result.reasoning_text, "thinking");
658 assert_eq!(result.normal_text, "answer");
659 }
660 }
661
662 #[test] fn test_minimax_m3_streaming() {
664 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
665
666 let r1 = parser.parse_reasoning_streaming_incremental("<mm:think>rea", &[]);
667 let r2 = parser.parse_reasoning_streaming_incremental("son</mm:think>answer", &[]);
668
669 assert_eq!(
670 format!("{}{}", r1.reasoning_text, r2.reasoning_text),
671 "reason"
672 );
673 assert_eq!(format!("{}{}", r1.normal_text, r2.normal_text), "answer");
674 }
675
676 #[test] fn test_minimax_m3_streaming_with_prompt_prefilled_start_marker() {
678 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
679 parser.set_in_reasoning(true);
680
681 let result = parser.parse_reasoning_streaming_incremental("reason</mm:think>answer", &[]);
682
683 assert_eq!(result.reasoning_text, "reason");
684 assert_eq!(result.normal_text, "answer");
685 }
686
687 #[test] fn test_minimax_m3_native_tool_namespace_ends_prompt_prefilled_reasoning() {
689 for parser_name in ["minimax_m3", "minimax-m3"] {
690 for prompt_prefilled_state_set in [false, true] {
691 let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
692 if prompt_prefilled_state_set {
693 parser.set_in_reasoning(true);
694 }
695
696 let input = format!(
697 "reasoning{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>\n\
698 {MINIMAX_M3_TOOL_NAMESPACE}<invoke name=\"get_weather\">"
699 );
700 let result = parser.detect_and_parse_reasoning(&input, &[]);
701
702 assert_eq!(
703 result.reasoning_text, "reasoning",
704 "parser {parser_name}, prompt-prefilled state set: \
705 {prompt_prefilled_state_set}"
706 );
707 assert_eq!(
708 result.normal_text,
709 format!(
710 "{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>\n\
711 {MINIMAX_M3_TOOL_NAMESPACE}<invoke name=\"get_weather\">"
712 ),
713 "parser {parser_name}, prompt-prefilled state set: \
714 {prompt_prefilled_state_set}"
715 );
716 }
717 }
718 }
719
720 #[test] fn test_minimax_m3_native_tool_namespace_is_lossless_across_stream_chunks() {
722 let mut chunkings = vec![vec![
723 "reasoning".to_string(),
724 MINIMAX_M3_TOOL_NAMESPACE.to_string(),
725 "<tool_call>".to_string(),
726 ]];
727 chunkings.extend((1..MINIMAX_M3_TOOL_NAMESPACE.len()).map(|split| {
728 let (namespace_prefix, namespace_suffix) = MINIMAX_M3_TOOL_NAMESPACE.split_at(split);
729 vec![
730 format!("reasoning{namespace_prefix}"),
731 format!("{namespace_suffix}<tool_call>"),
732 ]
733 }));
734
735 for chunks in chunkings {
736 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
737 parser.set_in_reasoning(true);
738
739 let results: Vec<_> = chunks
740 .iter()
741 .map(|chunk| parser.parse_reasoning_streaming_incremental(chunk, &[]))
742 .collect();
743
744 assert_eq!(
745 results
746 .iter()
747 .map(|result| result.reasoning_text.as_str())
748 .collect::<String>(),
749 "reasoning",
750 "namespace chunking {chunks:?} leaked into reasoning"
751 );
752 assert_eq!(
753 results
754 .iter()
755 .map(|result| result.normal_text.as_str())
756 .collect::<String>(),
757 format!("{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>"),
758 "namespace chunking {chunks:?} was not preserved for the tool parser"
759 );
760 }
761 }
762
763 #[test] fn test_minimax_m3_native_tool_namespace_recovers_implicit_streaming_reasoning() {
765 for parser_name in ["minimax_m3", "minimax-m3"] {
766 let cases = [
767 (
768 vec![format!("reasoning{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>")],
769 "reasoning",
770 ),
771 (
772 vec![
773 "reasoning]".to_string(),
774 "<]minimax[>[<tool_call>".to_string(),
775 ],
776 "reasoning",
777 ),
778 (
779 vec![
780 "reasoning".to_string(),
781 format!("{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>"),
782 ],
783 "reasoning",
784 ),
785 (
786 vec![
787 "let ".to_string(),
788 "me ".to_string(),
789 "think".to_string(),
790 MINIMAX_M3_TOOL_NAMESPACE.to_string(),
791 "<tool_call>".to_string(),
792 ],
793 "let me think",
794 ),
795 ];
796
797 for (chunks, expected_reasoning) in cases {
798 let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
799 let results: Vec<_> = chunks
800 .iter()
801 .map(|chunk| parser.parse_reasoning_streaming_incremental(chunk, &[]))
802 .collect();
803
804 assert_eq!(
805 results
806 .iter()
807 .map(|result| result.reasoning_text.as_str())
808 .collect::<String>(),
809 expected_reasoning,
810 "parser {parser_name}, chunks {chunks:?}"
811 );
812 assert_eq!(
813 results
814 .iter()
815 .map(|result| result.normal_text.as_str())
816 .collect::<String>(),
817 format!("{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>"),
818 "parser {parser_name}, chunks {chunks:?}"
819 );
820 }
821 }
822 }
823
824 #[test] fn test_minimax_m3_implicit_streaming_recovery_waits_for_a_decisive_boundary() {
826 let chunks = ["see item [1]", " and [2]", " and [3]", " done."];
827 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
828
829 for chunk in chunks {
830 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
831 assert_eq!(result.reasoning_text, "");
832 assert_eq!(result.normal_text, "");
833 }
834
835 let finished = parser.finish_reasoning_stream();
836 assert_eq!(finished.reasoning_text, "");
837 assert_eq!(finished.normal_text, "see item [1] and [2] and [3] done.");
838 }
839
840 #[test] fn test_minimax_m3_partial_implicit_namespace_fakeout_is_normal() {
842 for finish_after_prefix in [false, true] {
843 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
844
845 let first = parser.parse_reasoning_streaming_incremental("plain]", &[]);
846 let second = if finish_after_prefix {
847 parser.finish_reasoning_stream()
848 } else {
849 parser.parse_reasoning_streaming_incremental("not-a-namespace", &[])
850 };
851 let finished = if finish_after_prefix {
852 ParserResult::default()
853 } else {
854 parser.finish_reasoning_stream()
855 };
856
857 assert_eq!(
858 format!(
859 "{}{}{}",
860 first.reasoning_text, second.reasoning_text, finished.reasoning_text
861 ),
862 ""
863 );
864 assert_eq!(
865 format!(
866 "{}{}{}",
867 first.normal_text, second.normal_text, finished.normal_text
868 ),
869 if finish_after_prefix {
870 "plain]"
871 } else {
872 "plain]not-a-namespace"
873 }
874 );
875 }
876 }
877
878 #[test] fn test_minimax_m3_explicit_normal_state_streams_each_chunk_immediately() {
880 let chunks = ["see item [1]", " and [2]", " and [3]", " done."];
881 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
882 parser.set_in_reasoning(false);
883
884 for chunk in chunks {
885 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
886 assert_eq!(result.reasoning_text, "");
887 assert_eq!(result.normal_text, chunk);
888 }
889
890 let finished = parser.finish_reasoning_stream();
891 assert_eq!(finished.reasoning_text, "");
892 assert_eq!(finished.normal_text, "");
893 }
894
895 #[test] fn test_minimax_m3_explicit_normal_state_disables_implicit_namespace_recovery() {
897 let input = format!("normal{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>");
898
899 let mut batch_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
900 batch_parser.set_in_reasoning(false);
901 let batch = batch_parser.detect_and_parse_reasoning(&input, &[]);
902
903 let mut stream_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
904 stream_parser.set_in_reasoning(false);
905 let streamed = stream_parser.parse_reasoning_streaming_incremental(&input, &[]);
906
907 for result in [batch, streamed] {
908 assert_eq!(result.reasoning_text, "");
909 assert_eq!(result.normal_text, input);
910 }
911 }
912
913 #[test] fn test_minimax_m3_native_tool_namespace_with_empty_reasoning() {
915 let input = format!("{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>");
916
917 let mut batch_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
918 batch_parser.set_in_reasoning(true);
919 let batch = batch_parser.detect_and_parse_reasoning(&input, &[]);
920
921 let mut stream_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
922 stream_parser.set_in_reasoning(true);
923 let streamed = stream_parser.parse_reasoning_streaming_incremental(&input, &[]);
924
925 for result in [batch, streamed] {
926 assert_eq!(result.reasoning_text, "");
927 assert_eq!(result.normal_text, input);
928 }
929 }
930
931 #[test] fn test_minimax_m3_namespace_after_explicit_end_keeps_normal_prefix() {
933 let input = format!("reason</mm:think>answer{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>");
934 let expected_normal = format!("answer{MINIMAX_M3_TOOL_NAMESPACE}<tool_call>");
935
936 let mut batch_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
937 let batch = batch_parser.detect_and_parse_reasoning(&input, &[]);
938
939 let mut stream_parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
940 let streamed = stream_parser.parse_reasoning_streaming_incremental(&input, &[]);
941
942 for result in [batch, streamed] {
943 assert_eq!(result.reasoning_text, "reason");
944 assert_eq!(result.normal_text, expected_normal);
945 }
946 }
947
948 #[test] fn test_minimax_m3_detect_and_parse_dangling_end_marker() {
950 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
951 let result = parser.detect_and_parse_reasoning("reason</mm:think>answer", &[]);
952
953 assert_eq!(result.reasoning_text, "reason");
954 assert_eq!(result.normal_text, "answer");
955 }
956
957 #[test] fn test_minimax_m3_streaming_dangling_end_marker() {
959 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
960
961 let r1 = parser.parse_reasoning_streaming_incremental("reason</mm:", &[]);
962 let r2 = parser.parse_reasoning_streaming_incremental("think>answer", &[]);
963
964 assert_eq!(
965 format!("{}{}", r1.reasoning_text, r2.reasoning_text),
966 "reason"
967 );
968 assert_eq!(format!("{}{}", r1.normal_text, r2.normal_text), "answer");
969 }
970
971 #[test] fn test_minimax_m3_streaming_close_marker_only_is_stripped() {
973 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
974
975 let result = parser.parse_reasoning_streaming_incremental(
976 "</mm:think>I'll check the content of both files.",
977 &[],
978 );
979
980 assert_eq!(result.reasoning_text, "");
981 assert_eq!(result.normal_text, "I'll check the content of both files.");
982 }
983
984 #[test] fn test_minimax_m3_detect_and_parse_multiple_spans() {
986 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
987 let result = parser.detect_and_parse_reasoning(
988 "<mm:think>first</mm:think> middle <mm:think>second</mm:think> done",
989 &[],
990 );
991
992 assert_eq!(result.reasoning_text, "firstsecond");
993 assert_eq!(result.normal_text, "middle done");
994 }
995
996 #[test] fn test_minimax_m3_streaming_prompt_prefilled_close_after_complete_span() {
998 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
999
1000 let r1 = parser.parse_reasoning_streaming_incremental("<mm:think>preamble</mm:think>", &[]);
1001 let r2 = parser.parse_reasoning_streaming_incremental("body</mm:think>", &[]);
1002 let r3 = parser.parse_reasoning_streaming_incremental("answer", &[]);
1003
1004 assert_eq!(
1005 format!(
1006 "{}{}{}",
1007 r1.reasoning_text, r2.reasoning_text, r3.reasoning_text
1008 ),
1009 "preamblebody"
1010 );
1011 assert_eq!(
1012 format!("{}{}{}", r1.normal_text, r2.normal_text, r3.normal_text),
1013 "answer"
1014 );
1015 }
1016
1017 #[test] fn test_minimax_m3_streaming_partial_start_prefix_becomes_normal_text_at_eof() {
1019 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
1020
1021 let r1 = parser.parse_reasoning_streaming_incremental("plain <mm:th", &[]);
1022 let r2 = parser.parse_reasoning_streaming_incremental("esis answer", &[]);
1023 let finished = parser.finish_reasoning_stream();
1024
1025 assert_eq!(r1.reasoning_text, "");
1026 assert_eq!(r1.normal_text, "");
1027 assert_eq!(r2.reasoning_text, "");
1028 assert_eq!(r2.normal_text, "");
1029 assert_eq!(finished.reasoning_text, "");
1030 assert_eq!(finished.normal_text, "plain <mm:thesis answer");
1031 }
1032
1033 #[test] fn test_deepseek_v4_detect_and_parse() {
1035 for parser_name in ["deepseek_v4", "deepseek-v4", "deepseekv4"] {
1036 let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
1037 let result = parser.detect_and_parse_reasoning("<think>thinking</think>answer", &[]);
1038 assert_eq!(result.reasoning_text, "thinking");
1039 assert_eq!(result.normal_text, "answer");
1040 }
1041 }
1042
1043 #[test] fn test_deepseek_v4_no_forced_reasoning_without_tags() {
1045 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
1046 let result = parser.detect_and_parse_reasoning("answer only", &[]);
1047 assert_eq!(result.reasoning_text, "");
1048 assert_eq!(result.normal_text, "answer only");
1049 }
1050
1051 #[test] fn test_deepseek_v4_streaming() {
1053 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
1054
1055 let chunks = ["<think>rea", "son</think>answer"];
1056 let mut reasoning = String::new();
1057 let mut normal = String::new();
1058
1059 for chunk in chunks {
1060 let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
1061 reasoning.push_str(&result.reasoning_text);
1062 normal.push_str(&result.normal_text);
1063 }
1064
1065 assert_eq!(reasoning, "reason");
1066 assert_eq!(normal, "answer");
1067 }
1068
1069 #[test] fn test_kimi_k25_detect_and_parse() {
1071 let cases = [
1073 (
1074 "force reasoning: no think tags",
1075 "no think tags here",
1076 "no think tags here",
1077 "",
1078 ),
1079 (
1080 "standard think tags",
1081 "<think>Let me reason about this.</think>Hello!",
1082 "Let me reason about this.",
1083 "Hello!",
1084 ),
1085 (
1086 "empty think block (instant mode)",
1087 "<think></think>Hello from instant mode!",
1088 "",
1089 "Hello from instant mode!",
1090 ),
1091 (
1092 "empty think block with newline",
1093 "<think>\n</think>Hello from instant mode!",
1094 "",
1095 "Hello from instant mode!",
1096 ),
1097 ];
1098
1099 for (desc, input, expected_reasoning, expected_normal) in cases {
1100 let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
1101 let result = parser.detect_and_parse_reasoning(input, &[]);
1102 assert_eq!(
1103 result.reasoning_text, expected_reasoning,
1104 "FAILED reasoning: {desc}"
1105 );
1106 assert_eq!(result.normal_text, expected_normal, "FAILED normal: {desc}");
1107 }
1108 }
1109
1110 #[test] fn test_kimi_k25_streaming_force_reasoning() {
1112 let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
1114
1115 let r1 = parser.parse_reasoning_streaming_incremental("<thi", &[]);
1117 assert_eq!(r1.reasoning_text, "");
1118 assert_eq!(r1.normal_text, "");
1119
1120 let r2 = parser.parse_reasoning_streaming_incremental("nk>reasoning here", &[]);
1122 assert_eq!(r2.reasoning_text, "reasoning here");
1123 assert_eq!(r2.normal_text, "");
1124
1125 let r3 = parser.parse_reasoning_streaming_incremental("</think>Hello!", &[]);
1127 assert_eq!(r3.reasoning_text, "");
1128 assert_eq!(r3.normal_text, "Hello!");
1129 }
1130
1131 #[test] fn test_kimi_k25_streaming() {
1133 let cases: Vec<(&str, &[&str], &str, &str)> = vec![
1135 (
1136 "complete response",
1137 &[
1138 "<think>",
1139 "I need to",
1140 " think about",
1141 " this carefully.",
1142 "</think>",
1143 "Bonjour",
1144 "!",
1145 ],
1146 "I need to think about this carefully.",
1147 "Bonjour!",
1148 ),
1149 (
1150 "empty think (instant mode)",
1151 &["<think>", "</think>", "Direct answer."],
1152 "",
1153 "Direct answer.",
1154 ),
1155 ];
1156
1157 for (desc, tokens, expected_reasoning, expected_content) in cases {
1158 let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
1159 let mut all_reasoning = String::new();
1160 let mut all_content = String::new();
1161 for token in tokens {
1162 let r = parser.parse_reasoning_streaming_incremental(token, &[]);
1163 all_reasoning.push_str(&r.reasoning_text);
1164 all_content.push_str(&r.normal_text);
1165 }
1166 assert_eq!(
1167 all_reasoning, expected_reasoning,
1168 "FAILED reasoning: {desc}"
1169 );
1170 assert_eq!(all_content, expected_content, "FAILED content: {desc}");
1171 }
1172 }
1173
1174 #[test] fn test_kimi_k25_parser_lookup_by_name() {
1176 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k25");
1178 let result = parser.detect_and_parse_reasoning("<think>thinking</think>answer", &[]);
1179 assert_eq!(result.reasoning_text, "thinking");
1180 assert_eq!(result.normal_text, "answer");
1181 }
1182
1183 #[test]
1184 fn test_kimi_k3_explicit_reasoning_channel() {
1185 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k3");
1186 let result = parser.detect_and_parse_reasoning(
1187 "<|open|>think<|sep|>check weather<|close|>think<|sep|><|open|>response<|sep|>It is raining.<|close|>response<|sep|>",
1188 &[],
1189 );
1190
1191 assert_eq!(result.reasoning_text, "check weather");
1192 assert_eq!(
1193 result.normal_text,
1194 "<|open|>response<|sep|>It is raining.<|close|>response<|sep|>"
1195 );
1196 }
1197
1198 #[test]
1199 fn test_kimi_k3_prompt_prefilled_reasoning_channel() {
1200 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi-k3");
1201 parser.set_in_reasoning(true);
1202
1203 let result = parser.parse_reasoning_streaming_incremental(
1204 "check weather<|close|>think<|sep|><|open|>response<|sep|>It is raining.",
1205 &[],
1206 );
1207
1208 assert_eq!(result.reasoning_text, "check weather");
1209 assert_eq!(result.normal_text, "<|open|>response<|sep|>It is raining.");
1210 }
1211
1212 #[test]
1213 fn test_kimi_k3_streaming_dangling_close_split_across_chunks() {
1214 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k3");
1215
1216 let first = parser.parse_reasoning_streaming_incremental("check weather<|close|>thi", &[]);
1217 let second = parser.parse_reasoning_streaming_incremental(
1218 "nk<|sep|><|open|>response<|sep|>It is raining.",
1219 &[],
1220 );
1221
1222 assert_eq!(
1223 format!("{}{}", first.reasoning_text, second.reasoning_text),
1224 "check weather"
1225 );
1226 assert_eq!(
1227 format!("{}{}", first.normal_text, second.normal_text),
1228 "<|open|>response<|sep|>It is raining."
1229 );
1230 }
1231
1232 #[test]
1233 fn test_kimi_k3_thinking_disabled_is_normal_text() {
1234 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k3");
1235 let response = "answer<|close|>response<|sep|><|close|>message<|sep|>";
1236 let result = parser.detect_and_parse_reasoning(response, &[]);
1237
1238 assert_eq!(result.reasoning_text, "");
1239 assert_eq!(result.normal_text, response);
1240 }
1241
1242 #[test]
1243 fn test_kimi_k3_response_marker_recovers_missing_think_close() {
1244 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k3");
1245 parser.set_in_reasoning(true);
1246 let result = parser
1247 .detect_and_parse_reasoning("check weather<|open|>response<|sep|>It is raining.", &[]);
1248
1249 assert_eq!(result.reasoning_text, "check weather");
1250 assert_eq!(result.normal_text, "<|open|>response<|sep|>It is raining.");
1251 }
1252
1253 #[test]
1254 fn test_kimi_k3_reasoning_output_composes_with_tool_parser() {
1255 let output = concat!(
1256 "<|open|>think<|sep|>use the calculator<|close|>think<|sep|>",
1257 "<|open|>response<|sep|>I will calculate.<|close|>response<|sep|>",
1258 "<|open|>tools<|sep|>",
1259 "<|open|>call tool=\"calc\" index=\"1\"<|sep|>",
1260 "<|open|>argument key=\"x\" type=\"number\"<|sep|>42",
1261 "<|close|>argument<|sep|><|close|>call<|sep|>",
1262 "<|close|>tools<|sep|><|close|>message<|sep|><|end_of_msg|>"
1263 );
1264 let mut reasoning = ReasoningParserType::KimiK3.get_reasoning_parser();
1265 let split = reasoning.detect_and_parse_reasoning(output, &[]);
1266
1267 let (calls, content) = crate::tool_calling::try_tool_call_parse_kimi_k3(
1268 &split.normal_text,
1269 &crate::tool_calling::KimiK3ParserConfig::default(),
1270 None,
1271 )
1272 .unwrap();
1273
1274 assert_eq!(split.reasoning_text, "use the calculator");
1275 assert_eq!(content.as_deref(), Some("I will calculate."));
1276 assert_eq!(calls.len(), 1);
1277 assert_eq!(calls[0].id, "calc:0");
1278 assert_eq!(calls[0].function.name, "calc");
1279 assert_eq!(calls[0].function.arguments, r#"{"x":42}"#);
1280 }
1281
1282 fn parse_streamed_kimi_k3(
1283 completion: &str,
1284 split: usize,
1285 ) -> (String, Vec<crate::tool_calling::ToolCallResponse>, String) {
1286 parse_streamed_kimi_k3_chunks(&[&completion[..split], &completion[split..]])
1287 }
1288
1289 fn parse_streamed_kimi_k3_chunks(
1290 chunks: &[&str],
1291 ) -> (String, Vec<crate::tool_calling::ToolCallResponse>, String) {
1292 let mut parser = ReasoningParserType::KimiK3.get_reasoning_parser();
1293 parser.set_in_reasoning(true);
1294
1295 let mut reasoning = String::new();
1296 let mut xtml = String::new();
1297 for chunk in chunks {
1298 let parsed = parser.parse_reasoning_streaming_incremental(chunk, &[]);
1299 reasoning.push_str(&parsed.reasoning_text);
1300 xtml.push_str(&parsed.normal_text);
1301 }
1302 let finished = parser.finish_reasoning_stream();
1303 reasoning.push_str(&finished.reasoning_text);
1304 xtml.push_str(&finished.normal_text);
1305
1306 let (calls, content) = crate::tool_calling::try_tool_call_parse_kimi_k3(
1307 &xtml,
1308 &crate::tool_calling::KimiK3ParserConfig::default(),
1309 None,
1310 )
1311 .unwrap();
1312 (reasoning, calls, content.unwrap_or_default())
1313 }
1314
1315 fn assert_kimi_k3_all_splits<F>(completion: &str, mut assert_result: F)
1316 where
1317 F: FnMut(usize, &str, &[crate::tool_calling::ToolCallResponse], &str),
1318 {
1319 for split in completion
1320 .char_indices()
1321 .map(|(position, _)| position)
1322 .skip(1)
1323 .chain(std::iter::once(completion.len()))
1324 {
1325 let (reasoning, calls, content) = parse_streamed_kimi_k3(completion, split);
1326 assert_result(split, &reasoning, &calls, &content);
1327 }
1328 }
1329
1330 #[test]
1331 fn test_kimi_k3_think_close_all_splits_preserve_response_content() {
1332 const THINK_CLOSE: &str = "<|close|>think<|sep|>";
1333
1334 for answer in ["17", r#"{"answer":81}"#] {
1335 for split in THINK_CLOSE
1336 .char_indices()
1337 .map(|(position, _)| position)
1338 .chain(std::iter::once(THINK_CLOSE.len()))
1339 {
1340 let (reasoning, calls, content) = parse_streamed_kimi_k3_chunks(&[
1341 "reasoning text",
1342 &THINK_CLOSE[..split],
1343 &THINK_CLOSE[split..],
1344 answer,
1345 ]);
1346
1347 assert_eq!(reasoning, "reasoning text", "split at byte {split}");
1348 assert!(calls.is_empty(), "split at byte {split}");
1349 assert_eq!(content, answer, "split at byte {split}");
1350 }
1351 }
1352 }
1353
1354 #[test]
1355 fn test_kimi_k3_think_close_all_splits_preserve_native_tool_call() {
1356 const THINK_CLOSE: &str = "<|close|>think<|sep|>";
1357 let tool_call = concat!(
1358 "<|open|>tools<|sep|>",
1359 "<|open|>call tool=\"calc\" index=\"1\"<|sep|>",
1360 "<|open|>argument key=\"x\" type=\"number\"<|sep|>5",
1361 "<|close|>argument<|sep|><|close|>call<|sep|>",
1362 "<|close|>tools<|sep|><|close|>message<|sep|><|end_of_msg|>"
1363 );
1364
1365 for split in THINK_CLOSE
1366 .char_indices()
1367 .map(|(position, _)| position)
1368 .chain(std::iter::once(THINK_CLOSE.len()))
1369 {
1370 let (reasoning, calls, content) = parse_streamed_kimi_k3_chunks(&[
1371 "reasoning text",
1372 &THINK_CLOSE[..split],
1373 &THINK_CLOSE[split..],
1374 tool_call,
1375 ]);
1376
1377 assert_eq!(reasoning, "reasoning text", "split at byte {split}");
1378 assert_eq!(content, "", "split at byte {split}");
1379 assert_eq!(calls.len(), 1, "split at byte {split}");
1380 assert_eq!(calls[0].function.name, "calc");
1381 assert_eq!(calls[0].function.arguments, r#"{"x":5}"#);
1382 }
1383 }
1384
1385 #[test]
1386 fn test_kimi_k3_aggregated_think_close_does_not_leak() {
1387 let mut reasoning = ReasoningParserType::KimiK3.get_reasoning_parser();
1388 let split = reasoning.detect_and_parse_reasoning(
1389 concat!(
1390 "<|open|>think<|sep|>reasoning text",
1391 "<|close|>think<|sep|>answer"
1392 ),
1393 &[],
1394 );
1395
1396 let (calls, content) = crate::tool_calling::try_tool_call_parse_kimi_k3(
1397 &split.normal_text,
1398 &crate::tool_calling::KimiK3ParserConfig::default(),
1399 None,
1400 )
1401 .unwrap();
1402
1403 assert_eq!(split.reasoning_text, "reasoning text");
1404 assert!(calls.is_empty());
1405 assert_eq!(content.as_deref(), Some("answer"));
1406 }
1407
1408 #[test]
1409 fn test_kimi_k3_live_response_handoff_is_chunk_boundary_independent() {
1410 let completion = concat!(
1411 "Final exactly '4'.",
1412 "<|open|>response<|sep|>4",
1413 "<|close|>response<|sep|>",
1414 "<|close|>message<|sep|>",
1415 "<|end_of_msg|>"
1416 );
1417
1418 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1419 assert_eq!(
1420 reasoning, "Final exactly '4'.",
1421 "split at byte {split} leaked the response channel into reasoning"
1422 );
1423 assert!(calls.is_empty(), "split at byte {split}");
1424 assert_eq!(content, "4", "split at byte {split}");
1425 });
1426 }
1427
1428 #[test]
1429 fn test_kimi_k3_live_multi_argument_call_is_chunk_boundary_independent() {
1430 let completion = concat!(
1431 "Now call add_numbers.",
1432 "<|open|>tools<|sep|>",
1433 "<|open|>call tool=\"add_numbers\" index=\"1\"<|sep|>",
1434 "<|open|>argument key=\"a\" type=\"number\"<|sep|>17",
1435 "<|close|>argument<|sep|>",
1436 "<|open|>argument key=\"b\" type=\"number\"<|sep|>19",
1437 "<|close|>argument<|sep|>",
1438 "<|close|>call<|sep|>",
1439 "<|close|>tools<|sep|>",
1440 "<|close|>message<|sep|>",
1441 "<|end_of_msg|>"
1442 );
1443
1444 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1445 assert_eq!(
1446 reasoning, "Now call add_numbers.",
1447 "split at byte {split} leaked argument framing into reasoning"
1448 );
1449 assert_eq!(content, "", "split at byte {split}");
1450 assert_eq!(calls.len(), 1, "split at byte {split}");
1451 assert_eq!(calls[0].function.name, "add_numbers");
1452 assert_eq!(calls[0].function.arguments, r#"{"a":17,"b":19}"#);
1453 });
1454 }
1455
1456 #[test]
1457 fn test_kimi_k3_live_parallel_calls_are_chunk_boundary_independent() {
1458 let completion = concat!(
1459 "Fetch both URLs.",
1460 "<|open|>tools<|sep|>",
1461 "<|open|>call tool=\"fetch_url\" index=\"1\"<|sep|>",
1462 "<|open|>argument key=\"url\" type=\"string\"<|sep|>https://a.example/x",
1463 "<|close|>argument<|sep|><|close|>call<|sep|>",
1464 "<|open|>call tool=\"fetch_url\" index=\"2\"<|sep|>",
1465 "<|open|>argument key=\"url\" type=\"string\"<|sep|>https://b.example/y",
1466 "<|close|>argument<|sep|><|close|>call<|sep|>",
1467 "<|close|>tools<|sep|>",
1468 "<|close|>message<|sep|><|end_of_msg|>"
1469 );
1470
1471 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1472 assert_eq!(
1473 reasoning, "Fetch both URLs.",
1474 "split at byte {split} leaked the second call into reasoning"
1475 );
1476 assert_eq!(content, "", "split at byte {split}");
1477 assert_eq!(calls.len(), 2, "split at byte {split}");
1478 assert_eq!(
1479 calls[0].function.arguments,
1480 r#"{"url":"https://a.example/x"}"#
1481 );
1482 assert_eq!(
1483 calls[1].function.arguments,
1484 r#"{"url":"https://b.example/y"}"#
1485 );
1486 });
1487 }
1488
1489 #[test]
1490 fn test_kimi_k3_live_bare_call_recovers_missing_outer_tools_wrapper() {
1491 let completion = concat!(
1492 "Call the calculator.",
1493 "<|open|>call tool=\"calc\" index=\"1\"<|sep|>",
1494 "<|open|>argument key=\"x\" type=\"number\"<|sep|>5",
1495 "<|close|>argument<|sep|><|close|>call<|sep|>",
1496 "<|close|>message<|sep|><|end_of_msg|>"
1497 );
1498
1499 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1500 assert_eq!(reasoning, "Call the calculator.", "split at byte {split}");
1501 assert_eq!(content, "", "split at byte {split}");
1502 assert_eq!(calls.len(), 1, "split at byte {split}");
1503 assert_eq!(calls[0].function.name, "calc");
1504 assert_eq!(calls[0].function.arguments, r#"{"x":5}"#);
1505 });
1506 }
1507
1508 #[test]
1509 fn test_kimi_k3_live_orphan_argument_is_quarantined_not_leaked() {
1510 let completion = concat!(
1511 "Choose the unit.",
1512 "<|open|>argument key=\"unit\" type=\"string\"<|sep|>celsius",
1513 "<|close|>argument<|sep|>",
1514 "<|close|>call<|sep|><|close|>tools<|sep|>"
1515 );
1516
1517 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1518 assert_eq!(reasoning, "Choose the unit.", "split at byte {split}");
1519 assert!(calls.is_empty(), "split at byte {split}");
1520 assert_eq!(content, "", "split at byte {split}");
1521 });
1522 }
1523
1524 #[test]
1525 fn test_kimi_k3_added_token_spacing_is_chunk_boundary_independent() {
1526 let completion = concat!(
1527 "Use the calculator.",
1528 "<|open|> tools <|sep|>",
1529 "<|open|> call tool=\"calc\" index=\"1\" <|sep|>",
1530 "<|open|> argument key=\"x\" type=\"number\" <|sep|>5",
1531 "<|close|> argument <|sep|>",
1532 "<|close|> call <|sep|>",
1533 "<|close|> tools <|sep|>",
1534 "<|close|> message <|sep|>",
1535 "<|end_of_msg|>"
1536 );
1537
1538 assert_kimi_k3_all_splits(completion, |split, reasoning, calls, content| {
1539 assert_eq!(reasoning, "Use the calculator.", "split at byte {split}");
1540 assert_eq!(content, "", "split at byte {split}");
1541 assert_eq!(calls.len(), 1, "split at byte {split}");
1542 assert_eq!(calls[0].function.name, "calc");
1543 assert_eq!(calls[0].function.arguments, r#"{"x":5}"#);
1544 });
1545 }
1546
1547 #[test]
1548 fn test_kimi_k3_single_char_marker_buffering_preserves_literal_at_eof() {
1549 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k3");
1550 parser.set_in_reasoning(true);
1551
1552 let streamed = parser.parse_reasoning_streaming_incremental("literal<", &[]);
1553 let finished = parser.finish_reasoning_stream();
1554
1555 assert_eq!(streamed.reasoning_text, "literal");
1556 assert_eq!(streamed.normal_text, "");
1557 assert_eq!(finished.reasoning_text, "<");
1558 assert_eq!(finished.normal_text, "");
1559 }
1560
1561 #[test]
1562 fn test_non_k3_single_char_marker_behavior_is_unchanged() {
1563 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("qwen3");
1564 parser.set_in_reasoning(true);
1565
1566 let streamed = parser.parse_reasoning_streaming_incremental("literal<", &[]);
1567 let finished = parser.finish_reasoning_stream();
1568
1569 assert_eq!(streamed.reasoning_text, "literal<");
1570 assert_eq!(streamed.normal_text, "");
1571 assert_eq!(finished.reasoning_text, "");
1572 assert_eq!(finished.normal_text, "");
1573 }
1574
1575 #[test] fn test_kimi_vs_kimi_k25_different_tags() {
1577 let mut kimi = ReasoningParserType::Kimi.get_reasoning_parser();
1579 let mut kimi_k25 = ReasoningParserType::KimiK25.get_reasoning_parser();
1580
1581 let r_kimi = kimi.detect_and_parse_reasoning("<think>reasoning</think>answer", &[]);
1583 assert_eq!(r_kimi.normal_text, "<think>reasoning</think>answer");
1584 assert_eq!(r_kimi.reasoning_text, "");
1585
1586 let r_k25 = kimi_k25.detect_and_parse_reasoning("<think>reasoning</think>answer", &[]);
1588 assert_eq!(r_k25.reasoning_text, "reasoning");
1589 assert_eq!(r_k25.normal_text, "answer");
1590 }
1591
1592 #[test] fn test_nemotron_streaming_with_set_in_reasoning() {
1598 let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
1599 parser.set_in_reasoning(true); let tokens = &["Think", "ing about", " this", ".\n\n", "</think>", "Four"];
1602
1603 let mut all_reasoning = String::new();
1604 let mut all_content = String::new();
1605 for token in tokens {
1606 let r = parser.parse_reasoning_streaming_incremental(token, &[]);
1607 all_reasoning.push_str(&r.reasoning_text);
1608 all_content.push_str(&r.normal_text);
1609 }
1610 assert_eq!(all_reasoning, "Thinking about this.\n\n");
1611 assert_eq!(all_content, "Four");
1612 }
1613
1614 #[test] fn test_nemotron_streaming_force_reasoning_without_set_in_reasoning() {
1621 let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
1623
1624 let tokens = &["Think", "ing about", " this", ".\n\n", "</think>", "Four"];
1625
1626 let mut all_reasoning = String::new();
1627 let mut all_content = String::new();
1628 for token in tokens {
1629 let r = parser.parse_reasoning_streaming_incremental(token, &[]);
1630 all_reasoning.push_str(&r.reasoning_text);
1631 all_content.push_str(&r.normal_text);
1632 }
1633 assert_eq!(all_reasoning, "Thinking about this.\n\n");
1634 assert_eq!(all_content, "Four");
1635 }
1636
1637 #[test] fn test_nemotron_streaming_split_end_think_tokens() {
1644 let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
1645 parser.set_in_reasoning(true);
1646
1647 let tokens = &[
1649 "reason", "ing", " done", ".", "</", "think", ">", "Hello", " world",
1650 ];
1651
1652 let mut all_reasoning = String::new();
1653 let mut all_content = String::new();
1654 for token in tokens {
1655 let r = parser.parse_reasoning_streaming_incremental(token, &[]);
1656 all_reasoning.push_str(&r.reasoning_text);
1657 all_content.push_str(&r.normal_text);
1658 }
1659 assert_eq!(all_reasoning, "reasoning done.");
1660 assert_eq!(all_content, "Hello world");
1661 }
1662
1663 #[test] fn test_nemotron_v3_detect_and_parse_vllm_cases() {
1670 let cases = [
1671 (
1672 "without start token",
1673 "This is a reasoning section</think>This is the rest",
1674 "This is a reasoning section",
1675 "This is the rest",
1676 ),
1677 (
1678 "with start token",
1679 "<think>This is a reasoning section</think>This is the rest",
1680 "This is a reasoning section",
1681 "This is the rest",
1682 ),
1683 ];
1684
1685 for (desc, input, expected_reasoning, expected_content) in cases {
1686 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("nemotron_v3");
1687 let result = parser.detect_and_parse_reasoning(input, &[]);
1688 assert_eq!(
1689 result.reasoning_text, expected_reasoning,
1690 "FAILED reasoning: {desc}"
1691 );
1692 assert_eq!(
1693 result.normal_text, expected_content,
1694 "FAILED content: {desc}"
1695 );
1696 }
1697 }
1698
1699 #[test] fn test_nemotron_v3_streaming_vllm_cases() {
1705 let cases: Vec<(&str, &[&str], &str, &str)> = vec![
1706 (
1707 "without start token",
1708 &[
1709 "This is a reasoning section",
1710 "</think>",
1711 "This is the rest",
1712 ],
1713 "This is a reasoning section",
1714 "This is the rest",
1715 ),
1716 (
1717 "with start token",
1718 &[
1719 "<think>",
1720 "This is a reasoning section",
1721 "</think>",
1722 "This is the rest",
1723 ],
1724 "This is a reasoning section",
1725 "This is the rest",
1726 ),
1727 ];
1728
1729 for (desc, tokens, expected_reasoning, expected_content) in cases {
1730 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("nemotron_v3");
1731 let mut all_reasoning = String::new();
1732 let mut all_content = String::new();
1733 for token in tokens {
1734 let result = parser.parse_reasoning_streaming_incremental(token, &[]);
1735 all_reasoning.push_str(&result.reasoning_text);
1736 all_content.push_str(&result.normal_text);
1737 }
1738 assert_eq!(
1739 all_reasoning, expected_reasoning,
1740 "FAILED reasoning: {desc}"
1741 );
1742 assert_eq!(all_content, expected_content, "FAILED content: {desc}");
1743 }
1744 }
1745
1746 #[test]
1751 fn test_deepseek_v4_streaming_with_set_in_reasoning() {
1752 let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
1753 parser.set_in_reasoning(true);
1754
1755 let tokens = &[
1758 "Wei", "gh", "ing ", "options", ".", "</think>", "Bei", "jing", " is", " sunny.",
1759 ];
1760
1761 let mut all_reasoning = String::new();
1762 let mut all_content = String::new();
1763 for token in tokens {
1764 let r = parser.parse_reasoning_streaming_incremental(token, &[]);
1765 all_reasoning.push_str(&r.reasoning_text);
1766 all_content.push_str(&r.normal_text);
1767 }
1768 assert_eq!(all_reasoning, "Weighing options.");
1769 assert_eq!(all_content, "Beijing is sunny.");
1770 }
1771}