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]]: