diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py
index d24c7739c..0bcbd4a37 100644
--- a/python/sglang/srt/entrypoints/http_server.py
+++ b/python/sglang/srt/entrypoints/http_server.py
@@ -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)
diff --git a/python/sglang/srt/entrypoints/openai/serving_chat.py b/python/sglang/srt/entrypoints/openai/serving_chat.py
index c59f4b443..f833120db 100644
--- a/python/sglang/srt/entrypoints/openai/serving_chat.py
+++ b/python/sglang/srt/entrypoints/openai/serving_chat.py
@@ -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)
diff --git a/python/sglang/srt/entrypoints/openai/serving_responses.py b/python/sglang/srt/entrypoints/openai/serving_responses.py
index b2f22c1b5..d2c46ed06 100644
--- a/python/sglang/srt/entrypoints/openai/serving_responses.py
+++ b/python/sglang/srt/entrypoints/openai/serving_responses.py
@@ -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:
diff --git a/python/sglang/srt/parser/reasoning_parser.py b/python/sglang/srt/parser/reasoning_parser.py
index 72477a4b6..87535d95e 100644
--- a/python/sglang/srt/parser/reasoning_parser.py
+++ b/python/sglang/srt/parser/reasoning_parser.py
@@ -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 `` 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 `` token
super().__init__(
"",
"",
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__(
"",
"",
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 `` 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__(
"",
"",
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__(
"",
"",
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]]: