[Nemotron] Small reasoning parser fix (#20284)
This commit is contained in:
@@ -431,6 +431,7 @@ class Nemotron3Detector(BaseReasoningFormatDetector):
|
|||||||
force_reasoning: bool = False,
|
force_reasoning: bool = False,
|
||||||
continue_final_message: bool = False,
|
continue_final_message: bool = False,
|
||||||
previous_content: str = "",
|
previous_content: str = "",
|
||||||
|
force_nonempty_content: bool = False,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
"<think>",
|
"<think>",
|
||||||
@@ -440,6 +441,13 @@ class Nemotron3Detector(BaseReasoningFormatDetector):
|
|||||||
continue_final_message=continue_final_message,
|
continue_final_message=continue_final_message,
|
||||||
previous_content=previous_content,
|
previous_content=previous_content,
|
||||||
)
|
)
|
||||||
|
self._force_nonempty_content = force_nonempty_content
|
||||||
|
|
||||||
|
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
||||||
|
ret = super().detect_and_parse(text)
|
||||||
|
if self._force_nonempty_content and not ret.normal_text:
|
||||||
|
ret.normal_text, ret.reasoning_text = ret.reasoning_text, ret.normal_text
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class ReasoningParser:
|
class ReasoningParser:
|
||||||
@@ -502,6 +510,10 @@ class ReasoningParser:
|
|||||||
kwargs["continue_final_message"] = True
|
kwargs["continue_final_message"] = True
|
||||||
kwargs["previous_content"] = request.messages[-1].content
|
kwargs["previous_content"] = request.messages[-1].content
|
||||||
|
|
||||||
|
chat_template_kwargs = getattr(request, "chat_template_kwargs", None) or {}
|
||||||
|
if chat_template_kwargs.get("force_nonempty_content") is True:
|
||||||
|
kwargs["force_nonempty_content"] = True
|
||||||
|
|
||||||
self.detector = detector_class(**kwargs)
|
self.detector = detector_class(**kwargs)
|
||||||
|
|
||||||
def parse_non_stream(self, full_text: str) -> Tuple[Optional[str], Optional[str]]:
|
def parse_non_stream(self, full_text: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from sglang.srt.parser.reasoning_parser import (
|
|||||||
Glm45Detector,
|
Glm45Detector,
|
||||||
KimiDetector,
|
KimiDetector,
|
||||||
KimiK2Detector,
|
KimiK2Detector,
|
||||||
|
Nemotron3Detector,
|
||||||
Qwen3Detector,
|
Qwen3Detector,
|
||||||
ReasoningParser,
|
ReasoningParser,
|
||||||
StreamingParseResult,
|
StreamingParseResult,
|
||||||
@@ -512,6 +513,74 @@ class TestGlm45Detector(CustomTestCase):
|
|||||||
self.assertEqual(result.normal_text, "<tool_call>tool call")
|
self.assertEqual(result.normal_text, "<tool_call>tool call")
|
||||||
|
|
||||||
|
|
||||||
|
class TestNemotron3Detector(CustomTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.detector = Nemotron3Detector()
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""Test Nemotron3Detector initialization."""
|
||||||
|
self.assertEqual(self.detector.think_start_token, "<think>")
|
||||||
|
self.assertEqual(self.detector.think_end_token, "</think>")
|
||||||
|
self.assertFalse(self.detector._in_reasoning)
|
||||||
|
self.assertTrue(self.detector.stream_reasoning)
|
||||||
|
self.assertFalse(self.detector._force_nonempty_content)
|
||||||
|
|
||||||
|
def test_detect_and_parse_complete_reasoning(self):
|
||||||
|
"""Test parsing complete reasoning block."""
|
||||||
|
text = "<think>Let me think about this</think>The answer is 42."
|
||||||
|
result = self.detector.detect_and_parse(text)
|
||||||
|
self.assertEqual(result.reasoning_text, "Let me think about this")
|
||||||
|
self.assertEqual(result.normal_text, "The answer is 42.")
|
||||||
|
|
||||||
|
def test_detect_and_parse_no_thinking(self):
|
||||||
|
"""Test parsing without thinking tokens."""
|
||||||
|
text = "Direct answer without thinking."
|
||||||
|
result = self.detector.detect_and_parse(text)
|
||||||
|
self.assertEqual(result.normal_text, text)
|
||||||
|
self.assertEqual(result.reasoning_text, "")
|
||||||
|
|
||||||
|
def test_detect_and_parse_reasoning_only(self):
|
||||||
|
"""Test parsing when output is all reasoning (no content after </think>)."""
|
||||||
|
text = "<think>All reasoning, no answer</think>"
|
||||||
|
result = self.detector.detect_and_parse(text)
|
||||||
|
self.assertEqual(result.reasoning_text, "All reasoning, no answer")
|
||||||
|
self.assertEqual(result.normal_text, "")
|
||||||
|
|
||||||
|
def test_force_nonempty_content_swaps_when_no_normal_text(self):
|
||||||
|
"""Test force_nonempty_content swaps reasoning to content when content is empty."""
|
||||||
|
detector = Nemotron3Detector(force_nonempty_content=True)
|
||||||
|
text = "<think>All reasoning, no answer</think>"
|
||||||
|
result = detector.detect_and_parse(text)
|
||||||
|
self.assertEqual(result.normal_text, "All reasoning, no answer")
|
||||||
|
self.assertEqual(result.reasoning_text, "")
|
||||||
|
|
||||||
|
def test_force_nonempty_content_no_swap_when_normal_text_exists(self):
|
||||||
|
"""Test force_nonempty_content does not swap when content already exists."""
|
||||||
|
detector = Nemotron3Detector(force_nonempty_content=True)
|
||||||
|
text = "<think>Reasoning here</think>The answer is 42."
|
||||||
|
result = detector.detect_and_parse(text)
|
||||||
|
self.assertEqual(result.reasoning_text, "Reasoning here")
|
||||||
|
self.assertEqual(result.normal_text, "The answer is 42.")
|
||||||
|
|
||||||
|
def test_force_nonempty_content_truncated_reasoning(self):
|
||||||
|
"""Test force_nonempty_content with truncated reasoning (no end token)."""
|
||||||
|
detector = Nemotron3Detector(force_nonempty_content=True)
|
||||||
|
text = "<think>Truncated reasoning without end token"
|
||||||
|
result = detector.detect_and_parse(text)
|
||||||
|
# Truncated reasoning has no normal_text, so swap should occur
|
||||||
|
self.assertEqual(result.normal_text, "Truncated reasoning without end token")
|
||||||
|
self.assertEqual(result.reasoning_text, "")
|
||||||
|
|
||||||
|
def test_force_nonempty_content_no_thinking_tokens(self):
|
||||||
|
"""Test force_nonempty_content with plain text (no thinking tokens)."""
|
||||||
|
detector = Nemotron3Detector(force_nonempty_content=True)
|
||||||
|
text = "Plain text without any thinking."
|
||||||
|
result = detector.detect_and_parse(text)
|
||||||
|
# Normal text already exists, no swap needed
|
||||||
|
self.assertEqual(result.normal_text, text)
|
||||||
|
self.assertEqual(result.reasoning_text, "")
|
||||||
|
|
||||||
|
|
||||||
class TestReasoningParser(CustomTestCase):
|
class TestReasoningParser(CustomTestCase):
|
||||||
def test_init_valid_model(self):
|
def test_init_valid_model(self):
|
||||||
"""Test initialization with valid model types."""
|
"""Test initialization with valid model types."""
|
||||||
|
|||||||
Reference in New Issue
Block a user