[Bugfix] Fix Step-3.5 reasoning parser for structured outputs (#53174)
Signed-off-by: Yifan Zong <yzong@redhat.com>
This commit is contained in:
@@ -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 </think>
|
||||
COMPLETE_REASONING = {
|
||||
"output": "This is a reasoning section</think>",
|
||||
"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": "<think>This is a reasoning section</think>",
|
||||
"reasoning": "This is a reasoning section",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
MULTIPLE_LINES_WITH_THINK = {
|
||||
"output": "<think>This\nThat</think>This is the rest\nThat",
|
||||
@@ -113,21 +112,21 @@ EMPTY_STREAMING = {
|
||||
NEW_LINE = {
|
||||
"output": "\n<think>This is a reasoning section</think>\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": "\n<think>This is a reasoning section\n</think>\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</think>\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 = "<think>calc</think>\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"
|
||||
|
||||
@@ -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 <think>...</think> format, but it tends to emit an extra
|
||||
newline immediately before and/or after the </think> token. This parser trims:
|
||||
- the newline right before </think>
|
||||
- the newline right after </think>
|
||||
newline immediately before and/or after the </think> token. This parser
|
||||
trims the newline before </think> in extract_reasoning*.
|
||||
|
||||
Reasoning end detection doesn't wait for an extra token after </think> so
|
||||
structured output can constrain the first post-think token.
|
||||
"""
|
||||
|
||||
@property
|
||||
@@ -38,60 +40,6 @@ class Step3p5ReasoningParser(BaseThinkingReasoningParser):
|
||||
# whether it is immediately before </think>.
|
||||
self._pending_reasoning_newline = False
|
||||
|
||||
# Tracks whether we've seen </think> 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, <think> or </think>.
|
||||
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 </think>, do not
|
||||
# clear the pending state just because the prompt contains <think>.
|
||||
# Streaming deltas should not include <think> 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 </think> 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 </think>, 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 </think>.
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user