Add granite_thinking_parser reasoning parser for Granite 4.2 (#38693)
Signed-off-by: Yousaf Shah <yousaf.shah@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com> Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude
Xinyuan Tong
Xinyuan Tong
parent
335f6aab27
commit
593c7a900d
@@ -1043,6 +1043,71 @@ class Nemotron3Detector(BaseReasoningFormatDetector):
|
||||
)
|
||||
|
||||
|
||||
class GraniteThinkingDetector(BaseReasoningFormatDetector):
|
||||
"""Detector for Granite 4.2 thinking models (ibm-granite/granite-4.2-*).
|
||||
|
||||
Strips leading newlines from content after </think> — the Granite chat
|
||||
template writes ``\\n</think>\\n``, so content starts with ``\\n``.
|
||||
Matches the behavior of the HF plugin ``granite_thinking_parser.py``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream_reasoning: bool = True,
|
||||
force_reasoning: bool = False,
|
||||
continue_final_message: bool = False,
|
||||
previous_content: str = "",
|
||||
force_nonempty_content: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
"<think>",
|
||||
"</think>",
|
||||
force_reasoning=force_reasoning,
|
||||
stream_reasoning=stream_reasoning,
|
||||
tool_start_token="<tool_call>",
|
||||
continue_final_message=continue_final_message,
|
||||
previous_content=previous_content,
|
||||
reasoning_default="enable_thinking",
|
||||
force_nonempty_content=force_nonempty_content,
|
||||
)
|
||||
self._content_started = False
|
||||
self._reasoning_seen = False
|
||||
|
||||
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
||||
ret = self._detect_and_parse_impl(text)
|
||||
think_end_present = self.think_end_token in text
|
||||
# HF plugin swaps when parsed content is absent (text ends at </think>);
|
||||
# newline-only content the plugin strips itself, without swapping.
|
||||
content_absent = think_end_present and not ret.normal_text
|
||||
if think_end_present and ret.normal_text:
|
||||
ret.normal_text = ret.normal_text.lstrip("\n")
|
||||
if (
|
||||
self._force_nonempty_content
|
||||
and not ret.normal_text
|
||||
and (content_absent or not think_end_present)
|
||||
):
|
||||
ret.normal_text, ret.reasoning_text = ret.reasoning_text, ret.normal_text
|
||||
return ret
|
||||
|
||||
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
||||
was_in_reasoning = self._in_reasoning
|
||||
ret = super().parse_streaming_increment(new_text)
|
||||
# Sampling only the pre-call state misses a whole think block arriving
|
||||
# in one chunk; post-call evidence keeps stripping chunk-independent.
|
||||
if (
|
||||
was_in_reasoning
|
||||
or self._in_reasoning
|
||||
or self.stripped_think_start
|
||||
or ret.reasoning_text
|
||||
):
|
||||
self._reasoning_seen = True
|
||||
if self._reasoning_seen and not self._content_started and ret.normal_text:
|
||||
ret.normal_text = ret.normal_text.lstrip("\n")
|
||||
if ret.normal_text:
|
||||
self._content_started = True
|
||||
return ret
|
||||
|
||||
|
||||
class MiniMaxM3Detector(BaseReasoningFormatDetector):
|
||||
"""MiniMax-M3 detector. Format: (<mm:think>)*(.*)</mm:think>.
|
||||
|
||||
@@ -2123,6 +2188,7 @@ class ReasoningParser:
|
||||
"step3p5": DeepSeekR1Detector,
|
||||
"mistral": MistralDetector,
|
||||
"nemotron_3": Nemotron3Detector,
|
||||
"granite_thinking_parser": GraniteThinkingDetector,
|
||||
"interns1": Qwen3Detector,
|
||||
"gemma4": Gemma4Detector,
|
||||
"inkling": InklingDetector,
|
||||
@@ -2177,6 +2243,10 @@ class ReasoningParser:
|
||||
if chat_template_kwargs.get("force_nonempty_content") is True:
|
||||
kwargs["force_nonempty_content"] = True
|
||||
|
||||
if model_type.lower() == "granite_thinking_parser":
|
||||
if chat_template_kwargs.get("enable_thinking") is False:
|
||||
kwargs["force_nonempty_content"] = True
|
||||
|
||||
if model_type.lower() == "k2_horizon":
|
||||
# Template kwargs are the final values passed to Jinja and therefore
|
||||
# take precedence over the convenience fields on API requests.
|
||||
|
||||
@@ -314,6 +314,18 @@ def _is_k2_v3(ctx):
|
||||
)
|
||||
|
||||
|
||||
def _is_granite_thinking_parser(ctx):
|
||||
# Nemotron-3 templates share the same <parameter= tool-call block, so it
|
||||
# cannot discriminate; defer_loading is Granite's deferred tool loading.
|
||||
return (
|
||||
ctx.has_text("truncate_history_thinking")
|
||||
and ctx.has_text("defer_loading")
|
||||
and ctx.reasoning_config is not None
|
||||
and ctx.reasoning_config.toggle_param == "enable_thinking"
|
||||
and ctx.reasoning_config.default_enabled is True
|
||||
)
|
||||
|
||||
|
||||
def _is_nemotron_3(ctx):
|
||||
return ctx.has_text("truncate_history_thinking") and (
|
||||
ctx.reasoning_config is not None
|
||||
@@ -502,6 +514,11 @@ REASONING_PARSER_RULES = (
|
||||
DetectionRule(name="mistral", value="mistral", predicate=_is_mistral),
|
||||
DetectionRule(name="gpt_oss", value="gpt-oss", predicate=_is_gpt_oss),
|
||||
DetectionRule(name="kimi_k2", value="kimi_k2", predicate=_is_kimi_k2),
|
||||
DetectionRule(
|
||||
name="granite_thinking_parser",
|
||||
value="granite_thinking_parser",
|
||||
predicate=_is_granite_thinking_parser,
|
||||
),
|
||||
DetectionRule(name="nemotron_3", value="nemotron_3", predicate=_is_nemotron_3),
|
||||
DetectionRule(name="glm45", value="glm45", predicate=_is_glm_family),
|
||||
DetectionRule(name="hunyuan", value="hunyuan", predicate=_is_hunyuan),
|
||||
|
||||
Reference in New Issue
Block a user