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,