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
6 changes: 5 additions & 1 deletion megatron/core/tokenizers/text/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
from megatron.core.tokenizers.text.parsers.nemotron_v3_reasoning_parser import (
NemotronV3ReasoningParser,
)
from megatron.core.tokenizers.text.parsers.qwen3_coder_tool_parser import Qwen3CoderToolParser
from megatron.core.tokenizers.text.parsers.qwen3_coder_tool_parser import (
Qwen3CoderToolCombinedParser,
Qwen3CoderToolParser,
)

PARSER_MAPPING = {
"deepseek-r1-reasoning": DeepSeekR1ReasoningParser,
"qwen3-coder-tool": Qwen3CoderToolParser,
"qwen3-coder-tool-combined": Qwen3CoderToolCombinedParser,
"nemotron-v3-reasoning": NemotronV3ReasoningParser,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ def extract_tool_calls(
class Qwen3CoderToolParser(BaseParser):
"""Parser for Qwen3 Coder style tool calls."""

implicit_reasoning_end_markers = ("<tool_call>",)
streaming_markers = ("<tool_call>", "<function=")

@staticmethod
Expand All @@ -290,3 +289,15 @@ def parse(text: str, **kwargs) -> tuple[str, dict[str, list[dict]]]:
return information.get("content", ""), {"tool_calls": information.get("tool_calls", [])}
else:
return text, {}


class Qwen3CoderToolCombinedParser(Qwen3CoderToolParser):
"""Qwen3 Coder tool parser where <tool_call> also ends an open reasoning block.

`qwen3-coder-tool` leaves a tool call emitted before `</think>` inside the
reasoning text, like vLLM's standalone reasoning parsers paired with the
qwen3_coder tool parser. This variant treats `<tool_call>` as an implicit
end of reasoning and parses it, like vLLM's combined qwen3 parser.
"""

implicit_reasoning_end_markers = ("<tool_call>",)
4 changes: 3 additions & 1 deletion megatron/training/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3025,7 +3025,9 @@ def _add_rl_args(parser):
help='Directory to write RL profiling data. Defaults to {save}/profiles.')
group.add_argument('--rl-inference-parsers', nargs='*', default=[],
help='List of response parsers to enable for RL inference '
'(e.g. --rl-inference-parsers deepseek-r1-reasoning qwen3-coder-tool).')
'(e.g. --rl-inference-parsers deepseek-r1-reasoning qwen3-coder-tool). '
'qwen3-coder-tool-combined additionally treats <tool_call> as the end of '
'an unterminated reasoning block, like vLLM\'s combined qwen3 parser.')
return parser

def _add_training_args(parser):
Expand Down
31 changes: 29 additions & 2 deletions tests/unit_tests/tokenizers/test_text_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ def test_parser_mapping_registers_nemotron_v3_reasoning():
assert PARSER_MAPPING["nemotron-v3-reasoning"] is NemotronV3ReasoningParser


def test_tool_call_marker_implicitly_ends_reasoning_for_downstream_parser():
def test_tool_call_marker_implicitly_ends_reasoning_for_combined_parser():
tool_text = (
"<tool_call><function=bash><parameter=command>echo hi</parameter>" "</function></tool_call>"
)
model_output = f"I should inspect this first.\n{tool_text}"
tool_parser = PARSER_MAPPING["qwen3-coder-tool"]
tool_parser = PARSER_MAPPING["qwen3-coder-tool-combined"]

content, reasoning_info = DeepSeekR1ReasoningParser.parse(
model_output, implicit_reasoning_end_markers=tool_parser.implicit_reasoning_end_markers
Expand Down Expand Up @@ -133,3 +133,30 @@ def test_tool_call_marker_does_not_end_reasoning_unless_configured():
model_output = "reasoning<tool_call>not enabled</tool_call>"

assert DeepSeekR1ReasoningParser.parse(model_output) == ("", {"reasoning": model_output})


def test_strict_tool_parser_keeps_unterminated_reasoning_intact():
tool_text = (
"<tool_call><function=bash><parameter=command>echo hi</parameter>" "</function></tool_call>"
)
model_output = f"I should inspect this first.\n{tool_text}"
tool_parser = PARSER_MAPPING["qwen3-coder-tool"]

assert getattr(tool_parser, "implicit_reasoning_end_markers", ()) == ()

content, reasoning_info = DeepSeekR1ReasoningParser.parse(
model_output,
implicit_reasoning_end_markers=getattr(tool_parser, "implicit_reasoning_end_markers", ()),
)

assert content == ""
assert reasoning_info == {"reasoning": model_output}


def test_parser_mapping_registers_both_qwen3_coder_tool_parsers():
strict = PARSER_MAPPING["qwen3-coder-tool"]
combined = PARSER_MAPPING["qwen3-coder-tool-combined"]

assert issubclass(combined, strict)
assert combined.implicit_reasoning_end_markers == ("<tool_call>",)
assert combined.streaming_markers == strict.streaming_markers
Loading