main
py 128 lines 4.79 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Iterable, Sequence
5 from itertools import islice
6 from typing import TYPE_CHECKING
7
8 import regex as re
9 from transformers import PreTrainedTokenizerBase
10
11 from vllm.entrypoints.generate.base.protocol import DeltaMessage
12 from vllm.reasoning import ReasoningParser
13
14 if TYPE_CHECKING:
15 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
16 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
17
18
19 class Step3ReasoningParser(ReasoningParser):
20 """Reasoning parser for Step3 model.
21
22 The Step3 model uses </think> token to denote the end of reasoning
23 text. This parser extracts all content before </think> as reasoning content.
24 """
25
26 def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
27 super().__init__(tokenizer, *args, **kwargs)
28 self.think_start_token = "<think>"
29 self.think_end_token = "</think>"
30
31 self.reasoning_regex = re.compile(rf"(.*?){self.think_end_token}", re.DOTALL)
32
33 if not self.model_tokenizer:
34 raise ValueError(
35 "The model tokenizer must be passed to the ReasoningParser "
36 "constructor during construction."
37 )
38
39 think_end_token_id = self.vocab.get(self.think_end_token)
40 if think_end_token_id is None:
41 raise RuntimeError(
42 "Step3 reasoning parser could not locate think end "
43 "token in the tokenizer!"
44 )
45 self.think_end_token_id: int = think_end_token_id
46
47 @property
48 def reasoning_start_str(self) -> str:
49 return self.think_start_token
50
51 @property
52 def reasoning_end_str(self) -> str:
53 return self.think_end_token
54
55 def extract_reasoning_streaming(
56 self,
57 previous_text: str,
58 current_text: str,
59 delta_text: str,
60 previous_token_ids: Sequence[int],
61 current_token_ids: Sequence[int],
62 delta_token_ids: Sequence[int],
63 ) -> DeltaMessage | None:
64 """Extract reasoning content from a delta message.
65 Handles streaming output where previous + delta = current.
66 Uses token IDs for faster processing.
67 For text "abc</think>xyz":
68 - 'abc' goes to reasoning
69 - 'xyz' goes to content
70 """
71 # Skip single special token
72 if len(delta_token_ids) == 1 and delta_token_ids[0] == self.think_end_token_id:
73 return None
74
75 if self.think_end_token_id in delta_token_ids:
76 # </think> in delta, extract reasoning content and remaining content
77 end_index = delta_text.find(self.think_end_token)
78 reasoning = delta_text[:end_index]
79 content = delta_text[end_index + len(self.think_end_token) :]
80 return DeltaMessage(
81 reasoning=reasoning,
82 content=content if content else None,
83 )
84 elif self.think_end_token_id in previous_token_ids:
85 # </think> already seen in previous text, everything is content
86 return DeltaMessage(content=delta_text)
87 else:
88 # No </think> seen yet, everything is reasoning
89 return DeltaMessage(reasoning=delta_text)
90
91 def extract_reasoning(
92 self, model_output: str, request: "ChatCompletionRequest | ResponsesRequest"
93 ) -> tuple[str | None, str | None]:
94 # Check if the model output contains the </think> token
95 if self.think_end_token not in model_output:
96 # If no </think> token, everything is reasoning content
97 return model_output, None
98 else:
99 # Find the first occurrence of </think>
100 end_index = model_output.find(self.think_end_token)
101 reasoning = model_output[:end_index]
102
103 # Content after </think> token
104 content = model_output[end_index + len(self.think_end_token) :] or None
105
106 return reasoning, content
107
108 def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
109 return self.think_end_token_id in input_ids
110
111 def is_reasoning_end_streaming(
112 self, input_ids: Sequence[int], delta_ids: Iterable[int]
113 ) -> bool:
114 end_token_id = self.think_end_token_id
115 return end_token_id in delta_ids
116
117 def extract_content_ids(self, input_ids: list[int]) -> list[int]:
118 if self.think_end_token_id not in islice(
119 input_ids, 0, max(0, len(input_ids) - 1)
120 ):
121 return []
122 else:
123 return input_ids[input_ids.index(self.think_end_token_id) + 1 :]
124
125 def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
126 if self.think_end_token_id not in token_ids:
127 return len(token_ids)
128 return list(token_ids).index(self.think_end_token_id)