@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3a04061701
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

[Refactor][Parser] Unify Response API to use parser.parse() like Chat Completion API (#45190)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed Jun 11, 2026 at 00:37 UTC 3a0406170105b8710b3754d87bddc2a7cfd81d31
6 files changed +101 -211
tests/entrypoints/openai/test_responses_parser_unified.py
+2 -2
@@ -3,8 +3,8 @@
3 """Unit tests for ResponsesParser with the unified Parser interface.
4
5 These tests verify that ResponsesParser correctly delegates to the unified
6 -Parser (via extract_response_outputs) instead of calling separate
7 -ReasoningParser / ToolParser instances directly.
6 +Parser (via parse) instead of calling separate ReasoningParser / ToolParser
7 +instances directly.
8 """
9
10 from collections.abc import Sequence
tests/entrypoints/openai/test_tool_choice_content_none.py
+2 -2
@@ -78,9 +78,9 @@ def test_responses_parser_allows_named_tool_choice_with_none_content():
78 )
79 parser = _DummyDelegatingParser(tokenizer=None)
80
81 - tool_calls, content = parser._parse_tool_calls(
82 - request=request,
81 + tool_calls, content = parser._extract_tool_calls(
82 content=None,
83 + request=request,
84 enable_auto_tools=False,
85 )
86
vllm/entrypoints/openai/parser/responses_parser.py
+9 -4
@@ -16,6 +16,7 @@ from vllm.entrypoints.openai.responses.protocol import (
16 ResponseInputOutputItem,
17 ResponsesRequest,
18 )
19 +from vllm.entrypoints.openai.responses.utils import build_response_output_items
20 from vllm.entrypoints.serve.utils.constants import MCP_PREFIX
21 from vllm.outputs import CompletionOutput
22 from vllm.parser.abstract_parser import Parser
@@ -73,11 +74,15 @@ class ResponsesParser:
74 self.finish_reason = output.finish_reason
75
76 if self.parser_instance is not None:
76 - output_items = self.parser_instance.extract_response_outputs(
77 - model_output=output.text,
78 - model_output_token_ids=output.token_ids,
79 - request=self.request,
77 + reasoning, content, tool_calls = self.parser_instance.parse(
78 + output.text,
79 + self.request,
80 enable_auto_tools=self.enable_auto_tools,
81 + )
82 + output_items = build_response_output_items(
83 + reasoning=reasoning,
84 + content=content,
85 + tool_calls=tool_calls,
86 tool_call_id_type=self.tool_call_id_type,
87 )
88 self.response_messages.extend(output_items)
vllm/entrypoints/openai/responses/serving.py
+11 -6
@@ -86,6 +86,7 @@ from vllm.entrypoints.openai.responses.streaming_events import (
86 split_delta,
87 )
88 from vllm.entrypoints.openai.responses.utils import (
89 + build_response_output_items,
90 construct_input_messages,
91 construct_tool_dicts,
92 extract_function_tool_names,
@@ -1028,19 +1029,23 @@ class OpenAIServingResponses(OpenAIServing):
1029 top_logprobs=request.top_logprobs,
1030 )
1031
1031 - # Use parser to extract and create response output items
1032 + # Use parser to extract reasoning, content, and tool calls
1033 if self.parser:
1034 chat_template_kwargs = self._effective_chat_template_kwargs(request)
1035 parser = self.parser(
1036 tokenizer, request.tools, chat_template_kwargs=chat_template_kwargs
1037 )
1037 - return parser.extract_response_outputs(
1038 - model_output=final_output.text,
1039 - model_output_token_ids=final_output.token_ids,
1040 - request=request,
1038 + reasoning, content, tool_calls = parser.parse(
1039 + final_output.text,
1040 + request,
1041 enable_auto_tools=self.enable_auto_tools,
1042 - tool_call_id_type=self.tool_call_id_type,
1042 + )
1043 + return build_response_output_items(
1044 + reasoning=reasoning,
1045 + content=content,
1046 + tool_calls=tool_calls,
1047 logprobs=logprobs,
1048 + tool_call_id_type=self.tool_call_id_type,
1049 )
1050
1051 # Fallback when no parser is configured
vllm/entrypoints/openai/responses/utils.py
+76 -3
@@ -12,23 +12,96 @@ from openai.types.chat import (
12 from openai.types.chat.chat_completion_message_tool_call_param import (
13 Function as FunctionCallTool,
14 )
15 -from openai.types.responses import ResponseFunctionToolCall, ResponseOutputItem
15 +from openai.types.responses import (
16 + ResponseFunctionToolCall,
17 + ResponseOutputItem,
18 + ResponseOutputMessage,
19 + ResponseOutputText,
20 + ResponseReasoningItem,
21 +)
22 from openai.types.responses.response import ToolChoice
23 from openai.types.responses.response_function_tool_call_output_item import (
24 ResponseFunctionToolCallOutputItem,
25 )
20 -from openai.types.responses.response_output_message import ResponseOutputMessage
21 -from openai.types.responses.response_reasoning_item import ResponseReasoningItem
26 +from openai.types.responses.response_output_text import Logprob
27 +from openai.types.responses.response_reasoning_item import (
28 + Content as ResponseReasoningTextContent,
29 +)
30 from openai.types.responses.tool import Tool
31
32 from vllm import envs
33 +from vllm.entrypoints.chat_utils import make_tool_call_id
34 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionMessageParam
35 +from vllm.entrypoints.openai.engine.protocol import FunctionCall
36 from vllm.entrypoints.openai.responses.protocol import ResponseInputOutputItem
37 from vllm.logger import init_logger
38 +from vllm.utils import random_uuid
39
40 logger = init_logger(__name__)
41
42
43 +def build_response_output_items(
44 + reasoning: str | None,
45 + content: str | None,
46 + tool_calls: list[FunctionCall] | None,
47 + logprobs: list[Logprob] | None = None,
48 + tool_call_id_type: str = "random",
49 +) -> list[ResponseOutputItem]:
50 + outputs: list[ResponseOutputItem] = []
51 +
52 + if reasoning:
53 + outputs.append(
54 + ResponseReasoningItem(
55 + id=f"rs_{random_uuid()}",
56 + summary=[],
57 + type="reasoning",
58 + content=[
59 + ResponseReasoningTextContent(text=reasoning, type="reasoning_text")
60 + ],
61 + status=None,
62 + )
63 + )
64 +
65 + if content:
66 + outputs.append(
67 + ResponseOutputMessage(
68 + id=f"msg_{random_uuid()}",
69 + content=[
70 + ResponseOutputText(
71 + text=content,
72 + annotations=[],
73 + type="output_text",
74 + logprobs=logprobs,
75 + )
76 + ],
77 + role="assistant",
78 + status="completed",
79 + type="message",
80 + )
81 + )
82 +
83 + if tool_calls:
84 + for idx, tool_call in enumerate(tool_calls):
85 + outputs.append(
86 + ResponseFunctionToolCall(
87 + id=f"fc_{random_uuid()}",
88 + call_id=tool_call.id
89 + if tool_call.id
90 + else make_tool_call_id(
91 + id_type=tool_call_id_type,
92 + func_name=tool_call.name,
93 + idx=idx,
94 + ),
95 + type="function_call",
96 + status="completed",
97 + name=tool_call.name,
98 + arguments=tool_call.arguments,
99 + )
100 + )
101 +
102 + return outputs
103 +
104 +
105 def should_continue_final_message(
106 request_input: str | list[ResponseInputOutputItem],
107 ) -> bool:
vllm/parser/abstract_parser.py
+1 -194
@@ -8,21 +8,9 @@ from collections.abc import Sequence
8 from dataclasses import dataclass, field
9 from functools import cached_property
10
11 -from openai.types.responses import (
12 - ResponseFunctionToolCall,
13 - ResponseOutputItem,
14 - ResponseOutputMessage,
15 - ResponseOutputText,
16 - ResponseReasoningItem,
17 - ToolChoiceFunction,
18 -)
19 -from openai.types.responses.response_output_text import Logprob
20 -from openai.types.responses.response_reasoning_item import (
21 - Content as ResponseReasoningTextContent,
22 -)
11 +from openai.types.responses import ToolChoiceFunction
12 from pydantic import TypeAdapter, ValidationError
13
25 -from vllm.entrypoints.chat_utils import make_tool_call_id
14 from vllm.entrypoints.openai.chat_completion.protocol import (
15 ChatCompletionNamedToolChoiceParam,
16 ChatCompletionRequest,
@@ -43,7 +31,6 @@ from vllm.tool_parsers.streaming import (
31 extract_named_tool_call_streaming,
32 extract_required_tool_call_streaming,
33 )
46 -from vllm.utils import random_uuid
34
35 logger = init_logger(__name__)
36
@@ -179,36 +166,6 @@ class Parser:
166 The extracted content token IDs.
167 """
168
182 - @abstractmethod
183 - def extract_response_outputs(
184 - self,
185 - *,
186 - model_output: str,
187 - model_output_token_ids: Sequence[int],
188 - request: ResponsesRequest,
189 - enable_auto_tools: bool = False,
190 - tool_call_id_type: str = "random",
191 - logprobs: list[Logprob] | None = None,
192 - ) -> list[ResponseOutputItem]:
193 - """
194 - Extract reasoning, content, and tool calls from a complete
195 - model-generated string and return as ResponseOutputItem objects.
196 -
197 - Used for non-streaming responses where we have the entire model
198 - response available before sending to the client.
199 -
200 - Args:
201 - model_output: The complete model-generated string.
202 - model_output_token_ids: The token IDs of the model output.
203 - request: The request object used to generate the output.
204 - enable_auto_tools: Whether to enable automatic tool call parsing.
205 - tool_call_id_type: Type of tool call ID generation ("random", etc).
206 - logprobs: Pre-computed logprobs for the output text, if any.
207 -
208 - Returns:
209 - A list of ResponseOutputItem objects.
210 - """
211 -
169 @abstractmethod
170 def extract_reasoning(
171 self,
@@ -375,83 +332,6 @@ class DelegatingParser(Parser):
332 return None, model_output
333 return self._reasoning_parser.extract_reasoning(model_output, request)
334
378 - def extract_response_outputs(
379 - self,
380 - *,
381 - model_output: str,
382 - model_output_token_ids: Sequence[int],
383 - request: ResponsesRequest,
384 - enable_auto_tools: bool = False,
385 - tool_call_id_type: str = "random",
386 - logprobs: list[Logprob] | None = None,
387 - ) -> list[ResponseOutputItem]:
388 - # First extract reasoning
389 - reasoning, content = self.extract_reasoning(model_output, request)
390 -
391 - # Then parse tool calls from the content
392 - tool_calls, content = self._parse_tool_calls(
393 - request=request,
394 - content=content,
395 - enable_auto_tools=enable_auto_tools,
396 - )
397 -
398 - # Build output items
399 - outputs: list[ResponseOutputItem] = []
400 -
401 - # Add reasoning item if present
402 - if reasoning:
403 - reasoning_item = ResponseReasoningItem(
404 - id=f"rs_{random_uuid()}",
405 - summary=[],
406 - type="reasoning",
407 - content=[
408 - ResponseReasoningTextContent(text=reasoning, type="reasoning_text")
409 - ],
410 - status=None, # NOTE: Only the last output item has status.
411 - )
412 - outputs.append(reasoning_item)
413 -
414 - # Add message item if there's content
415 - if content:
416 - res_text_part = ResponseOutputText(
417 - text=content,
418 - annotations=[],
419 - type="output_text",
420 - logprobs=logprobs,
421 - )
422 - message_item = ResponseOutputMessage(
423 - id=f"msg_{random_uuid()}",
424 - content=[res_text_part],
425 - role="assistant",
426 - status="completed",
427 - type="message",
428 - )
429 - outputs.append(message_item)
430 -
431 - if tool_calls:
432 - # We use a simple counter for history_tool_call_count because
433 - # we don't track the history of tool calls in the Responses API yet.
434 - # This means that the tool call index will start from 0 for each
435 - # request.
436 - for history_tool_call_cnt, tool_call in enumerate(tool_calls):
437 - tool_call_item = ResponseFunctionToolCall(
438 - id=f"fc_{random_uuid()}",
439 - call_id=tool_call.id
440 - if tool_call.id
441 - else make_tool_call_id(
442 - id_type=tool_call_id_type,
443 - func_name=tool_call.name,
444 - idx=history_tool_call_cnt,
445 - ),
446 - type="function_call",
447 - status="completed",
448 - name=tool_call.name,
449 - arguments=tool_call.arguments,
450 - )
451 - outputs.append(tool_call_item)
452 -
453 - return outputs
454 -
335 def _get_function_name(
336 self, request: ChatCompletionRequest | ResponsesRequest
337 ) -> str:
@@ -463,79 +343,6 @@ class DelegatingParser(Parser):
343 return request.tool_choice.function.name
344 raise ValueError("Invalid tool_choice for function name extraction.")
345
466 - def _parse_tool_calls(
467 - self,
468 - request: ResponsesRequest,
469 - content: str | None,
470 - enable_auto_tools: bool,
471 - ) -> tuple[list[FunctionCall], str | None]:
472 - """
473 - TODO(qandrew): merge _parse_tool_calls_from_content
474 - for ChatCompletions into this function
475 - Parse tool calls from content based on request tool_choice settings.
476 -
477 - Returns:
478 - A tuple of (function_calls, remaining_content) if tool calls
479 - were parsed
480 - """
481 - function_calls: list[FunctionCall] = []
482 -
483 - if request.tool_choice and isinstance(
484 - request.tool_choice,
485 - (ToolChoiceFunction, ChatCompletionNamedToolChoiceParam),
486 - ):
487 - # Forced Function Call
488 - if content is None:
489 - return [], None
490 - function_calls.append(
491 - FunctionCall(name=self._get_function_name(request), arguments=content)
492 - )
493 - return function_calls, None # Clear content since tool is called.
494 -
495 - if request.tool_choice == "required":
496 - # Required tool calls - parse JSON
497 - tool_calls = []
498 - with contextlib.suppress(ValidationError):
499 - content = content or ""
500 - tool_calls = TypeAdapter(list[FunctionDefinition]).validate_json(
501 - content
502 - )
503 - for tool_call in tool_calls:
504 - function_calls.append(
505 - FunctionCall(
506 - name=tool_call.name,
507 - arguments=json.dumps(tool_call.parameters, ensure_ascii=False),
508 - )
509 - )
510 - return function_calls, None # Clear content since tool is called.
511 -
512 - if (
513 - self._tool_parser is not None
514 - and enable_auto_tools
515 - and (request.tool_choice == "auto" or request.tool_choice is None)
516 - ):
517 - # Automatic Tool Call Parsing
518 - tool_call_info = self.extract_tool_calls(
519 - content if content is not None else "",
520 - request=request,
521 - )
522 - if tool_call_info is not None and tool_call_info.tools_called:
523 - function_calls.extend(
524 - FunctionCall(
525 - id=tool_call.id,
526 - name=tool_call.function.name,
527 - arguments=tool_call.function.arguments,
528 - )
529 - for tool_call in tool_call_info.tool_calls
530 - )
531 - remaining_content = tool_call_info.content
532 - if remaining_content and remaining_content.strip() == "":
533 - remaining_content = None
534 - return function_calls, remaining_content
535 -
536 - # No tool calls
537 - return [], content
538 -
346 def _extract_tool_calls(
347 self,
348 content: str | None,