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",