main
py 157 lines 6.49 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 from transformers import PreTrainedTokenizerBase
8
9 from vllm.entrypoints.generate.base.protocol import DeltaMessage
10 from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
11
12 if TYPE_CHECKING:
13 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
14 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
15
16
17 class Ernie45ReasoningParser(BaseThinkingReasoningParser):
18 """Reasoning parser for Ernie45 thinking model.
19 The Ernie45 thinking model output format is
20 abc\n</think>\n\n<response>\ndef\n</response>\n
21 or abc\n</think>\ndef
22 """
23
24 response_start_token: str = "<response>"
25 response_end_token: str = "</response>"
26 newline_token: str = "<0x0A>"
27
28 @property
29 def start_token(self) -> str:
30 """The token that starts reasoning content."""
31 return "<think>"
32
33 @property
34 def end_token(self) -> str:
35 """The token that ends reasoning content."""
36 return "</think>"
37
38 def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
39 super().__init__(tokenizer, *args, **kwargs)
40
41 if not self.model_tokenizer:
42 raise ValueError(
43 "The model tokenizer must be passed to the ReasoningParser "
44 "constructor during construction."
45 )
46
47 self.response_start_token_id = self.vocab.get(self.response_start_token)
48 self.response_end_token_id = self.vocab.get(self.response_end_token)
49 self.newline_token_id = self.vocab.get(self.newline_token)
50
51 self.parser_token_ids = [self.end_token_id, self.response_end_token_id]
52
53 def extract_reasoning_streaming(
54 self,
55 previous_text: str,
56 current_text: str,
57 delta_text: str,
58 previous_token_ids: Sequence[int],
59 current_token_ids: Sequence[int],
60 delta_token_ids: Sequence[int],
61 ) -> DeltaMessage | None:
62 """Extract reasoning content from a delta message.
63 Handles streaming output where previous + delta = current.
64 Uses token IDs for faster processing.
65 The Ernie45 thinking model output format is
66 abc\n</think>\n\n<response>\ndef\n</response>\n
67 or abc\n</think>\ndef
68 - 'abc' goes to reasoning
69 - 'def' goes to content
70 """
71 # Skip single special tokens
72 if len(delta_token_ids) == 1 and (
73 delta_token_ids[0]
74 in [
75 self.start_token_id,
76 self.end_token_id,
77 self.response_start_token_id,
78 self.response_end_token_id,
79 ]
80 ):
81 return None
82
83 # No <think> in previous or delta, also need to check for </think>.
84 # Because the model may have generated </think> without <think>
85 if self.end_token_id in delta_token_ids:
86 # </think> in delta with more tokens,
87 # extract reasoning content and content
88 think_end_index = delta_text.find(self.end_token)
89 reasoning = delta_text[:think_end_index]
90 content = delta_text[think_end_index + len(self.end_token) :]
91 content = content.lstrip("\n")
92 response_start_idx = content.find(self.response_start_token)
93 response_end_idx = content.rfind(self.response_end_token)
94 if response_start_idx != -1:
95 content = content[response_start_idx + len(self.response_start_token) :]
96 if response_end_idx != -1:
97 content = content[:response_end_idx]
98 return DeltaMessage(
99 reasoning=reasoning,
100 content=content if content else None,
101 )
102 elif self.end_token_id in previous_token_ids:
103 # </think> in previous, thinking content ends
104 content = delta_text
105 if self.response_start_token_id in delta_token_ids:
106 content = content.lstrip("\n")
107 response_start_idx = content.find(self.response_start_token)
108 content = content[response_start_idx + len(self.response_start_token) :]
109 # if have </response>, remove it
110 response_end_idx = content.rfind(self.response_end_token)
111 if response_end_idx != -1:
112 content = content[:response_end_idx]
113 elif self.response_end_token_id in delta_token_ids:
114 response_end_idx = content.rfind(self.response_end_token)
115 if response_end_idx != -1:
116 content = content[:response_end_idx]
117 # remove \n after </think> or </response>
118 if previous_token_ids[-1] in self.parser_token_ids and (
119 len(delta_token_ids) > 0 and delta_token_ids[0] == self.newline_token_id
120 ):
121 content = content.lstrip("\n")
122 # remove \n after </think>\n
123 if (
124 len(previous_token_ids) > 1
125 and previous_token_ids[-2] == self.end_token_id
126 ) and (
127 len(delta_token_ids) > 0 and delta_token_ids[0] == self.newline_token_id
128 ):
129 content = content.lstrip("\n")
130
131 return DeltaMessage(content=content if content else None)
132 else:
133 # no </think> in previous or delta, reasoning content continues
134 return DeltaMessage(reasoning=delta_text)
135
136 def extract_reasoning(
137 self, model_output: str, request: "ChatCompletionRequest | ResponsesRequest"
138 ) -> tuple[str | None, str | None]:
139 """Extract reasoning content from the model output.
140 The Ernie45 thinking model output format is
141 abc\n</think>\n\n\n<response>\ndef\n</response>\n
142 or abc\n</think>\ndef
143 - 'abc' goes to reasoning
144 - 'def' goes to content
145 Returns:
146 tuple[Optional[str], Optional[str]]: reasoning content and content
147 """
148 reasoning, content = super().extract_reasoning(model_output, request)
149 if content:
150 start_idx = content.find(self.response_start_token)
151 end_idx = content.rfind(self.response_end_token)
152 # Simultaneously existing and in the correct order
153 if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
154 content = content[start_idx + len(self.response_start_token) : end_idx]
155 final_content = content or None
156
157 return reasoning, final_content