A high-throughput and memory-efficient inference and serving engine for LLMs
[Refactor] Deprecate ResponsesParser wrapper, inline parsing into ParsableContext (#45431)
Signed-off-by: sfeng33 <4florafeng@gmail.com>
Flora Feng committed
Jun 12, 2026 at 16:15 UTC
6e4a54717689b9f3de5f778fb030bd2c2c6ec20f
5 files changed
+169
-308
tests/entrypoints/openai/responses/test_parsable_context_unit.py
renamed
+72
-99
@@ -1,10 +1,9 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Unit tests for ResponsesParser with the unified Parser interface.
3
+"""Unit tests for ParsableContext's parsing behavior.
4
5
-These tests verify that ResponsesParser correctly delegates to the unified
6
-Parser (via parse) instead of calling separate ReasoningParser / ToolParser
7
-instances directly.
5
+These tests verify that ParsableContext correctly delegates to the unified
6
+Parser (via parse) and properly builds response output items.
7
"""
8
9
from collections.abc import Sequence
@@ -18,12 +17,9 @@ from vllm.entrypoints.openai.engine.protocol import (
17
FunctionCall,
18
ToolCall,
19
)
21
-from vllm.entrypoints.openai.parser.responses_parser import (
22
- ResponsesParser,
23
- get_responses_parser_for_simple_context,
24
-)
20
+from vllm.entrypoints.openai.responses.context import ParsableContext
21
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
26
-from vllm.outputs import CompletionOutput
22
+from vllm.outputs import CompletionOutput, RequestOutput
23
from vllm.parser.abstract_parser import DelegatingParser
24
25
pytestmark = pytest.mark.skip_global_cleanup
@@ -162,32 +158,42 @@ def _make_request(**overrides) -> ResponsesRequest:
158
return ResponsesRequest.model_validate(defaults)
159
160
165
-def _make_output(
161
+def _make_request_output(
162
text: str = "Hello, world!",
163
token_ids: Sequence[int] = (1, 2, 3),
164
finish_reason: str = "stop",
169
-) -> CompletionOutput:
170
- return CompletionOutput(
171
- index=0,
172
- text=text,
173
- token_ids=list(token_ids),
174
- cumulative_logprob=None,
175
- logprobs=None,
176
- finish_reason=finish_reason,
165
+) -> RequestOutput:
166
+ return RequestOutput(
167
+ request_id="test",
168
+ prompt=None,
169
+ prompt_token_ids=[],
170
+ prompt_logprobs=None,
171
+ outputs=[
172
+ CompletionOutput(
173
+ index=0,
174
+ text=text,
175
+ token_ids=list(token_ids),
176
+ cumulative_logprob=None,
177
+ logprobs=None,
178
+ finish_reason=finish_reason,
179
+ )
180
+ ],
181
+ finished=True,
182
)
183
184
180
-def _make_parser(parser_cls, **overrides):
185
+def _make_context(parser_cls, **overrides):
186
defaults = dict(
187
tokenizer=MagicMock(),
188
parser_cls=parser_cls,
189
response_messages=[],
190
request=_make_request(),
191
+ available_tools=None,
192
chat_template=None,
193
chat_template_content_format="auto",
194
)
195
defaults.update(overrides)
190
- return ResponsesParser(**defaults)
196
+ return ParsableContext(**defaults)
197
198
199
# ---------------------------------------------------------------------------
@@ -197,22 +203,22 @@ def _make_parser(parser_cls, **overrides):
203
204
def test_process_text_with_parser():
205
"""Parser with no reasoning/tools returns a single message item."""
200
- parser = _make_parser(_NoOpParser)
201
- parser.process(_make_output(text="Hello!"))
206
+ ctx = _make_context(_NoOpParser)
207
+ ctx.append_output(_make_request_output(text="Hello!"))
208
203
- assert len(parser.response_messages) == 1
204
- msg = parser.response_messages[0]
209
+ assert len(ctx.response_messages) == 1
210
+ msg = ctx.response_messages[0]
211
assert msg.type == "message"
212
assert msg.content[0].text == "Hello!"
213
214
215
def test_process_text_without_parser():
216
"""parser_cls=None falls back to plain text wrapping."""
211
- parser = _make_parser(None)
212
- parser.process(_make_output(text="Hello!"))
217
+ ctx = _make_context(None)
218
+ ctx.append_output(_make_request_output(text="Hello!"))
219
214
- assert len(parser.response_messages) == 1
215
- msg = parser.response_messages[0]
220
+ assert len(ctx.response_messages) == 1
221
+ msg = ctx.response_messages[0]
222
assert msg.type == "message"
223
assert msg.content[0].text == "Hello!"
224
@@ -224,18 +230,18 @@ def test_process_text_without_parser():
230
231
def test_process_empty_text_without_parser():
232
"""Empty text with no parser produces no output items."""
227
- parser = _make_parser(None)
228
- parser.process(_make_output(text=""))
233
+ ctx = _make_context(None)
234
+ ctx.append_output(_make_request_output(text=""))
235
230
- assert len(parser.response_messages) == 0
236
+ assert len(ctx.response_messages) == 0
237
238
239
def test_process_empty_text_with_parser():
240
"""Empty text with parser produces no output items."""
235
- parser = _make_parser(_NoOpParser)
236
- parser.process(_make_output(text=""))
241
+ ctx = _make_context(_NoOpParser)
242
+ ctx.append_output(_make_request_output(text=""))
243
238
- assert len(parser.response_messages) == 0
244
+ assert len(ctx.response_messages) == 0
245
246
247
# ---------------------------------------------------------------------------
@@ -245,26 +251,28 @@ def test_process_empty_text_with_parser():
251
252
def test_process_extracts_reasoning():
253
"""Parser that finds reasoning produces both reasoning and message items."""
248
- parser = _make_parser(_ReasoningOnlyParser)
249
- parser.process(_make_output(text="<think>Let me check</think>The answer is 42"))
254
+ ctx = _make_context(_ReasoningOnlyParser)
255
+ ctx.append_output(
256
+ _make_request_output(text="<think>Let me check</think>The answer is 42")
257
+ )
258
251
- types = [m.type for m in parser.response_messages]
259
+ types = [m.type for m in ctx.response_messages]
260
assert "reasoning" in types
261
assert "message" in types
262
255
- reasoning_item = next(m for m in parser.response_messages if m.type == "reasoning")
263
+ reasoning_item = next(m for m in ctx.response_messages if m.type == "reasoning")
264
assert reasoning_item.content[0].text == "Let me check"
265
258
- message_item = next(m for m in parser.response_messages if m.type == "message")
266
+ message_item = next(m for m in ctx.response_messages if m.type == "message")
267
assert message_item.content[0].text == "The answer is 42"
268
269
270
def test_process_reasoning_only_no_content():
271
"""When reasoning consumes all text, only a reasoning item is produced."""
264
- parser = _make_parser(_ReasoningOnlyParser)
265
- parser.process(_make_output(text="<think>Just thinking</think>"))
272
+ ctx = _make_context(_ReasoningOnlyParser)
273
+ ctx.append_output(_make_request_output(text="<think>Just thinking</think>"))
274
267
- types = [m.type for m in parser.response_messages]
275
+ types = [m.type for m in ctx.response_messages]
276
assert "reasoning" in types
277
assert "message" not in types
278
@@ -286,13 +294,13 @@ def test_process_extracts_tool_calls():
294
}
295
],
296
)
289
- parser = _make_parser(_ToolCallingParser, request=request, enable_auto_tools=True)
290
- parser.process(_make_output(text="calling tool"))
297
+ ctx = _make_context(_ToolCallingParser, request=request, enable_auto_tools=True)
298
+ ctx.append_output(_make_request_output(text="calling tool"))
299
292
- types = [m.type for m in parser.response_messages]
300
+ types = [m.type for m in ctx.response_messages]
301
assert "function_call" in types
302
295
- tool_item = next(m for m in parser.response_messages if m.type == "function_call")
303
+ tool_item = next(m for m in ctx.response_messages if m.type == "function_call")
304
assert tool_item.name == "get_weather"
305
assert tool_item.arguments == '{"location": "Paris"}'
306
assert tool_item.status == "completed"
@@ -304,15 +312,15 @@ def test_process_extracts_tool_calls():
312
313
314
def test_finish_reason_tracked():
307
- """finish_reason from CompletionOutput is stored on the parser."""
308
- parser = _make_parser(_NoOpParser)
309
- assert parser.finish_reason is None
315
+ """finish_reason from CompletionOutput is stored on the context."""
316
+ ctx = _make_context(_NoOpParser)
317
+ assert ctx.finish_reason is None
318
311
- parser.process(_make_output(finish_reason="stop"))
312
- assert parser.finish_reason == "stop"
319
+ ctx.append_output(_make_request_output(finish_reason="stop"))
320
+ assert ctx.finish_reason == "stop"
321
314
- parser.process(_make_output(finish_reason="length"))
315
- assert parser.finish_reason == "length"
322
+ ctx.append_output(_make_request_output(finish_reason="length"))
323
+ assert ctx.finish_reason == "length"
324
325
326
# ---------------------------------------------------------------------------
@@ -321,62 +329,27 @@ def test_finish_reason_tracked():
329
330
331
def test_multi_turn_accumulation():
324
- """Multiple process() calls accumulate response_messages."""
325
- parser = _make_parser(_NoOpParser)
332
+ """Multiple append_output() calls accumulate response_messages."""
333
+ ctx = _make_context(_NoOpParser)
334
327
- parser.process(_make_output(text="First turn"))
328
- parser.process(_make_output(text="Second turn"))
335
+ ctx.append_output(_make_request_output(text="First turn"))
336
+ ctx.append_output(_make_request_output(text="Second turn"))
337
330
- assert len(parser.response_messages) == 2
331
- texts = [m.content[0].text for m in parser.response_messages]
338
+ assert len(ctx.response_messages) == 2
339
+ texts = [m.content[0].text for m in ctx.response_messages]
340
assert texts == ["First turn", "Second turn"]
341
342
343
def test_num_init_messages_offset():
344
"""Initial messages are preserved and offset works correctly."""
345
init_messages = [MagicMock(type="message")]
338
- parser = _make_parser(_NoOpParser, response_messages=init_messages)
346
+ ctx = _make_context(_NoOpParser, response_messages=init_messages)
347
340
- assert parser.num_init_messages == 1
348
+ assert ctx.num_init_messages == 1
349
342
- parser.process(_make_output(text="New output"))
350
+ ctx.append_output(_make_request_output(text="New output"))
351
344
- assert len(parser.response_messages) == 2
345
- items = parser.make_response_output_items_from_parsable_context()
352
+ assert len(ctx.response_messages) == 2
353
+ items = ctx.make_response_output_items()
354
assert len(items) == 1
355
assert items[0].type == "message"
348
-
349
-
350
-# ---------------------------------------------------------------------------
351
-# Tests: factory function
352
-# ---------------------------------------------------------------------------
353
-
354
-
355
-def test_factory_function_creates_parser():
356
- """get_responses_parser_for_simple_context returns a working parser."""
357
- rp = get_responses_parser_for_simple_context(
358
- tokenizer=MagicMock(),
359
- parser_cls=_NoOpParser,
360
- response_messages=[],
361
- request=_make_request(),
362
- chat_template=None,
363
- chat_template_content_format="auto",
364
- )
365
- assert isinstance(rp, ResponsesParser)
366
-
367
- rp.process(_make_output(text="Works!"))
368
- assert len(rp.response_messages) == 1
369
-
370
-
371
-def test_factory_function_none_parser():
372
- """Factory function works with parser_cls=None."""
373
- rp = get_responses_parser_for_simple_context(
374
- tokenizer=MagicMock(),
375
- parser_cls=None,
376
- response_messages=[],
377
- request=_make_request(),
378
- chat_template=None,
379
- chat_template_content_format="auto",
380
- )
381
- assert isinstance(rp, ResponsesParser)
382
- assert rp.parser_instance is None
vllm/entrypoints/mcp/tool.py
+1
-1
@@ -159,7 +159,7 @@ class HarmonyPythonTool(Tool):
159
160
assert isinstance(context, ParsableContext)
161
162
- last_msg = context.parser.response_messages[-1]
162
+ last_msg = context.response_messages[-1]
163
args = json.loads(last_msg.arguments)
164
165
last_msg_harmony = Message(
vllm/entrypoints/openai/parser/responses_parser.py
deleted
-180
@@ -1,180 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-import logging
4
-from typing import Any
5
-
6
-from openai.types.responses import ResponseFunctionToolCall, ResponseOutputItem
7
-from openai.types.responses.response_function_tool_call_output_item import (
8
- ResponseFunctionToolCallOutputItem,
9
-)
10
-from openai.types.responses.response_output_item import McpCall
11
-from openai.types.responses.response_output_message import ResponseOutputMessage
12
-from openai.types.responses.response_output_text import ResponseOutputText
13
-
14
-from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
15
-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
23
-from vllm.tokenizers import TokenizerLike
24
-from vllm.utils import random_uuid
25
-
26
-logger = logging.getLogger(__name__)
27
-
28
-
29
-class ResponsesParser:
30
- """Incremental parser over completion tokens with reasoning support."""
31
-
32
- def __init__(
33
- self,
34
- *,
35
- tokenizer: TokenizerLike,
36
- parser_cls: type[Parser] | None,
37
- response_messages: list[ResponseInputOutputItem],
38
- request: ResponsesRequest,
39
- chat_template: str | None,
40
- chat_template_content_format: ChatTemplateContentFormatOption,
41
- enable_auto_tools: bool = False,
42
- tool_call_id_type: str = "random",
43
- ):
44
- self.response_messages: list[ResponseInputOutputItem] = (
45
- # TODO: initial messages may not be properly typed
46
- response_messages
47
- )
48
- self.num_init_messages = len(response_messages)
49
- self.tokenizer = tokenizer
50
- self.request = request
51
-
52
- self.parser_instance: Parser | None = None
53
- if parser_cls is not None:
54
- chat_template_kwargs = _effective_chat_template_kwargs(
55
- request,
56
- chat_template=chat_template,
57
- chat_template_content_format=chat_template_content_format,
58
- )
59
-
60
- self.parser_instance = parser_cls(
61
- tokenizer,
62
- tools=request.tools,
63
- chat_template_kwargs=chat_template_kwargs,
64
- )
65
-
66
- self.enable_auto_tools = enable_auto_tools
67
- self.tool_call_id_type = tool_call_id_type
68
-
69
- # Store the last finish_reason to determine response status
70
- self.finish_reason: str | None = None
71
-
72
- def process(self, output: CompletionOutput) -> "ResponsesParser":
73
- # Store the finish_reason from the output
74
- self.finish_reason = output.finish_reason
75
-
76
- if self.parser_instance is not None:
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)
89
- else:
90
- # No parser configured, treat entire output as text content
91
- if output.text:
92
- self.response_messages.append(
93
- ResponseOutputMessage(
94
- type="message",
95
- id=f"msg_{random_uuid()}",
96
- status="completed",
97
- role="assistant",
98
- content=[
99
- ResponseOutputText(
100
- annotations=[], # TODO
101
- type="output_text",
102
- text=output.text,
103
- logprobs=None, # TODO
104
- )
105
- ],
106
- )
107
- )
108
-
109
- return self
110
-
111
- def make_response_output_items_from_parsable_context(
112
- self,
113
- ) -> list[ResponseOutputItem]:
114
- """Given a list of sentences, construct ResponseOutput Items."""
115
- response_messages = self.response_messages[self.num_init_messages :]
116
- output_messages: list[ResponseOutputItem] = []
117
- for message in response_messages:
118
- if not isinstance(message, ResponseFunctionToolCallOutputItem):
119
- output_messages.append(message)
120
- else:
121
- if len(output_messages) == 0:
122
- raise ValueError(
123
- "Cannot have a FunctionToolCallOutput before FunctionToolCall."
124
- )
125
- if isinstance(output_messages[-1], ResponseFunctionToolCall):
126
- mcp_message = McpCall(
127
- id=f"{MCP_PREFIX}{random_uuid()}",
128
- arguments=output_messages[-1].arguments,
129
- name=output_messages[-1].name,
130
- server_label=output_messages[
131
- -1
132
- ].name, # TODO: store the server label
133
- type="mcp_call",
134
- status="completed",
135
- output=message.output,
136
- # TODO: support error output
137
- )
138
- output_messages[-1] = mcp_message
139
-
140
- return output_messages
141
-
142
-
143
-def get_responses_parser_for_simple_context(
144
- *,
145
- tokenizer: TokenizerLike,
146
- parser_cls: type[Parser] | None,
147
- response_messages: list[ResponseInputOutputItem],
148
- request: ResponsesRequest,
149
- chat_template: str | None,
150
- chat_template_content_format: ChatTemplateContentFormatOption,
151
- enable_auto_tools: bool = False,
152
- tool_call_id_type: str = "random",
153
-) -> ResponsesParser:
154
- """Factory function to create a ResponsesParser with
155
- optional unified parser.
156
-
157
- Returns:
158
- ResponsesParser instance configured with the provided parser
159
- """
160
- return ResponsesParser(
161
- tokenizer=tokenizer,
162
- parser_cls=parser_cls,
163
- response_messages=response_messages,
164
- request=request,
165
- chat_template=chat_template,
166
- chat_template_content_format=chat_template_content_format,
167
- enable_auto_tools=enable_auto_tools,
168
- tool_call_id_type=tool_call_id_type,
169
- )
170
-
171
-
172
-def _effective_chat_template_kwargs(
173
- request: ResponsesRequest,
174
- chat_template: str | None,
175
- chat_template_content_format: ChatTemplateContentFormatOption,
176
-) -> dict[str, Any]:
177
- return request.build_chat_params(
178
- default_template=chat_template,
179
- default_template_content_format=chat_template_content_format,
180
- ).chat_template_kwargs
vllm/entrypoints/openai/responses/context.py
+93
-25
@@ -10,9 +10,13 @@ from contextlib import AsyncExitStack
10
from dataclasses import replace
11
from typing import TYPE_CHECKING, Any, Final, Union
12
13
+from openai.types.responses import ResponseFunctionToolCall, ResponseOutputItem
14
from openai.types.responses.response_function_tool_call_output_item import (
15
ResponseFunctionToolCallOutputItem,
16
)
17
+from openai.types.responses.response_output_item import McpCall
18
+from openai.types.responses.response_output_message import ResponseOutputMessage
19
+from openai.types.responses.response_output_text import ResponseOutputText
20
from openai.types.responses.tool import Mcp
21
from openai_harmony import Author, Message, Role, StreamState, TextContent
22
@@ -30,15 +34,15 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
34
get_streamable_parser_for_assistant,
35
render_for_completion,
36
)
33
-from vllm.entrypoints.openai.parser.responses_parser import (
34
- get_responses_parser_for_simple_context,
35
-)
37
from vllm.entrypoints.openai.responses.protocol import (
38
ResponseInputOutputItem,
39
ResponseRawMessageAndToken,
40
ResponsesRequest,
41
)
41
-from vllm.entrypoints.openai.responses.utils import construct_tool_dicts
42
+from vllm.entrypoints.openai.responses.utils import (
43
+ build_response_output_items,
44
+ construct_tool_dicts,
45
+)
46
from vllm.entrypoints.serve.utils.constants import MCP_PREFIX
47
from vllm.outputs import RequestOutput
48
from vllm.parser.abstract_parser import Parser
@@ -286,16 +290,24 @@ class ParsableContext(ConversationContext):
290
# not implemented yet for ParsableContext
291
self.all_turn_metrics: list[TurnMetrics] = []
292
289
- self.parser = get_responses_parser_for_simple_context(
290
- tokenizer=tokenizer,
291
- parser_cls=parser_cls,
292
- response_messages=response_messages,
293
- request=request,
294
- chat_template=chat_template,
295
- chat_template_content_format=chat_template_content_format,
296
- enable_auto_tools=enable_auto_tools,
297
- tool_call_id_type=tool_call_id_type,
298
- )
293
+ self.response_messages: list[ResponseInputOutputItem] = response_messages
294
+ self.num_init_messages = len(response_messages)
295
+ self.finish_reason: str | None = None
296
+ self.enable_auto_tools = enable_auto_tools
297
+ self.tool_call_id_type = tool_call_id_type
298
+
299
+ self.parser_instance: Parser | None = None
300
+ if parser_cls is not None:
301
+ chat_template_kwargs = request.build_chat_params(
302
+ default_template=chat_template,
303
+ default_template_content_format=chat_template_content_format,
304
+ ).chat_template_kwargs
305
+ self.parser_instance = parser_cls(
306
+ tokenizer,
307
+ tools=request.tools,
308
+ chat_template_kwargs=chat_template_kwargs,
309
+ )
310
+
311
self.parser_cls = parser_cls
312
self.request = request
313
@@ -318,11 +330,44 @@ class ParsableContext(ConversationContext):
330
self.num_output_tokens += len(output.outputs[0].token_ids or [])
331
if output.kv_transfer_params is not None:
332
self.kv_transfer_params = output.kv_transfer_params
321
- self.parser.process(output.outputs[0])
322
- output_token_ids = output.outputs[0].token_ids or []
323
- self._accumulated_token_ids.extend(output_token_ids)
333
325
- # only store if enable_response_messages is True, save memory
334
+ completion = output.outputs[0]
335
+ self.finish_reason = completion.finish_reason
336
+
337
+ if self.parser_instance is not None:
338
+ reasoning, content, tool_calls = self.parser_instance.parse(
339
+ completion.text,
340
+ self.request,
341
+ enable_auto_tools=self.enable_auto_tools,
342
+ )
343
+ self.response_messages.extend(
344
+ build_response_output_items(
345
+ reasoning=reasoning,
346
+ content=content,
347
+ tool_calls=tool_calls,
348
+ tool_call_id_type=self.tool_call_id_type,
349
+ )
350
+ )
351
+ elif completion.text:
352
+ self.response_messages.append(
353
+ ResponseOutputMessage(
354
+ type="message",
355
+ id=f"msg_{random_uuid()}",
356
+ status="completed",
357
+ role="assistant",
358
+ content=[
359
+ ResponseOutputText(
360
+ annotations=[],
361
+ type="output_text",
362
+ text=completion.text,
363
+ logprobs=None,
364
+ )
365
+ ],
366
+ )
367
+ )
368
+
369
+ self._accumulated_token_ids.extend(completion.token_ids or [])
370
+
371
if self.request.enable_response_messages:
372
output_prompt = output.prompt or ""
373
output_prompt_token_ids = output.prompt_token_ids or []
@@ -342,18 +387,18 @@ class ParsableContext(ConversationContext):
387
)
388
self.output_messages.append(
389
ResponseRawMessageAndToken(
345
- message=output.outputs[0].text,
346
- tokens=output.outputs[0].token_ids,
390
+ message=completion.text,
391
+ tokens=completion.token_ids,
392
)
393
)
394
395
def append_tool_output(self, output: list[ResponseInputOutputItem]) -> None:
351
- self.parser.response_messages.extend(output)
396
+ self.response_messages.extend(output)
397
398
def need_builtin_tool_call(self) -> bool:
399
"""Return true if the last message is a builtin tool call
400
that the request has enabled."""
356
- last_message = self.parser.response_messages[-1]
401
+ last_message = self.response_messages[-1]
402
if last_message.type != "function_call":
403
return False
404
if last_message.name in ("code_interpreter", "python"):
@@ -457,12 +502,12 @@ class ParsableContext(ConversationContext):
502
return [message]
503
504
async def call_tool(self) -> list[ResponseInputOutputItem]:
460
- if not self.parser.response_messages:
505
+ if not self.response_messages:
506
return []
462
- last_msg = self.parser.response_messages[-1]
507
+ last_msg = self.response_messages[-1]
508
# change this to a mcp_ function call
509
last_msg.id = f"{MCP_PREFIX}{random_uuid()}"
465
- self.parser.response_messages[-1] = last_msg
510
+ self.response_messages[-1] = last_msg
511
if last_msg.name == "code_interpreter":
512
return await self.call_python_tool(self._tool_sessions["python"], last_msg)
513
elif last_msg.name == "web_search_preview":
@@ -473,6 +518,29 @@ class ParsableContext(ConversationContext):
518
)
519
return []
520
521
+ def make_response_output_items(self) -> list[ResponseOutputItem]:
522
+ response_messages = self.response_messages[self.num_init_messages :]
523
+ output_messages: list[ResponseOutputItem] = []
524
+ for message in response_messages:
525
+ if not isinstance(message, ResponseFunctionToolCallOutputItem):
526
+ output_messages.append(message)
527
+ else:
528
+ if len(output_messages) == 0:
529
+ raise ValueError(
530
+ "Cannot have a FunctionToolCallOutput before FunctionToolCall."
531
+ )
532
+ if isinstance(output_messages[-1], ResponseFunctionToolCall):
533
+ output_messages[-1] = McpCall(
534
+ id=f"{MCP_PREFIX}{random_uuid()}",
535
+ arguments=output_messages[-1].arguments,
536
+ name=output_messages[-1].name,
537
+ server_label=output_messages[-1].name,
538
+ type="mcp_call",
539
+ status="completed",
540
+ output=message.output,
541
+ )
542
+ return output_messages
543
+
544
def render_for_completion(self):
545
raise NotImplementedError("Should not be called.")
546
vllm/entrypoints/openai/responses/serving.py
+3
-3
@@ -702,7 +702,7 @@ class OpenAIServingResponses(OpenAIServing):
702
elif isinstance(context, ParsableContext):
703
(engine_input,) = await self._render_next_turn(
704
context.request,
705
- context.parser.response_messages,
705
+ context.response_messages,
706
context.tool_dicts,
707
context.parser_cls,
708
context.chat_template,
@@ -805,7 +805,7 @@ class OpenAIServingResponses(OpenAIServing):
805
else:
806
status = "incomplete"
807
elif isinstance(context, ParsableContext):
808
- output = context.parser.make_response_output_items_from_parsable_context()
808
+ output = context.make_response_output_items()
809
810
if request.enable_response_messages:
811
input_messages = context.input_messages
@@ -816,7 +816,7 @@ class OpenAIServingResponses(OpenAIServing):
816
num_tool_output_tokens = 0
817
818
# Check finish reason from the parser
819
- if context.parser.finish_reason == "length":
819
+ if context.finish_reason == "length":
820
status = "incomplete"
821
else:
822
assert isinstance(context, SimpleContext)