main
py 143 lines 5.34 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 typing import TYPE_CHECKING
6
7 from vllm.entrypoints.generate.base.protocol import DeltaMessage
8 from vllm.logger import init_logger
9 from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
10 from vllm.reasoning.identity_reasoning_parser import IdentityReasoningParser
11 from vllm.tokenizers import TokenizerLike
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 logger = init_logger(__name__)
18
19
20 class HYV3ReasoningParser(BaseThinkingReasoningParser):
21 """HYV3 parser that delegates to either HYV3ReasoningParser or
22 IdentityReasoningParser based on `reasoning_effort`.
23
24 The HYV3 model uses <think>...</think> tokens to denote reasoning text.
25 This parser extracts the reasoning content from the model output.
26 """
27
28 def __init__(self, tokenizer: TokenizerLike, *args, **kwargs):
29 init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
30 self.suffix: str = init_kwargs.get("token_suffix") or ""
31 super().__init__(tokenizer, *args, **kwargs)
32
33 # First, If there is reasoning_effort in chat_kwargs,
34 # prioritize using chat_kwargs.reasoning_effort.
35 # If it's not present, use the "reasoning_effort" field
36 # at the outer level of the chat message.
37 # Otherwise, If both are empty, assign "no_think".
38
39 chat_kwargs = kwargs.get("chat_template_kwargs", {}) or {}
40 reasoning_effort = (
41 chat_kwargs.get("reasoning_effort")
42 or kwargs.get("reasoning_effort")
43 or "no_think"
44 )
45
46 logger.debug("reasoning_effort for choosing parser: %s", reasoning_effort)
47
48 self._identity_parser: IdentityReasoningParser | None
49 if reasoning_effort == "no_think":
50 self._identity_parser = IdentityReasoningParser(tokenizer, *args, **kwargs)
51 else:
52 self._identity_parser = None
53
54 @property
55 def start_token(self) -> str:
56 """The token that starts reasoning content."""
57 return f"<think{self.suffix}>"
58
59 @property
60 def end_token(self) -> str:
61 """The token that ends reasoning content."""
62 return f"</think{self.suffix}>"
63
64 def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
65 if self._identity_parser is not None:
66 return self._identity_parser.is_reasoning_end(input_ids)
67
68 return super().is_reasoning_end(input_ids)
69
70 def is_reasoning_end_streaming(
71 self, input_ids: Sequence[int], delta_ids: Iterable[int]
72 ) -> bool:
73 if self._identity_parser is not None:
74 return self._identity_parser.is_reasoning_end_streaming(
75 input_ids, delta_ids
76 )
77
78 return super().is_reasoning_end_streaming(input_ids, delta_ids)
79
80 def extract_content_ids(self, input_ids: list[int]) -> list[int]:
81 if self._identity_parser is not None:
82 return self._identity_parser.extract_content_ids(input_ids)
83
84 return super().extract_content_ids(input_ids)
85
86 def extract_reasoning(
87 self, model_output: str, request: "ChatCompletionRequest | ResponsesRequest"
88 ) -> tuple[str | None, str | None]:
89 if self._identity_parser is not None:
90 return self._identity_parser.extract_reasoning(model_output, request)
91
92 return super().extract_reasoning(model_output, request)
93
94 def extract_reasoning_streaming(
95 self,
96 previous_text: str,
97 current_text: str,
98 delta_text: str,
99 previous_token_ids: Sequence[int],
100 current_token_ids: Sequence[int],
101 delta_token_ids: Sequence[int],
102 ) -> DeltaMessage | None:
103 if self._identity_parser is not None:
104 return self._identity_parser.extract_reasoning_streaming(
105 previous_text,
106 current_text,
107 delta_text,
108 previous_token_ids,
109 current_token_ids,
110 delta_token_ids,
111 )
112
113 ret = super().extract_reasoning_streaming(
114 previous_text,
115 current_text,
116 delta_text,
117 previous_token_ids,
118 current_token_ids,
119 delta_token_ids,
120 )
121 if (
122 ret is not None
123 and self.start_token_id not in previous_token_ids
124 and self.start_token_id not in delta_token_ids
125 ):
126 if self.end_token_id in delta_token_ids:
127 # end token in delta with more tokens,
128 # extract reasoning content and content
129 end_index = delta_text.find(self.end_token)
130 reasoning = delta_text[:end_index]
131 content = delta_text[end_index + len(self.end_token) :]
132 return DeltaMessage(
133 reasoning=reasoning,
134 content=content if content else None,
135 )
136 elif self.end_token_id in previous_token_ids:
137 # end token in previous, thinking content ends
138 return DeltaMessage(content=delta_text)
139 else:
140 # no end token in previous or delta, reasoning content continues
141 return DeltaMessage(reasoning=delta_text)
142
143 return ret