From 0d80979644e0237b6ef02ce0601dc0bd654e357b Mon Sep 17 00:00:00 2001 From: Flora Feng <4florafeng@gmail.com> Date: Mon, 15 Jun 2026 11:16:45 -0400 Subject: [PATCH] [Chore] Consolidate reasoning/tool parser attributes into unified Parser in chat serving (#45548) Signed-off-by: sfeng33 <4florafeng@gmail.com> --- .../chat_completion/test_serving_chat.py | 17 +++++- .../openai/chat_completion/batch_serving.py | 21 ++++---- .../openai/chat_completion/protocol.py | 2 + .../openai/chat_completion/serving.py | 52 +++++++------------ 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/tests/entrypoints/openai/chat_completion/test_serving_chat.py b/tests/entrypoints/openai/chat_completion/test_serving_chat.py index 27503ae56f..a12662ec7f 100644 --- a/tests/entrypoints/openai/chat_completion/test_serving_chat.py +++ b/tests/entrypoints/openai/chat_completion/test_serving_chat.py @@ -1350,6 +1350,21 @@ class TestServingChatWithHarmony: else serving_chat.chat_completion_full_generator ) + chat_template_kwargs = serving_chat._effective_chat_template_kwargs(req) + if stream: + extra_kwargs: dict[str, Any] = { + "chat_template_kwargs": chat_template_kwargs, + } + else: + parser = None + if serving_chat.parser_cls is not None: + parser = serving_chat.parser_cls( + tokenizer, + req.tools, + chat_template_kwargs=chat_template_kwargs, + ) + extra_kwargs = {"parser": parser} + result = generator_func( request=req, result_generator=result_generator(), @@ -1361,7 +1376,7 @@ class TestServingChatWithHarmony: request_id=req.request_id, model_name=req.model, ), - chat_template_kwargs=serving_chat._effective_chat_template_kwargs(req), + **extra_kwargs, ) if stream: diff --git a/vllm/entrypoints/openai/chat_completion/batch_serving.py b/vllm/entrypoints/openai/chat_completion/batch_serving.py index 2a0b20a3d8..96ed7dcb77 100644 --- a/vllm/entrypoints/openai/chat_completion/batch_serving.py +++ b/vllm/entrypoints/openai/chat_completion/batch_serving.py @@ -25,7 +25,7 @@ from vllm.entrypoints.serve.utils.api_utils import get_max_tokens from vllm.inputs import EngineInput from vllm.logger import init_logger from vllm.outputs import RequestOutput -from vllm.reasoning import ReasoningParser +from vllm.parser.abstract_parser import Parser from vllm.tokenizers import TokenizerLike from vllm.utils.async_utils import merge_async_iterators from vllm.utils.collection_utils import as_list @@ -119,14 +119,15 @@ class OpenAIServingChatBatch(OpenAIServingChat): for messages in request.messages ] - reasoning_parser: ReasoningParser | None = None - if self.reasoning_parser_cls: + parser: Parser | None = None + if self.parser_cls is not None: chat_template_kwargs = self._effective_chat_template_kwargs( single_requests[0] ) - reasoning_parser = self.reasoning_parser_cls( + parser = self.parser_cls( tokenizer, - chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg] + None, # tools + chat_template_kwargs=chat_template_kwargs, ) render_result = await self.render_batch_chat_request(request) @@ -194,7 +195,7 @@ class OpenAIServingChatBatch(OpenAIServingChat): all_conversations, tokenizer, request_metadata, - reasoning_parser, + parser, ) async def chat_completion_full_generator_batch( @@ -206,7 +207,7 @@ class OpenAIServingChatBatch(OpenAIServingChat): all_conversations: list[list[ConversationMessage]], tokenizer: TokenizerLike, request_metadata: RequestResponseMetadata, - reasoning_parser: ReasoningParser | None = None, + parser: Parser | None = None, ) -> ErrorResponse | ChatCompletionResponse: """Handle batched (non-streaming) chat completions. @@ -262,12 +263,12 @@ class OpenAIServingChatBatch(OpenAIServingChat): else: logprobs = None - if reasoning_parser: - reasoning, content = reasoning_parser.extract_reasoning( + if parser is not None: + reasoning, content, _ = parser.parse( output.text, request=request, # type: ignore[arg-type] ) - if not getattr(request, "include_reasoning", True): + if not request.include_reasoning: reasoning = None else: reasoning = None diff --git a/vllm/entrypoints/openai/chat_completion/protocol.py b/vllm/entrypoints/openai/chat_completion/protocol.py index 184ace5680..3457aa12f4 100644 --- a/vllm/entrypoints/openai/chat_completion/protocol.py +++ b/vllm/entrypoints/openai/chat_completion/protocol.py @@ -955,6 +955,8 @@ class BatchChatCompletionRequest(OpenAIBaseModel): temperature: float | None = 0.7 top_p: float | None = 1.0 user: str | None = None + tool_choice: Literal["none"] | None = "none" + include_reasoning: bool = True # vLLM extensions best_of: int | None = None diff --git a/vllm/entrypoints/openai/chat_completion/serving.py b/vllm/entrypoints/openai/chat_completion/serving.py index ed1820f4c4..911421029c 100644 --- a/vllm/entrypoints/openai/chat_completion/serving.py +++ b/vllm/entrypoints/openai/chat_completion/serving.py @@ -60,7 +60,6 @@ from vllm.logprobs import Logprob from vllm.outputs import RequestOutput from vllm.parser import ParserManager from vllm.parser.abstract_parser import Parser -from vllm.reasoning import ReasoningParser from vllm.renderers import ChatParams from vllm.sampling_params import BeamSearchParams, SamplingParams from vllm.tokenizers import TokenizerLike @@ -145,17 +144,7 @@ class OpenAIServingChat(OpenAIServing): self.enable_log_outputs = enable_log_outputs self.enable_log_deltas = enable_log_deltas - # set up reasoning parser - self.reasoning_parser_cls = ParserManager.get_reasoning_parser( - reasoning_parser_name=reasoning_parser - ) - # set up tool use self.enable_auto_tools: bool = enable_auto_tools - self.tool_parser = ParserManager.get_tool_parser( - tool_parser_name=tool_parser, - enable_auto_tools=enable_auto_tools, - model_name=self.model_config.model, - ) self.parser_cls = ParserManager.get_parser( tool_parser_name=tool_parser, reasoning_parser_name=reasoning_parser, @@ -164,8 +153,9 @@ class OpenAIServingChat(OpenAIServing): is_harmony=self.model_config.hf_config.model_type == "gpt_oss", ) if ( - is_mistral_tool_parser(self.tool_parser) - and self.reasoning_parser_cls is not None + self.parser_cls is not None + and is_mistral_tool_parser(self.parser_cls.tool_parser_cls) + and self.parser_cls.reasoning_parser_cls is not None ): from vllm.tool_parsers.mistral_tool_parser import MistralToolParser @@ -267,11 +257,12 @@ class OpenAIServingChat(OpenAIServing): tokenizer = self.renderer.tokenizer assert tokenizer is not None chat_template_kwargs = self._effective_chat_template_kwargs(request) - reasoning_parser: ReasoningParser | None = None - if self.reasoning_parser_cls: - reasoning_parser = self.reasoning_parser_cls( + parser: Parser | None = None + if self.parser_cls is not None: + parser = self.parser_cls( tokenizer, - chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg] + request.tools, + chat_template_kwargs=chat_template_kwargs, ) result = await self.render_chat_request(request) if isinstance(result, ErrorResponse): @@ -359,10 +350,8 @@ class OpenAIServingChat(OpenAIServing): # `think?` rule that handles both reasoning and # non-reasoning outputs. reasoning_ended = True - elif reasoning_parser: - reasoning_ended = reasoning_parser.is_reasoning_end( - prompt_token_ids or [] - ) + elif parser is not None and parser.reasoning_parser is not None: + reasoning_ended = parser.is_reasoning_end(prompt_token_ids or []) else: reasoning_ended = None @@ -378,7 +367,7 @@ class OpenAIServingChat(OpenAIServing): reasoning_parser_kwargs={ "chat_template_kwargs": chat_template_kwargs, } - if reasoning_parser + if parser is not None and parser.reasoning_parser is not None else None, ) @@ -408,7 +397,7 @@ class OpenAIServingChat(OpenAIServing): conversation, tokenizer, request_metadata, - chat_template_kwargs=chat_template_kwargs, + parser=parser, mm_token_counts=mm_token_counts, ) @@ -835,7 +824,7 @@ class OpenAIServingChat(OpenAIServing): conversation: list[ConversationMessage], tokenizer: TokenizerLike, request_metadata: RequestResponseMetadata, - chat_template_kwargs: dict[str, Any] | None = None, + parser: Parser | None = None, mm_token_counts: dict[str, int] | None = None, ) -> ErrorResponse | ChatCompletionResponse: created_time = int(time.time()) @@ -861,6 +850,9 @@ class OpenAIServingChat(OpenAIServing): history_tool_call_cnt = 0 role = self.get_chat_request_role(request) + tool_parser_cls = ( + self.parser_cls.tool_parser_cls if self.parser_cls is not None else None + ) for output in final_res.outputs: # check for error finish reason and raise GenerationError # finish_reason='error' indicates a retryable request-level internal error @@ -880,14 +872,6 @@ class OpenAIServingChat(OpenAIServing): else: logprobs = None - parser: Parser | None = None - if self.parser_cls is not None: - parser = self.parser_cls( - tokenizer, - request.tools, - chat_template_kwargs=chat_template_kwargs, - ) - if parser is not None: reasoning, content, tool_calls = parser.parse( output.text, @@ -904,7 +888,7 @@ class OpenAIServingChat(OpenAIServing): auto_tools_called = False - if (not self.enable_auto_tools or not self.tool_parser) and ( + if (not self.enable_auto_tools or not tool_parser_cls) and ( not isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam) and request.tool_choice != "required" ): @@ -963,7 +947,7 @@ class OpenAIServingChat(OpenAIServing): request.tools and (request.tool_choice == "auto" or request.tool_choice is None) and self.enable_auto_tools - and self.tool_parser + and tool_parser_cls ): auto_tools_called = tool_calls is not None and len(tool_calls) > 0 if tool_calls: