Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/sglang/srt/parser/reasoning_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def __init__(
"</think>",
force_reasoning=force_reasoning,
stream_reasoning=stream_reasoning,
tool_start_token="<tool_call>",
continue_final_message=continue_final_message,
previous_content=previous_content,
)
Expand Down
42 changes: 42 additions & 0 deletions test/registered/unit/parser/test_reasoning_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,48 @@ def test_streaming_qwen3_forced_reasoning_format(self):
self.assertEqual(result.reasoning_text, "") # Buffer cleared
self.assertEqual(result.normal_text, "The answer is 42.")

def test_detect_and_parse_tool_call_without_think_close(self):
"""
Regression test: when force_reasoning=True and the model emits <tool_call>
without first closing </think>, the tool_call must be split into normal_text
so the downstream tool-call parser can still see it. Otherwise the entire
output is silently swallowed into reasoning_content and the function call
is lost (observed with Qwen3.5-27B serving via SGLang in production).
"""
text = "I should call the tool.<tool_call>\n<function=foo>\n</function>\n</tool_call>"
result = self.detector.detect_and_parse(text)
self.assertEqual(result.reasoning_text, "I should call the tool.")
self.assertEqual(
result.normal_text,
"<tool_call>\n<function=foo>\n</function>\n</tool_call>",
)

def test_streaming_tool_call_without_think_close(self):
"""
Streaming regression: same scenario as above but for incremental parsing.
Once <tool_call> appears while still in reasoning state, the parser must
flip to normal_text and forward <tool_call>... downstream.
"""
# Initial reasoning chunks (no </think>)
result = self.detector.parse_streaming_increment("Let me ")
self.assertEqual(result.reasoning_text, "Let me ")
self.assertEqual(result.normal_text, "")

result = self.detector.parse_streaming_increment("call the tool.")
self.assertEqual(result.reasoning_text, "call the tool.")
self.assertEqual(result.normal_text, "")

# Tool call appears WITHOUT a preceding </think>
result = self.detector.parse_streaming_increment(
"<tool_call>\n<function=foo>\n</function>\n</tool_call>"
)
self.assertEqual(result.reasoning_text, "")
self.assertEqual(
result.normal_text,
"<tool_call>\n<function=foo>\n</function>\n</tool_call>",
)
self.assertFalse(self.detector._in_reasoning)


class TestKimiDetector(CustomTestCase):
def setUp(self):
Expand Down
Loading