@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 6e4a547176
Fork 0

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)