Skip to content

[router][tool parser] Modify tool parser to return both normal text and tool calls (non-stream)#10995

Merged
slin1237 merged 10 commits intomainfrom
chang/response
Sep 27, 2025
Merged

[router][tool parser] Modify tool parser to return both normal text and tool calls (non-stream)#10995
slin1237 merged 10 commits intomainfrom
chang/response

Conversation

@CatherineSue
Copy link
Collaborator

@CatherineSue CatherineSue commented Sep 27, 2025

Motivation

Tool parsers should reserve any content before and after tool calls. Especially, when tool call parsing fails, the original text should be preserved as normal text.

The current behavior only returns a tool-calls list. When it is empty due to parsing failures, the user won't get any content. But completion tokens are still counted.

Modifications

  • Fix typo in reasoning_parser/traits.rs

Modify the tool parser system to return both normal text and tool calls

  • Updated ToolParser trait signature - Changed parse_complete from returning ToolParserResult<Vec<ToolCall>> to ToolParserResult<(String, Vec<ToolCall>)>
  • Updated all parser implementations with efficient normal text extraction:
    • JsonParser: Use stack approach in extract_json_from_text to return both normal text and json content
    • MistralParser: Implemented extract_json_array_with_pos() method to extract normal text before the tool calls. (After is ignored)
    • LlamaParser: Extracts text before <|python_tag|> markers
    • QwenParser: Uses captures_iter() with match positions for both tool calls and normal text
    • DeepSeekParser: Uses find_iter() with match positions
    • KimiK2Parser: Uses captures_iter() with match positions
    • PythonicParser: Extract normal texts around the tool call array
    • Step3Parser: Uses regex extractors with match positions
    • GptOssParser: Ignored
  • Fixed router implementation - Use normal_text as processed_text
  • Fixed all test cases - Updated 166+ test cases across multiple files to handle the new (String, Vec) tuple format
  • Add test for parser failure

Proper error handling for parsing failures

  • Capture ParsingFailed error in MistralParser
    • parse_json_array throws a ToolParserError::ParsingFailed error but it is not captured later.
    • Tool Parser should handle parsing failure errors instead of bubbling up to router.
  • Add similar handling in QwenParser

Accuracy Tests

JsonParser

This PR refactored extract_json_from_text to use a stack based approach to extract a valid JSON object from a mixed test.

Here is a detailed comparison:

JSON Parser Implementation Comparison

Test Case Description Old Implementation New Stack-Based Winner
Valid Tool Cases
Valid single tool {"name": "test", "arguments": {"x": 1}} ✅ 1 tool (test) ✅ 1 tool (test) 🤝 Tie
Tool in mixed text prefix {"name": "tool1", "arguments": {}} suffix ✅ 1 tool (tool1) ✅ 1 tool (tool1) 🤝 Tie
Array of tools [{"name": "tool1", "arguments": {}}, {"name": "tool2", "arguments": {}}] ✅ 2 tools (tool1, tool2) ✅ 2 tools (tool1, tool2) 🤝 Tie
Array tool in text text [{"name": "arr_tool", "arguments": {"k": [3]}}] end ✅ 1 tool (arr_tool) ✅ 1 tool (arr_tool) 🤝 Tie
Escaped quotes {"name": "escape", "arguments": {"text": "quote " here"}} ✅ 1 tool (escape) ✅ 1 tool (escape) 🤝 Tie
Nested data before {"name": "nested", "arguments": {"data": {"deep": true}}} after ✅ 1 tool (nested) ✅ 1 tool (nested) 🤝 Tie
Array parameter {"name": "array_param", "arguments": {"list": [1,2,3]}} ✅ 1 tool (array_param) ✅ 1 tool (array_param) 🤝 Tie
Brackets in strings {"name": "string_brackets", "arguments": {"msg": "array [1] and obj {x:1}"}} ✅ 1 tool (string_brackets) ✅ 1 tool (string_brackets) 🤝 Tie
Edge Cases
Stray closer before nonsense} {"name": "after_stray", "arguments": {}} end ✅ 1 tool (after_stray) ✅ 1 tool (after_stray) 🤝 Tie
Fake tool in string text "fake {name: tool}" {"name": "real", "arguments": {}} end ❌ 0 tools ✅ 1 tool (real) 🏆 New
Priority Cases
Array vs object [{"name": "first", "arguments": {}}] {"name": "second", "arguments": {}} ✅ 1 tool (first) ✅ 1 tool (first) 🤝 Tie
Malformed Cases (Should Fail)
Malformed brackets garbage { "name": "bad", "arguments": [1,2} } more ✅ 0 tools (correct) ✅ 0 tools (correct) 🤝 Tie
Incomplete JSON {"name": "incomplete", "arguments": {"x": [1,2} ✅ 0 tools (correct) ✅ 0 tools (correct) 🤝 Tie
Mismatched brackets pre { "name": "mismatch", "arguments": [1,2} then after ✅ 0 tools (correct) ✅ 0 tools (correct) 🤝 Tie

Detailed Edge Case Analysis

Case Input Old Result New Result Analysis
Stray Closer stray} {"name": "tool", "arguments": {}} ✅ Found tool ✅ Found tool Both correctly ignore stray }
String Content text "fake {name: tool}" {"name": "real", "arguments": {}} ❌ No tools found ✅ Found real New correctly ignores string content
Malformed Inner bad{ "name": "tool", "arguments": [1,2} good ✅ No tools (correct) ✅ No tools (correct) Both correctly reject malformed JSON
Array Priority [{"name": "first", "arguments": {}}] {"name": "second", "arguments": {}} ✅ Found first ✅ Found first Both correctly extract array first

Benchmarking and Profiling

Checklist

1. Updated `ToolParser` trait signature - Changed parse_complete from
returning `ToolParserResult<Vec<ToolCall>>` to
`ToolParserResult<(String, Vec<ToolCall>)>`
2. Updated all parser implementations with efficient normal text extraction:
 - JsonParser: Returns empty normal text for all cases (as requested)
 - MistralParser: Implemented `extract_json_array_with_pos()` method
 to eliminate redundant find() calls
 - LlamaParser: Extracts text before `<|python_tag|>` markers
 - QwenParser: Uses `captures_iter()` with match positions for both
 tool calls and normal text
 - DeepSeekParser: Uses `find_iter()` with match positions
 - KimiK2Parser: Uses `captures_iter()` with match positions
 - PythonicParser: Empty normal text (by design)
 - Step3Parser: Uses regex extractors with match positions
 - GptOssParser: Uses `captures_iter()` with match positions
3. Fixed router implementation - Use normal_text as processed_text
4. Fixed all test cases - Updated 166+ test cases across multiple files
to handle the new (String, Vec<ToolCall>) tuple format
5. Fix tool_parser_benchmark.rs
- Use stack based approach to extract json from a mixed text
- Properly handle reset state when we hit malformed JSON
- Add unit tests
- Add normal text extraction in `extract_tool_calls`
- Add unit tests
- When tool call parsing fails, the original text
should be preserved as normal text
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @CatherineSue, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and completeness of the tool parsing system by ensuring that all input text is accounted for. Previously, if tool calls were not found or parsing encountered an error, any non-tool text might have been implicitly discarded. The updated system now explicitly separates and returns both successfully parsed tool calls and any remaining 'normal' text, preventing data loss and providing a more reliable foundation for downstream processing, especially in scenarios involving mixed model outputs or partial parsing failures.

Highlights

  • Enhanced ToolParser Trait: The core ToolParser trait has been updated to return a tuple containing both extracted normal text and a list of tool calls, ensuring no content is lost even if tool calls are not found or parsing fails.
  • Comprehensive Normal Text Extraction: All existing tool parser implementations (JsonParser, MistralParser, LlamaParser, QwenParser, DeepSeekParser, KimiK2Parser, PythonicParser, Step3Parser, GptOssParser) have been modified to correctly identify and extract normal text surrounding or interspersed with tool calls.
  • Robust JSON Parsing Logic: The JsonParser received a significant refactor, now utilizing a stack-based approach for more accurate JSON extraction from mixed text, improving its ability to handle edge cases like stray characters or fake tool calls within strings.
  • Improved Error Handling and Content Preservation: Specific parsers like MistralParser and QwenParser now properly capture ParsingFailed errors, ensuring that if tool call parsing fails, the original text is preserved as normal text instead of being discarded.
  • Extensive Test Coverage Updates: Over 166 existing test cases across various parser implementations have been updated to reflect the new (String, Vec<ToolCall>) return type, and a new dedicated test file (tool_parser_fallback.rs) has been added to validate the content preservation behavior on parsing failures.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a significant and valuable refactoring of the tool parsing logic. The core change to have parse_complete return both normal text and tool calls is well-implemented across the majority of the parsers. The addition of fallback tests is also a great improvement for robustness.

I've identified a few areas for improvement:

  1. A high-severity bug in llama_parser.rs where it incorrectly recalculates normal_text instead of using the value from the underlying json_parser.
  2. A medium-severity issue in several parsers where text between tool calls is lost.
  3. A medium-severity issue in the new fallback tests where assertions are incorrect.
  4. A medium-severity issue in mistral_parser.rs where text after the tool call block is lost.

Addressing these points will make the new parsing logic even more robust and correct. Overall, this is a solid contribution.

- When split among separators we should preserve the separator
- Update tests
- When all cases fail, it should return original text
- Add test case
@CatherineSue CatherineSue changed the title [router][tool parser] Modify tool parser to return both normal text and tool calls [router][tool parser] Modify tool parser to return both normal text and tool calls (non-stream) Sep 27, 2025
@slin1237 slin1237 merged commit c1c8dd1 into main Sep 27, 2025
36 of 37 checks passed
@slin1237 slin1237 deleted the chang/response branch September 27, 2025 22:10
Lekro-xsy pushed a commit to Lekro-xsy/sglang that referenced this pull request Sep 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments