main
py 226 lines 9.16 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Sequence
5 from typing import TYPE_CHECKING
6
7 import regex as re
8 from transformers import PreTrainedTokenizerBase
9
10 from vllm.entrypoints.generate.base.protocol import DeltaMessage
11 from vllm.reasoning import ReasoningParser
12
13 if TYPE_CHECKING:
14 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
15 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
16
17
18 class HunyuanA13BReasoningParser(ReasoningParser):
19 """Reasoning parser for Hunyuan A13B Model.
20
21 HunyuanReasoningParser
22
23 This class implements a reasoning parser specifically designed
24 for the Hunyuan A13B Model. It is responsible for parsing and
25 extracting structured reasoning and answer segments from model
26 outputs that follow a specific pattern.
27
28 Key Features:
29 - For non-stream output , Recognizes and extracts reasoning ("think")
30 and answer ("answer") sections from text using regular expressions.
31 - For stream process, it requires a token id sequences to change the
32 reasoning state and other state so it maintains internal state to
33 manage parsing across multiple token.
34
35
36 think start: "<think>\n": [14023, 771, 397]
37 think ends: "\n</think>\n<answer>\n": [198, 524, 27963, 397, 27, 9399, 397]
38 response ends: "\n</answer>": [524, 9399, 29]
39 """
40
41 def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
42 super().__init__(tokenizer, *args, **kwargs)
43 self.think_start_expr = r"<think>\n"
44 self.think_end_expr = r"\n</think>\n"
45
46 self.response_start_expr = r"\n</think>\n<answer>\n"
47 self.response_end_expr = r"\n</answer>"
48
49 self.full_match_reasoning_regex = re.compile(
50 rf"(?:{self.think_start_expr}(.*?){self.response_start_expr})?(.*?){self.response_end_expr}",
51 re.DOTALL,
52 )
53
54 self.half_match_reasoning_regex = re.compile(
55 rf"{self.think_start_expr}(.*?){self.response_start_expr}(.*)", re.DOTALL
56 )
57
58 self.think_start_ids = [14023, 771, 397]
59 self.think_start_ids_fast = [14023, 771, 1363]
60 self.response_start_ids = [198, 524, 27963, 397, 27, 9399, 397]
61 self.response_start_ids_fast = [524, 27963, 397, 27, 9399, 397]
62 self.response_end_ids = [198, 524, 9399, 29]
63 self.fast_think_ids = [14023, 771, 1363, 524, 27963, 397, 27, 9399, 397]
64
65 # when state change, send out all the buffered text in last state
66 self.buffered_text: list[str] = []
67 self.buffered_ids: list[int] = []
68
69 self.current_state = "reasoning"
70 self.all_states = ["reasoning", "response"]
71
72 self.current_state = "idle"
73 self.expected_sequence = self.think_start_ids
74 # this sequence only for the think start, it has two way to start.
75 self.expected_sequence_side = self.think_start_ids_fast
76 self.sequence_index = 0
77 self.token_buffer: list[int] = []
78 self.text_buffer = ""
79
80 def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
81 return self.current_state == "response"
82
83 def extract_content_ids(self, input_ids: list[int]) -> list[int]:
84 # for hunyuan streaming reason parsing, the stream parse
85 # will call first, and the same token will be called in
86 # is_reasoning_end and extract_content_ids
87 # this id is not part of content, so just return [] here.
88 return []
89
90 def extract_reasoning(
91 self, model_output: str, request: "ChatCompletionRequest | ResponsesRequest"
92 ) -> tuple[str | None, str | None]:
93 """Extract the reasoning content & content sections, respectively.
94 If the sequence doesn't match what we expect, i.e., the model generates
95 something else, all content is considered non-reasoning content.
96
97 Args:
98 model_output (str): Output of the model to be parsed.
99 request (ChatCompletionRequest): Request being processed.
100
101 Returns:
102 tuple[Optional[str], Optional[str]]: Tuple pair containing the
103 reasoning content and non-reasoning content.
104
105 """
106 re_match = self.full_match_reasoning_regex.findall(model_output)
107 if re_match:
108 reasoning, response_content = re_match[0]
109 if len(reasoning) == 0:
110 reasoning = None
111 if len(response_content) == 0:
112 response_content = None
113 return reasoning, response_content
114
115 fallback_regex = self.half_match_reasoning_regex
116 fallback_match = fallback_regex.findall(model_output)
117 if fallback_match:
118 reasoning, response_content = fallback_match[0]
119
120 if response_content.endswith(self.response_end_expr):
121 response_content = response_content[: -len(self.response_end_expr)]
122
123 if len(reasoning) == 0:
124 reasoning = None
125 if len(response_content) == 0:
126 response_content = None
127
128 return reasoning, response_content
129
130 return None, model_output
131
132 def extract_reasoning_streaming(
133 self,
134 previous_text: str,
135 current_text: str,
136 delta_text: str,
137 previous_token_ids: Sequence[int],
138 current_token_ids: Sequence[int],
139 delta_token_ids: Sequence[int],
140 ) -> DeltaMessage | None:
141 """Extract content using token ID sequence state machine."""
142 # Define sequences
143 think_start_sequence = self.think_start_ids
144 response_start_sequence = self.response_start_ids
145 response_end_sequence = self.response_end_ids
146
147 assert len(delta_token_ids) == 1
148 # Process each token in the delta
149 token = delta_token_ids[0]
150
151 def check_token_with_sequence(token):
152 if self.current_state == "idle" or self.current_state == "think":
153 return (
154 token == self.expected_sequence[self.sequence_index]
155 or token == self.expected_sequence_side[self.sequence_index]
156 )
157 else:
158 return token == self.expected_sequence[self.sequence_index]
159
160 def check_last_token(token):
161 if self.current_state == "idle" or self.current_state == "think":
162 # only return true if it's judge using a side sequence.
163 if (
164 self.sequence_index - 1 < len(self.expected_sequence_side)
165 and token == self.expected_sequence_side[self.sequence_index - 1]
166 ):
167 return self.sequence_index == len(self.expected_sequence_side)
168 else:
169 return self.sequence_index == len(self.expected_sequence)
170 else:
171 return self.sequence_index == len(self.expected_sequence)
172
173 # Check if token matches expected sequence
174 token_in_state_seq = check_token_with_sequence(token)
175
176 if token_in_state_seq:
177 # Store matching token
178 self.token_buffer.append(token)
179 self.text_buffer += delta_text
180 self.sequence_index += 1
181 ## state change from idle->think->response->idle
182
183 # Check if sequence fully matched
184 if check_last_token(token):
185 # State transition
186 if self.current_state == "idle":
187 self.current_state = "think"
188 self.expected_sequence = response_start_sequence
189 self.expected_sequence_side = self.response_start_ids_fast
190 elif self.current_state == "think":
191 self.current_state = "response"
192 self.expected_sequence = response_end_sequence
193 elif self.current_state == "response":
194 self.current_state = "idle"
195 self.expected_sequence = think_start_sequence
196 self.expected_sequence_side = self.think_start_ids_fast
197
198 # Reset matching state
199 self.sequence_index = 0
200 self.token_buffer = []
201 self.text_buffer = ""
202 # Do not send content for state transition texts.
203 else:
204 # Sequence broken - handle buffered content
205 if self.token_buffer and len(self.token_buffer) > 0:
206 # Send buffered tokens
207 buffered_content = self.text_buffer + delta_text
208 # Reset matching state
209 self.sequence_index = 0
210 self.token_buffer = []
211 self.text_buffer = ""
212
213 # Return content based on current state
214 if self.current_state == "think":
215 return DeltaMessage(reasoning=buffered_content, content=None)
216 else:
217 return DeltaMessage(reasoning=None, content=buffered_content)
218 else:
219 # No buffered content, send normally
220 if self.current_state == "think":
221 return DeltaMessage(reasoning=delta_text, content=None)
222 else:
223 return DeltaMessage(reasoning=None, content=delta_text)
224
225 # If no content to send in this delta
226 return None