main
py 252 lines 9.32 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Adapters that expose :class:`ParserEngine` through the legacy
4 :class:`ReasoningParser` and :class:`ToolParser` interfaces.
5
6 This lets parser engines flow through the existing serving-layer code
7 paths that expect separate reasoning and tool parser instances, without
8 any changes to the serving layer itself.
9 """
10
11 from __future__ import annotations
12
13 from collections.abc import Iterator, Sequence
14 from contextlib import contextmanager
15 from typing import TYPE_CHECKING
16
17 from vllm.parser.engine.parser_engine_config import ParserState
18 from vllm.reasoning.abs_reasoning_parsers import ReasoningParser
19 from vllm.tool_parsers.abstract_tool_parser import ToolParser
20
21 if TYPE_CHECKING:
22 from vllm.entrypoints.generate.base.protocol import (
23 DeltaMessage,
24 ExtractedToolCallInformation,
25 )
26 from vllm.entrypoints.openai.chat_completion.protocol import (
27 ChatCompletionRequest,
28 )
29 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
30 from vllm.parser.engine.parser_engine import ParserEngine
31 from vllm.tokenizers import TokenizerLike
32 from vllm.tool_parsers.utils import Tool
33
34
35 class ParserEngineReasoningAdapter(ReasoningParser):
36 """Adapts a :class:`ParserEngine` to the :class:`ReasoningParser`
37 interface so parser engines can be used as reasoning parsers in the
38 existing serving code.
39
40 Subclasses set :attr:`_parser_engine_cls` to the concrete
41 :class:`ParserEngine` class.
42 """
43
44 _parser_engine_cls: type[ParserEngine]
45 engine_based_streaming: bool = True
46
47 def __init__(self, tokenizer: TokenizerLike, *args, **kwargs) -> None:
48 super().__init__(tokenizer, *args, **kwargs)
49 self._parser_engine = self._parser_engine_cls(tokenizer, **kwargs) # type: ignore[call-arg]
50 self._parser_engine_kwargs = kwargs
51 self._counting_parser_engine: ParserEngine | None = None
52 # TODO: Remove once Responses finalization reuses accumulated streaming
53 # parser results instead of reparsing the complete output.
54 self._streaming_count_valid = False
55
56 @contextmanager
57 def _skip_tool_parsing(self) -> Iterator[None]:
58 saved = self._parser_engine.skip_tool_parsing
59 self._parser_engine.skip_tool_parsing = True
60 try:
61 yield
62 finally:
63 self._parser_engine.skip_tool_parsing = saved
64
65 def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
66 return self._parser_engine.is_reasoning_end(list(input_ids))
67
68 def find_reasoning_end_offset(self, token_ids: Sequence[int]) -> int | None:
69 return self._parser_engine.find_reasoning_end_offset(token_ids)
70
71 def adjust_initial_state_from_prompt(self, prompt_token_ids: Sequence[int]) -> None:
72 self._parser_engine.adjust_initial_state_from_prompt(prompt_token_ids)
73
74 def extract_content_ids(self, input_ids: list[int]) -> list[int]:
75 return self._parser_engine.extract_content_ids(input_ids)
76
77 def extract_reasoning(
78 self,
79 model_output: str,
80 request: ChatCompletionRequest | ResponsesRequest,
81 ) -> tuple[str | None, str | None]:
82 self._streaming_count_valid = False
83 with self._skip_tool_parsing():
84 return self._parser_engine.extract_reasoning(model_output, request)
85
86 def extract_reasoning_streaming(
87 self,
88 previous_text: str,
89 current_text: str,
90 delta_text: str,
91 previous_token_ids: Sequence[int],
92 current_token_ids: Sequence[int],
93 delta_token_ids: Sequence[int],
94 ) -> DeltaMessage | None:
95 self._streaming_count_valid = True
96 with self._skip_tool_parsing():
97 return self._parser_engine.extract_reasoning_streaming(
98 previous_text,
99 current_text,
100 delta_text,
101 previous_token_ids,
102 current_token_ids,
103 delta_token_ids,
104 )
105
106 @property
107 def reasoning_start_str(self) -> str | None:
108 return self._parser_engine.reasoning_start_str
109
110 @property
111 def reasoning_end_str(self) -> str | None:
112 return self._parser_engine.reasoning_end_str
113
114 @property
115 def reasoning_end_token_ids(self) -> frozenset[int]:
116 return self._parser_engine.reasoning_end_token_ids
117
118 def adjust_request(
119 self,
120 request: ChatCompletionRequest | ResponsesRequest,
121 ) -> ChatCompletionRequest | ResponsesRequest:
122 return self._parser_engine.adjust_request(request)
123
124 def has_engine_confirmed_reasoning_end(self) -> bool:
125 return self._parser_engine.reasoning_ended
126
127 def finish_streaming(self) -> DeltaMessage | None:
128 with self._skip_tool_parsing():
129 return self._parser_engine.finish_streaming()
130
131 def get_streaming_fallback_content(
132 self,
133 text: str,
134 request: ChatCompletionRequest | ResponsesRequest,
135 ) -> str | None:
136 return self._parser_engine.get_streaming_fallback_content(text, request)
137
138 def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
139 if self._streaming_count_valid:
140 return self._parser_engine.count_reasoning_tokens(token_ids)
141 if not token_ids:
142 return 0
143 if self._counting_parser_engine is None:
144 self._counting_parser_engine = self._parser_engine_cls(
145 self.model_tokenizer, **self._parser_engine_kwargs
146 ) # type: ignore[call-arg]
147 self._counting_parser_engine._single_pass_parse(
148 self.model_tokenizer.decode(token_ids), token_ids
149 )
150 return self._counting_parser_engine.count_reasoning_tokens(token_ids)
151
152
153 class ParserEngineToolAdapter(ToolParser):
154 """Adapts a :class:`ParserEngine` to the :class:`ToolParser` interface.
155
156 :meth:`extract_tool_calls` starts the parser engine in ``CONTENT``
157 state so it can parse reasoning-stripped content (i.e. the output of
158 :meth:`ReasoningParser.extract_reasoning`).
159
160 When no reasoning parser is configured at the serving layer, the
161 owning :class:`~vllm.parser.abstract_parser.Parser` sets
162 :attr:`skip_reasoning_parsing` so reasoning markup — plain content
163 in that configuration — passes through verbatim instead of being
164 consumed or reclassified by the engine. The engine honors the flag
165 only for markers that are reasoning-exclusive in its config; a
166 grammar like Inkling, whose block closer is shared with text and
167 tool blocks, keeps parsing unchanged.
168
169 Subclasses set :attr:`_parser_engine_cls` to the concrete
170 :class:`ParserEngine` class.
171 """
172
173 _parser_engine_cls: type[ParserEngine]
174 engine_based_streaming: bool = True
175
176 def __init__(
177 self,
178 tokenizer: TokenizerLike,
179 tools: list[Tool] | None = None,
180 **kwargs,
181 ) -> None:
182 super().__init__(tokenizer, tools)
183 self._parser_engine = self._parser_engine_cls(tokenizer, tools, **kwargs) # type: ignore[call-arg]
184
185 @property
186 def skip_reasoning_parsing(self) -> bool:
187 return self._parser_engine.skip_reasoning_parsing
188
189 @skip_reasoning_parsing.setter
190 def skip_reasoning_parsing(self, value: bool) -> None:
191 self._parser_engine.skip_reasoning_parsing = value
192
193 def adjust_request(
194 self,
195 request: ChatCompletionRequest | ResponsesRequest,
196 ) -> ChatCompletionRequest | ResponsesRequest:
197 request = super().adjust_request(request)
198 return self._parser_engine.adjust_request(request)
199
200 def extract_tool_calls(
201 self,
202 model_output: str,
203 request: ChatCompletionRequest,
204 ) -> ExtractedToolCallInformation:
205 return self._parser_engine.extract_tool_calls_from_content(
206 model_output, request
207 )
208
209 def extract_tool_calls_streaming(
210 self,
211 previous_text: str,
212 current_text: str,
213 delta_text: str,
214 previous_token_ids: Sequence[int],
215 current_token_ids: Sequence[int],
216 delta_token_ids: Sequence[int],
217 request: ChatCompletionRequest,
218 ) -> DeltaMessage | None:
219 engine = self._parser_engine
220 engine.initialize_streaming(initial_state=ParserState.CONTENT)
221 return engine.extract_tool_calls_streaming(
222 previous_text,
223 current_text,
224 delta_text,
225 previous_token_ids,
226 current_token_ids,
227 delta_token_ids,
228 request,
229 )
230
231 def finish_streaming(self) -> DeltaMessage | None:
232 return self._parser_engine.finish_streaming()
233
234
235 def make_adapters(
236 parser_engine_cls: type[ParserEngine],
237 ) -> tuple[type[ParserEngineReasoningAdapter], type[ParserEngineToolAdapter]]:
238 reasoning_adapter = type(
239 f"{parser_engine_cls.__name__}ReasoningAdapter",
240 (ParserEngineReasoningAdapter,),
241 {"_parser_engine_cls": parser_engine_cls},
242 )
243 tool_adapter = type(
244 f"{parser_engine_cls.__name__}ToolAdapter",
245 (ParserEngineToolAdapter,),
246 {"_parser_engine_cls": parser_engine_cls},
247 )
248 # Let the serving layer find the adapters and call adjust_request(),
249 # which sets skip_special_tokens=False for the detokenizer.
250 parser_engine_cls.reasoning_parser_cls = reasoning_adapter # type: ignore[attr-defined]
251 parser_engine_cls.tool_parser_cls = tool_adapter # type: ignore[attr-defined]
252 return reasoning_adapter, tool_adapter