-
Notifications
You must be signed in to change notification settings - Fork 178
fix(tool_parser): fix func call parsing for native tool-call token #1062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,15 @@ impl BaseReasoningParser { | |
| || (self.config.think_end_token.starts_with(text) | ||
| && self.config.think_end_token != text) | ||
| } | ||
|
|
||
| /// Find the earliest tool-section start marker in `text`. | ||
| fn find_tool_section_start(&self, text: &str) -> Option<usize> { | ||
| self.config | ||
| .tool_section_start_markers | ||
| .iter() | ||
| .filter_map(|marker| text.find(marker.as_str())) | ||
| .min() | ||
| } | ||
| } | ||
|
|
||
| impl ReasoningParser for BaseReasoningParser { | ||
|
|
@@ -63,7 +72,11 @@ impl ReasoningParser for BaseReasoningParser { | |
| .to_string(); | ||
|
|
||
| if !processed_text.contains(&self.config.think_end_token) { | ||
| // Assume reasoning was truncated before end token | ||
| if let Some(tool_pos) = self.find_tool_section_start(&processed_text) { | ||
| let reasoning_text = processed_text[..tool_pos].trim().to_string(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Nit: The hardcoded |
||
| let normal_text = processed_text[tool_pos..].to_string(); | ||
| return Ok(ParserResult::new(normal_text, reasoning_text)); | ||
| } | ||
| return Ok(ParserResult::reasoning(processed_text)); | ||
| } | ||
|
|
||
|
|
@@ -133,7 +146,13 @@ impl ReasoningParser for BaseReasoningParser { | |
|
|
||
| // Continue with reasoning content | ||
| if self.in_reasoning && self.config.stream_reasoning { | ||
| // Stream the content immediately | ||
| if let Some(tool_pos) = self.find_tool_section_start(¤t_text) { | ||
| let reasoning_text = current_text[..tool_pos].trim().to_string(); | ||
| let normal_text = current_text[tool_pos..].to_string(); | ||
| self.buffer.clear(); | ||
|
Comment on lines
+149
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new reasoning bailout only checks for a full tool-section marker in Useful? React with 👍 / 👎. |
||
| self.in_reasoning = false; | ||
| return Ok(ParserResult::new(normal_text, reasoning_text)); | ||
| } | ||
| let reasoning_text = current_text; | ||
| self.buffer.clear(); | ||
| Ok(ParserResult::reasoning(reasoning_text)) | ||
|
|
@@ -188,6 +207,7 @@ mod tests { | |
| stream_reasoning, | ||
| max_buffer_size: DEFAULT_MAX_BUFFER_SIZE, | ||
| always_in_reasoning, | ||
| ..Default::default() | ||
| }; | ||
| BaseReasoningParser::new(config) | ||
| } | ||
|
|
@@ -368,4 +388,55 @@ mod tests { | |
| _ => panic!("Expected BufferOverflow error"), | ||
| } | ||
| } | ||
|
|
||
| fn create_parser_with_markers() -> BaseReasoningParser { | ||
| let config = ParserConfig { | ||
| think_start_token: "<think>".to_string(), | ||
| think_end_token: "</think>".to_string(), | ||
| tool_section_start_markers: vec!["<|tool_calls_section_begin|>".to_string()], | ||
| ..Default::default() | ||
| }; | ||
| BaseReasoningParser::new(config) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tool_marker_stops_reasoning_non_streaming() { | ||
| let mut parser = create_parser_with_markers(); | ||
| let input = "<think>thinking here<|tool_calls_section_begin|>tool call data"; | ||
| let result = parser.detect_and_parse_reasoning(input).unwrap(); | ||
| assert_eq!(result.reasoning_text, "thinking here"); | ||
| assert_eq!( | ||
| result.normal_text, | ||
| "<|tool_calls_section_begin|>tool call data" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tool_marker_stops_reasoning_streaming() { | ||
| let mut parser = create_parser_with_markers(); | ||
| let r1 = parser | ||
| .parse_reasoning_streaming_incremental("<think>reasoning ") | ||
| .unwrap(); | ||
| assert_eq!(r1.reasoning_text, "reasoning "); | ||
| assert!(parser.is_in_reasoning()); | ||
|
|
||
| let r2 = parser | ||
| .parse_reasoning_streaming_incremental("more<|tool_calls_section_begin|>tool data") | ||
| .unwrap(); | ||
| assert_eq!(r2.reasoning_text, "more"); | ||
| assert_eq!(r2.normal_text, "<|tool_calls_section_begin|>tool data"); | ||
| assert!(!parser.is_in_reasoning()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_no_markers_does_not_stop_reasoning() { | ||
| let mut parser = create_test_parser(false, true); | ||
| let input = "<think>thinking<|tool_calls_section_begin|>stuff"; | ||
| let result = parser.detect_and_parse_reasoning(input).unwrap(); | ||
| assert_eq!( | ||
| result.reasoning_text, | ||
| "thinking<|tool_calls_section_begin|>stuff" | ||
| ); | ||
| assert_eq!(result.normal_text, ""); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider using
..Default::default()for consistency.The explicit fields
stream_reasoning,max_buffer_size, andalways_in_reasoning: falseall match the default values. For consistency with thedeepseek_v31and passthrough parser registrations, consider simplifying:♻️ Suggested simplification
registry.register_parser("kimi_k25", { let markers = kimi_tool_markers.clone(); move || { let config = ParserConfig { think_start_token: "<think>".to_string(), think_end_token: "</think>".to_string(), - stream_reasoning: true, - max_buffer_size: DEFAULT_MAX_BUFFER_SIZE, - always_in_reasoning: false, tool_section_start_markers: markers.clone(), + ..Default::default() }; Box::new(BaseReasoningParser::new(config).with_model_type("kimi_k25".to_string())) } });🤖 Prompt for AI Agents