[Bug Fix] Fix reasoning parser when continue_final_message=true (#17065)

Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
laixin
2026-01-27 14:04:44 +08:00
committed by GitHub
parent df42f4d386
commit 6c9b054ab7
4 changed files with 100 additions and 15 deletions

View File

@@ -1274,7 +1274,7 @@ async def separate_reasoning_request(obj: SeparateReasoningReqInput, request: Re
A native API endpoint to separate reasoning from a text.
"""
# 1) Initialize the parser based on the request body
parser = ReasoningParser(model_type=obj.reasoning_parser)
parser = ReasoningParser(model_type=obj.reasoning_parser, request=request)
# 2) Call the non-stream parsing method (non-stream)
reasoning_text, normal_text = parser.parse_non_stream(obj.text)

View File

@@ -917,6 +917,7 @@ class OpenAIServingChat(OpenAIServingBase):
model_type=reasoning_parser,
stream_reasoning=False,
force_reasoning=is_force_reasoning,
request=request,
)
reasoning_text, text = parser.parse_non_stream(text)
except Exception as e:
@@ -1166,6 +1167,7 @@ class OpenAIServingChat(OpenAIServingBase):
self.reasoning_parser,
request.stream_reasoning,
is_force_reasoning,
request,
)
reasoning_parser = reasoning_parser_dict[index]
return reasoning_parser.parse_stream_chunk(delta)

View File

@@ -530,7 +530,9 @@ class OpenAIServingResponses(OpenAIServingChat):
if self.reasoning_parser:
# Use standard reasoning parser (openai maps to T4Detector internally)
reasoning_parser = ReasoningParser(
model_type=self.reasoning_parser, stream_reasoning=False
model_type=self.reasoning_parser,
stream_reasoning=False,
request=request,
)
reasoning_content, content = reasoning_parser.parse_non_stream(final_output)
else:

View File

@@ -1,5 +1,6 @@
from typing import Dict, Optional, Tuple, Type
from sglang.srt.entrypoints.openai.protocol import ChatCompletionRequest
from sglang.srt.parser.harmony_parser import HarmonyParser
@@ -24,6 +25,8 @@ class BaseReasoningFormatDetector:
think_end_token: str,
force_reasoning: bool = False,
stream_reasoning: bool = True,
continue_final_message: bool = False,
previous_content: str = "",
):
self.think_start_token = think_start_token
self.think_end_token = think_end_token
@@ -33,6 +36,19 @@ class BaseReasoningFormatDetector:
self._buffer = ""
self.stripped_think_start = False
self.continue_final_message = continue_final_message
if self.continue_final_message:
self.previous_content = previous_content
self.previous_count = len(previous_content)
else:
self.previous_content = ""
self.previous_count = 0
if self.think_start_token in self.previous_content:
self._in_reasoning = True
if self.think_end_token in self.previous_content:
self._in_reasoning = False
def detect_and_parse(self, text: str) -> StreamingParseResult:
"""
One-time parsing: Detects and parses reasoning sections in the provided text.
@@ -46,18 +62,25 @@ class BaseReasoningFormatDetector:
# The text is considered to be in a reasoning block.
processed_text = text.replace(self.think_start_token, "").strip()
if self.think_end_token not in processed_text:
if (
self.think_end_token not in processed_text
and self.think_end_token not in self.previous_content
):
# Assume reasoning was truncated before `</think>` token
return StreamingParseResult(reasoning_text=processed_text)
# Extract reasoning content
splits = processed_text.split(self.think_end_token, maxsplit=1)
reasoning_text = splits[0]
normal_text = splits[1].strip()
if self.think_end_token in processed_text:
splits = processed_text.split(self.think_end_token, maxsplit=1)
reasoning_text = splits[0]
normal_text = splits[1].strip()
return StreamingParseResult(
normal_text=normal_text, reasoning_text=reasoning_text
)
return StreamingParseResult(
normal_text=normal_text, reasoning_text=reasoning_text
)
else:
# think_end_token is in self.previous_content for continue_final_message=True case
return StreamingParseResult(normal_text=processed_text)
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
"""
@@ -137,13 +160,21 @@ class DeepSeekR1Detector(BaseReasoningFormatDetector):
If True, streams reasoning content as it arrives.
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = True):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = True,
continue_final_message: bool = False,
previous_content: str = "",
):
# DeepSeek-R1 is assumed to be reasoning until `</think>` token
super().__init__(
"<think>",
"</think>",
force_reasoning=True,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
# https://github.com/sgl-project/sglang/pull/3202#discussion_r1950153599
@@ -184,12 +215,20 @@ class Qwen3Detector(BaseReasoningFormatDetector):
If True, streams reasoning content as it arrives.
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = False):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = False,
continue_final_message: bool = False,
previous_content: str = "",
):
super().__init__(
"<think>",
"</think>",
force_reasoning=force_reasoning,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
@@ -202,12 +241,20 @@ class KimiDetector(BaseReasoningFormatDetector):
and the rest of the text as `normal_text`.
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = False):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = False,
continue_final_message: bool = False,
previous_content: str = "",
):
super().__init__(
"◁think▷",
"◁/think▷",
force_reasoning=False,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
@@ -216,12 +263,20 @@ class GptOssDetector(BaseReasoningFormatDetector):
Detector for T4-style reasoning format (GPT-OSS), using the HarmonyParser.
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = True):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = True,
continue_final_message: bool = False,
previous_content: str = "",
):
super().__init__(
"<|channel|>analysis<|message|>",
"<|end|>",
force_reasoning=force_reasoning,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
self.parser = HarmonyParser()
@@ -274,13 +329,21 @@ class MiniMaxAppendThinkDetector(BaseReasoningFormatDetector):
Append `<think>` token to the beginning of the text.
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = False):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = False,
continue_final_message: bool = False,
previous_content: str = "",
):
# scheduler.py need `reasoning_parser.detector.think_end_token`
super().__init__(
"<think>",
"</think>",
force_reasoning=force_reasoning,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
self.is_first_chunk = False
@@ -301,12 +364,20 @@ class NanoV3Detector(BaseReasoningFormatDetector):
"""
def __init__(self, stream_reasoning: bool = True, force_reasoning: bool = False):
def __init__(
self,
stream_reasoning: bool = True,
force_reasoning: bool = False,
continue_final_message: bool = False,
previous_content: str = "",
):
super().__init__(
"<think>",
"</think>",
force_reasoning=force_reasoning,
stream_reasoning=stream_reasoning,
continue_final_message=continue_final_message,
previous_content=previous_content,
)
@@ -342,6 +413,7 @@ class ReasoningParser:
model_type: Optional[str] = None,
stream_reasoning: bool = True,
force_reasoning: Optional[bool] = None,
request: ChatCompletionRequest = None,
):
if not model_type:
raise ValueError("Model type must be specified")
@@ -359,6 +431,15 @@ class ReasoningParser:
if force_reasoning is not None:
kwargs["force_reasoning"] = force_reasoning
if (
request is not None
and isinstance(request, ChatCompletionRequest)
and request.continue_final_message
and request.messages[-1].role == "assistant"
):
kwargs["continue_final_message"] = True
kwargs["previous_content"] = request.messages[-1].content
self.detector = detector_class(**kwargs)
def parse_non_stream(self, full_text: str) -> Tuple[Optional[str], Optional[str]]: