@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 2a49284c8a
Fork 0

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

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 09:45 UTC

Fix Responses JSON schema alias serialization (#38519)

Signed-off-by: noobhappylife <aratar1991@hotmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>

noobHappylife committed Apr 9, 2026 at 10:50 UTC 2a49284c8a24294463518c91b7a1ae402d1b51d1
6 files changed +104 -32
tests/entrypoints/openai/responses/test_harmony.py
+14 -16
@@ -999,17 +999,21 @@ async def test_mcp_tool_multi_turn(client: OpenAI, model_name: str, server):
999 (msg.get("recipient") or "").startswith("python")
1000 for msg in response1.output_messages
1001 )
1002 + parsed_output_messages = [
1003 + Message.from_dict(msg) for msg in response1.output_messages
1004 + ]
1005 tool_response_found = any(
1003 - msg.get("author", {}).get("role") == "tool"
1004 - and (msg.get("author", {}).get("name") or "").startswith("python")
1005 - for msg in response1.output_messages
1006 + (msg.author.role == "tool" and (msg.author.name or "").startswith("python"))
1007 + for msg in parsed_output_messages
1008 )
1009 assert tool_call_found, "MCP tool call not found in output_messages"
1010 assert tool_response_found, "MCP tool response not found in output_messages"
1011
1012 # No developer messages expected for elevated tools
1013 developer_msgs = [
1012 - msg for msg in response1.input_messages if msg["author"]["role"] == "developer"
1014 + msg
1015 + for msg in (Message.from_dict(raw) for raw in response1.input_messages)
1016 + if msg.author.role == "developer"
1017 ]
1018 assert len(developer_msgs) == 0, "No developer message expected for elevated tools"
1019
@@ -1119,12 +1123,10 @@ async def test_function_call_with_previous_input_messages(
1123 num_system = 0
1124 num_developer = 0
1125 num_tool = 0
1122 - for msg_dict in response_2.input_messages:
1123 - # input_messages use {"author": {"role": "..."}} format,
1124 - # not the top-level {"role": "..."} that Message.from_dict
1125 - # expects.
1126 - author = msg_dict.get("author", {})
1127 - role = author.get("role") if isinstance(author, dict) else None
1126 + for message in (
1127 + Message.from_dict(msg_dict) for msg_dict in response_2.input_messages
1128 + ):
1129 + role = message.author.role
1130 if role == "system":
1131 num_system += 1
1132 elif role == "developer":
@@ -1183,12 +1185,8 @@ async def test_system_prompt_override_no_duplication(client: OpenAI, model_name:
1185 assert response.output_text is not None
1186
1187 num_system = 0
1186 - for msg in response.input_messages:
1187 - # input_messages use {"author": {"role": "system"}} format,
1188 - # not the top-level {"role": "system"} that Message.from_dict expects.
1189 - author = msg.get("author", {})
1190 - role = author.get("role") if isinstance(author, dict) else None
1191 - if role == "system":
1188 + for message in (Message.from_dict(msg) for msg in response.input_messages):
1189 + if message.author.role == "system":
1190 num_system += 1
1191 assert num_system == 1, f"Expected 1 system message, got {num_system}"
1192
tests/entrypoints/openai/responses/test_mcp_tools.py
+6 -6
@@ -7,7 +7,7 @@ from __future__ import annotations
7 import pytest
8 import pytest_asyncio
9 from openai import OpenAI
10 -from openai_harmony import ToolDescription, ToolNamespaceConfig
10 +from openai_harmony import Message, ToolDescription, ToolNamespaceConfig
11
12 from tests.utils import RemoteOpenAIServer
13 from vllm.entrypoints.mcp.tool_server import MCPToolServer
@@ -173,10 +173,10 @@ class TestMCPEnabled:
173 if recipient and recipient.startswith("python"):
174 tool_call_found = True
175 assert message.get("channel") == "commentary"
176 - author = message.get("author", {})
177 - if author.get("role") == "tool" and (author.get("name") or "").startswith(
178 - "python"
179 - ):
176 + parsed_message = Message.from_dict(message)
177 + if parsed_message.author.role == "tool" and (
178 + parsed_message.author.name or ""
179 + ).startswith("python"):
180 tool_response_found = True
181 assert message.get("channel") == "commentary"
182
@@ -188,7 +188,7 @@ class TestMCPEnabled:
188 assert tool_response_found, "No Python tool response found"
189
190 for message in response.input_messages:
191 - assert message.get("author", {}).get("role") != "developer"
191 + assert Message.from_dict(message).author.role != "developer"
192
193 @pytest.mark.asyncio
194 @pytest.mark.parametrize("model_name", [MODEL_NAME])
tests/entrypoints/openai/responses/test_serving_responses.py
+71 -1
@@ -11,8 +11,12 @@ from openai.types.responses import (
11 ResponseReasoningItem,
12 ResponseReasoningTextDeltaEvent,
13 ResponseReasoningTextDoneEvent,
14 + ResponseTextConfig,
15 ResponseTextDeltaEvent,
16 )
17 +from openai.types.responses.response_format_text_json_schema_config import (
18 + ResponseFormatTextJSONSchemaConfig,
19 +)
20 from openai.types.responses.tool import (
21 CodeInterpreterContainerCodeInterpreterToolAuto,
22 LocalShell,
@@ -28,7 +32,13 @@ from vllm.entrypoints.openai.engine.protocol import (
32 RequestResponseMetadata,
33 )
34 from vllm.entrypoints.openai.responses.context import ConversationContext, SimpleContext
31 -from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
35 +from vllm.entrypoints.openai.responses.protocol import (
36 + ResponseCreatedEvent,
37 + ResponseRawMessageAndToken,
38 + ResponsesRequest,
39 + ResponsesResponse,
40 + serialize_message,
41 +)
42 from vllm.entrypoints.openai.responses.serving import (
43 OpenAIServingResponses,
44 _extract_allowed_tools_from_mcp_requests,
@@ -73,6 +83,16 @@ class MockConversationContext(ConversationContext):
83 pass
84
85
86 +def test_serialize_message_pydantic_model_returns_dict() -> None:
87 + msg = ResponseRawMessageAndToken(message="hello", tokens=[1, 2, 3])
88 +
89 + serialized = serialize_message(msg)
90 +
91 + assert isinstance(serialized, dict)
92 + assert serialized["type"] == "raw_message_tokens"
93 + assert serialized["message"] == "hello"
94 +
95 +
96 @pytest.fixture
97 def mock_serving_responses():
98 """Create a mock OpenAIServingResponses instance"""
@@ -132,6 +152,56 @@ def test_extract_tool_types(monkeypatch: pytest.MonkeyPatch) -> None:
152 }
153
154
155 +@pytest.mark.skip_global_cleanup
156 +def test_response_created_event_uses_public_json_schema_alias() -> None:
157 + schema = {
158 + "type": "object",
159 + "properties": {
160 + "event_name": {"type": "string"},
161 + "date": {"type": "string"},
162 + "participants": {"type": "array", "items": {"type": "string"}},
163 + },
164 + "required": ["event_name", "date", "participants"],
165 + "additionalProperties": False,
166 + }
167 + text = ResponseTextConfig()
168 + text.format = ResponseFormatTextJSONSchemaConfig(
169 + type="json_schema",
170 + name="calendar_event",
171 + schema=schema,
172 + description="A calendar event.",
173 + strict=True,
174 + )
175 + request = ResponsesRequest(
176 + model="test-model",
177 + input="Alice and Bob are going to a science fair on Friday.",
178 + text=text,
179 + )
180 + sampling_params = request.to_sampling_params(default_max_tokens=64)
181 + initial_response = ResponsesResponse.from_request(
182 + request=request,
183 + sampling_params=sampling_params,
184 + model_name="test-model",
185 + created_time=0,
186 + output=[],
187 + status="in_progress",
188 + usage=None,
189 + ).model_dump(mode="json", by_alias=True)
190 +
191 + fmt = initial_response["text"]["format"]
192 + assert fmt["schema"] == schema
193 + assert "schema_" not in fmt
194 +
195 + event = ResponseCreatedEvent(
196 + type="response.created",
197 + sequence_number=0,
198 + response=initial_response,
199 + )
200 + assert event.response.text is not None
201 + assert event.response.text.format is not None
202 + assert event.response.text.format.model_dump(by_alias=True)["schema"] == schema
203 +
204 +
205 class TestInitializeToolSessions:
206 """Test class for _initialize_tool_sessions method"""
207
vllm/entrypoints/openai/responses/api_router.py
+11 -7
@@ -39,7 +39,8 @@ async def _convert_stream_to_sse_events(
39 event_type = getattr(event, "type", "unknown")
40 # https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
41 event_data = (
42 - f"event: {event_type}\ndata: {event.model_dump_json(indent=None)}\n\n"
42 + f"event: {event_type}\ndata: "
43 + f"{event.model_dump_json(indent=None, by_alias=True)}\n\n"
44 )
45 yield event_data
46
@@ -65,10 +66,11 @@ async def create_responses(request: ResponsesRequest, raw_request: Request):
66
67 if isinstance(generator, ErrorResponse):
68 return JSONResponse(
68 - content=generator.model_dump(), status_code=generator.error.code
69 + content=generator.model_dump(mode="json", by_alias=True),
70 + status_code=generator.error.code,
71 )
72 elif isinstance(generator, ResponsesResponse):
71 - return JSONResponse(content=generator.model_dump())
73 + return JSONResponse(content=generator.model_dump(mode="json", by_alias=True))
74
75 return StreamingResponse(
76 content=_convert_stream_to_sse_events(generator), media_type="text/event-stream"
@@ -95,10 +97,11 @@ async def retrieve_responses(
97
98 if isinstance(response, ErrorResponse):
99 return JSONResponse(
98 - content=response.model_dump(), status_code=response.error.code
100 + content=response.model_dump(mode="json", by_alias=True),
101 + status_code=response.error.code,
102 )
103 elif isinstance(response, ResponsesResponse):
101 - return JSONResponse(content=response.model_dump())
104 + return JSONResponse(content=response.model_dump(mode="json", by_alias=True))
105 return StreamingResponse(
106 content=_convert_stream_to_sse_events(response), media_type="text/event-stream"
107 )
@@ -115,9 +118,10 @@ async def cancel_responses(response_id: str, raw_request: Request):
118
119 if isinstance(response, ErrorResponse):
120 return JSONResponse(
118 - content=response.model_dump(), status_code=response.error.code
121 + content=response.model_dump(mode="json", by_alias=True),
122 + status_code=response.error.code,
123 )
120 - return JSONResponse(content=response.model_dump())
124 + return JSONResponse(content=response.model_dump(mode="json", by_alias=True))
125
126
127 def attach_router(app: FastAPI):
vllm/entrypoints/openai/responses/protocol.py
+1 -1
@@ -106,7 +106,7 @@ def serialize_message(msg):
106 return msg.to_dict()
107 else:
108 # fallback to pydantic dump
109 - return msg.model_dump_json(by_alias=True)
109 + return msg.model_dump(mode="json", by_alias=True)
110
111
112 def serialize_messages(msgs):
vllm/entrypoints/openai/responses/serving.py
+1 -1
@@ -1909,7 +1909,7 @@ class OpenAIServingResponses(OpenAIServing):
1909 output=[],
1910 status="in_progress",
1911 usage=None,
1912 - ).model_dump()
1912 + ).model_dump(mode="json", by_alias=True)
1913 yield _increment_sequence_number_and_return(
1914 ResponseCreatedEvent(
1915 type="response.created",