@@ -116,6 +116,9 @@ def resolve_prefilled_thinking(cls, reasoning_parser: str,
116116 parser, parser has not opted in, or neither marker is at
117117 the tail). Callers must treat this as "ask elsewhere"
118118 (e.g. the relayed disagg value), not as thinking-off.
119+ A prompt ending with neither marker is referred to the parser's
120+ `resolve_unmarked_prompt`, which returns None unless the parser
121+ recognises the tail.
119122 """
120123 entry = cls ._parsers .get (reasoning_parser .lower ())
121124 if entry is None :
@@ -129,7 +132,7 @@ def resolve_prefilled_thinking(cls, reasoning_parser: str,
129132 return False
130133 if tail .endswith (parser_cls .reasoning_start ):
131134 return True
132- return None
135+ return parser_cls . resolve_unmarked_prompt ( tail )
133136
134137 @classmethod
135138 def keys (cls ) -> KeysView [str ]:
@@ -163,6 +166,15 @@ class BaseReasoningParser(ABC):
163166 reasoning_start : ClassVar [str ]
164167 reasoning_end : ClassVar [str ]
165168
169+ @classmethod
170+ def resolve_unmarked_prompt (cls , tail : str ) -> Optional [bool ]:
171+ """Mode of a rendered prompt whose stripped tail ends with neither marker.
172+
173+ None leaves the mode unresolved, which is right for a template that
174+ prefills a marker whenever it prefills anything at all.
175+ """
176+ return None
177+
166178 def __init__ (self ,
167179 * ,
168180 chat_template_kwargs : Optional [dict [str , Any ]] = None ) -> None :
@@ -542,35 +554,46 @@ class MiniMaxM3ReasoningParser(DeepSeekR1Parser):
542554 tag; with thinking disabled it prefills ``</mm:think>``, so the output is
543555 all content; in adaptive mode (the template default) it prefills nothing
544556 and the model emits ``<mm:think>`` or the bare sentinel first. The parser
545- therefore takes its mode from an explicit ``thinking_mode`` or from the
546- mode the server resolved off the rendered prompt (relayed as
547- ``enable_thinking``), and otherwise starts outside reasoning. Both
548- tags are delimiters and never appear in the result; text before an
549- opening tag is content; an opening tag without a closing tag is
550- reasoning cut off by the token limit. ``parse`` and ``parse_delta``
551- agree on every input for every chunking.
557+ takes its mode from the one the server resolved off the rendered prompt
558+ (relayed as ``enable_thinking``), falls back to ``thinking_mode`` when
559+ nothing was resolved, and otherwise starts outside reasoning. Both tags
560+ are delimiters and never appear in the result; text before an opening tag
561+ is content; an opening tag without a closing tag is reasoning cut off by
562+ the token limit. ``parse`` and ``parse_delta`` agree for every chunking.
552563 """
553564
554565 resolves_thinking_from_prompt = True
555566 reasoning_start = "<mm:think>"
556567 reasoning_end = "</mm:think>"
568+ # The template's generation prompt is ``bos_token ~ 'ai' ~ '\\n'``.
569+ _assistant_header = "]~b]ai"
570+
571+ @classmethod
572+ def resolve_unmarked_prompt (cls , tail : str ) -> Optional [bool ]:
573+ # Adaptive mode prefills nothing after the header, so the output
574+ # starts outside reasoning. Any other unmarked tail, such as a custom
575+ # template that prefills text after ``<mm:think>``, carries no mode the
576+ # tail can read and is left to ``thinking_mode``.
577+ if tail .endswith (cls ._assistant_header ):
578+ return False
579+ return None
557580
558581 def __init__ (self ,
559582 * ,
560583 chat_template_kwargs : Optional [dict [str , Any ]] = None ) -> None :
561584 kwargs = chat_template_kwargs or {}
562- # The template's own knob is `thinking_mode`; only "enabled" prefills
563- # the opening tag. Without it, trust the mode the server resolved
564- # from the rendered prompt, relayed as `enable_thinking` (a caller
565- # cannot pass that key: the template never reads it, so the
566- # unused-kwargs guard rejects it). A bare `thinking` is what the
567- # Responses path injects for `reasoning.effort`; this template does
568- # not act on it, so neither does the parser.
585+ # The server overwrites `enable_thinking` with the mode resolved from
586+ # the rendered prompt, so it wins over `thinking_mode`, which decides
587+ # only when nothing was resolved. The Responses path's bare `thinking`
588+ # is ignored because the template never reads it.
589+ resolved = kwargs .get ("enable_thinking" )
569590 thinking_mode = kwargs .get ("thinking_mode" )
570- if thinking_mode is not None :
591+ if resolved is not None :
592+ prefilled_open = resolved is True
593+ elif thinking_mode is not None :
571594 prefilled_open = thinking_mode == "enabled"
572595 else :
573- prefilled_open = kwargs . get ( "enable_thinking" ) is True
596+ prefilled_open = False
574597 super ().__init__ (reasoning_at_start = prefilled_open ,
575598 chat_template_kwargs = chat_template_kwargs )
576599 self .reasoning_start = "<mm:think>"
0 commit comments