diff --git a/tests/reasoning/test_step3p5_reasoning_parser.py b/tests/reasoning/test_step3p5_reasoning_parser.py
index 8f62e7a2cb..aeaf940213 100644
--- a/tests/reasoning/test_step3p5_reasoning_parser.py
+++ b/tests/reasoning/test_step3p5_reasoning_parser.py
@@ -25,12 +25,11 @@ SIMPLE_REASONING = {
"content": "This is the rest",
"is_reasoning_end": True,
}
-# need to get into parser again to remove newline after
COMPLETE_REASONING = {
"output": "This is a reasoning section",
"reasoning": "This is a reasoning section",
"content": None,
- "is_reasoning_end": False,
+ "is_reasoning_end": True,
}
NO_CONTENT = {
"output": "This is content",
@@ -72,7 +71,7 @@ COMPLETE_REASONING_WITH_THINK = {
"output": "This is a reasoning section",
"reasoning": "This is a reasoning section",
"content": None,
- "is_reasoning_end": False,
+ "is_reasoning_end": True,
}
MULTIPLE_LINES_WITH_THINK = {
"output": "This\nThatThis is the rest\nThat",
@@ -113,21 +112,21 @@ EMPTY_STREAMING = {
NEW_LINE = {
"output": "\nThis is a reasoning section\nThis is the rest",
"reasoning": "This is a reasoning section",
- "content": "This is the rest",
+ "content": "\nThis is the rest",
"is_reasoning_end": True,
}
NEW_LINE_STREAMING = {
"output": "\nThis is a reasoning section\n\nThis is the rest",
"reasoning": "\nThis is a reasoning section",
- "content": "This is the rest",
+ "content": "\nThis is the rest",
"is_reasoning_end": True,
}
NEW_LINE_STREAMING_COMPLEX_CONTENT = {
"output": "\n This is a \n reasoning section\n\n\n\n\nThis is the rest",
"reasoning": "\n This is a \n reasoning section\n\n",
- "content": "\nThis is the rest",
+ "content": "\n\nThis is the rest",
"is_reasoning_end": True,
}
@@ -301,9 +300,20 @@ def test_reasoning(
# Test is_reasoning_end
output_ids = step3p5_tokenizer.convert_tokens_to_ids(output)
- if streaming:
- is_reasoning_end = parser.is_reasoning_end(output_ids)
- assert is_reasoning_end == param_dict["is_reasoning_end"]
+ is_reasoning_end = parser.is_reasoning_end(output_ids)
+ assert is_reasoning_end == param_dict["is_reasoning_end"]
+
+ # Test is_reasoning_end_streaming
+ current_ids: list[int] = []
+ for token_id in output_ids:
+ current_ids.append(token_id)
+ if token_id == parser.end_token_id:
+ assert parser.is_reasoning_end_streaming(current_ids, [token_id])
+ assert parser.is_reasoning_end_streaming(output_ids, output_ids)
+ break
+ assert not parser.is_reasoning_end_streaming(current_ids, [token_id])
+ else:
+ assert not parser.is_reasoning_end_streaming(output_ids, output_ids)
# Test extract_content
if param_dict["content"] is not None:
@@ -324,18 +334,5 @@ def test_reasoning(
)
assert content == expected_content_ids
else:
- content = parser.extract_content_ids(output)
+ content = parser.extract_content_ids(output_ids)
assert content == []
-
-
-def test_step3p5_streaming_drops_leading_newline(step3p5_tokenizer):
- parser_cls = ReasoningParserManager.get_reasoning_parser("step3p5")
- parser = parser_cls(step3p5_tokenizer)
- output = "calc\nAnswer"
- tokens = step3p5_tokenizer.tokenize(output)
- output_tokens = [
- step3p5_tokenizer.convert_tokens_to_string([token]) for token in tokens
- ]
-
- _, content = run_reasoning_extraction(parser, output_tokens, streaming=True)
- assert content == "Answer"
diff --git a/vllm/reasoning/step3p5_reasoning_parser.py b/vllm/reasoning/step3p5_reasoning_parser.py
index fed6f2edaa..cc7c227e6d 100644
--- a/vllm/reasoning/step3p5_reasoning_parser.py
+++ b/vllm/reasoning/step3p5_reasoning_parser.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
-from collections.abc import Iterable, Sequence
+from collections.abc import Sequence
from typing import TYPE_CHECKING
from vllm.entrypoints.generate.base.protocol import DeltaMessage
@@ -18,9 +18,11 @@ class Step3p5ReasoningParser(BaseThinkingReasoningParser):
Reasoning parser for Step3p5 model.
Step3p5 uses the ... format, but it tends to emit an extra
- newline immediately before and/or after the token. This parser trims:
- - the newline right before
- - the newline right after
+ newline immediately before and/or after the token. This parser
+ trims the newline before in extract_reasoning*.
+
+ Reasoning end detection doesn't wait for an extra token after so
+ structured output can constrain the first post-think token.
"""
@property
@@ -38,60 +40,6 @@ class Step3p5ReasoningParser(BaseThinkingReasoningParser):
# whether it is immediately before .
self._pending_reasoning_newline = False
- # Tracks whether we've seen but are still waiting for one more
- # token to confirm the end.
- self._end_token_pending = False
-
- def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
- return self._is_reasoning_end_from_ids(input_ids)
-
- def is_reasoning_end_streaming(
- self, input_ids: Sequence[int], delta_ids: Iterable[int]
- ) -> bool:
- # Only examine newly generated tokens; they may contain multiple ids.
- return self._is_reasoning_end_from_ids(tuple(delta_ids))
-
- def _is_reasoning_end_from_ids(self, input_ids: Sequence[int]) -> bool:
- # Scan backwards to find the last special token, or .
- last_special = None
- last_idx = -1
- for i in range(len(input_ids) - 1, -1, -1):
- token_id = input_ids[i]
- if token_id == self.start_token_id:
- last_special = "start"
- last_idx = i
- break
- if token_id == self.end_token_id:
- last_special = "end"
- last_idx = i
- break
-
- if last_special == "start":
- # If we're already waiting for one token after , do not
- # clear the pending state just because the prompt contains .
- # Streaming deltas should not include for this model.
- if self._end_token_pending:
- return False
- # A start token after any end token means reasoning is ongoing.
- self._end_token_pending = False
- return False
-
- if last_special == "end":
- # Require at least one token after before ending.
- if last_idx < len(input_ids) - 1:
- self._end_token_pending = False
- return True
- self._end_token_pending = True
- return False
-
- # No special tokens in this input. If we were waiting for one token
- # after , any new token completes the end.
- if self._end_token_pending and input_ids:
- self._end_token_pending = False
- return True
-
- return False
-
def extract_reasoning(
self,
model_output: str,
@@ -100,8 +48,6 @@ class Step3p5ReasoningParser(BaseThinkingReasoningParser):
reasoning, content = super().extract_reasoning(model_output, request)
if reasoning is not None:
reasoning = reasoning.removesuffix("\n")
- if content is not None:
- content = content.removeprefix("\n")
return reasoning or None, content or None
def extract_reasoning_streaming(
@@ -113,14 +59,6 @@ class Step3p5ReasoningParser(BaseThinkingReasoningParser):
current_token_ids: Sequence[int],
delta_token_ids: Sequence[int],
) -> DeltaMessage | None:
- # Drop the immediate newline that models often emit after .
- if previous_text.endswith(self.end_token) and delta_text:
- if delta_text == "\n":
- return None
- elif delta_text.startswith("\n"):
- remaining = delta_text.removeprefix("\n")
- return DeltaMessage(content=remaining) if remaining else None
-
ret = super().extract_reasoning_streaming(
previous_text,
current_text,