Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
53223f2
[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
AndreasKaratzas May 18, 2026
f5b07f2
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 19, 2026
7128304
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 19, 2026
01e5eac
[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
AndreasKaratzas May 19, 2026
26d4c22
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 19, 2026
41abe07
[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
AndreasKaratzas May 19, 2026
de4e34b
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 20, 2026
d596a9f
[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
AndreasKaratzas May 20, 2026
1082646
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 20, 2026
80e73ce
[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
AndreasKaratzas May 20, 2026
fccb373
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 21, 2026
d86dfd2
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 23, 2026
0f205a9
Reverting bad merge
AndreasKaratzas May 23, 2026
9269a1e
Merge remote-tracking branch 'origin/main' into akaratza_stabilize_en…
AndreasKaratzas May 24, 2026
ef7bea3
Honor shutdown timeouts and update weight-transfer mocks
AndreasKaratzas May 24, 2026
5f13356
Removing eager check
AndreasKaratzas May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions vllm/entrypoints/chat_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1818,12 +1818,28 @@ def _postprocess_messages(messages: list[ConversationMessage]) -> None:
continue

for item in tool_calls:
if not isinstance(item, dict):
raise VLLMValidationError(
"assistant tool_calls entries must be objects.",
parameter="tool_calls",
)

function = item.get("function")
if item.get("type", "function") != "function" or not isinstance(
function, dict
):
raise VLLMValidationError(
"chat completions only support assistant tool_calls "
"of type 'function'.",
parameter="tool_calls",
)

# if arguments is None or empty string, set to {}
if content := item["function"].get("arguments"):
if content := function.get("arguments"):
if not isinstance(content, (dict, list)):
item["function"]["arguments"] = json.loads(content)
function["arguments"] = json.loads(content)
else:
item["function"]["arguments"] = {}
function["arguments"] = {}


def parse_chat_messages(
Expand Down
84 changes: 50 additions & 34 deletions vllm/entrypoints/openai/chat_completion/batch_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from http import HTTPStatus

from fastapi import Request
from pydantic import ValidationError

from vllm.entrypoints.chat_utils import ConversationMessage
from vllm.entrypoints.openai.chat_completion.protocol import (
Expand Down Expand Up @@ -81,21 +82,24 @@ async def render_batch_chat_request(
all_engine_prompts: list[EngineInput] = []

for messages in request.messages:
single_request = request.to_chat_completion_request(messages)
if render.use_harmony:
conversation, engine_prompts = render._make_request_with_harmony(
single_request, should_include_tools=tool_dicts is not None
)
else:
conversation, engine_prompts = await render.preprocess_chat(
single_request,
messages,
default_template=render.chat_template,
default_template_content_format=render.chat_template_content_format,
default_template_kwargs=render.default_chat_template_kwargs,
tool_dicts=tool_dicts,
tool_parser=tool_parser,
)
try:
single_request = request.to_chat_completion_request(messages)
if render.use_harmony:
conversation, engine_prompts = render._make_request_with_harmony(
single_request, should_include_tools=tool_dicts is not None
)
else:
conversation, engine_prompts = await render.preprocess_chat(
single_request,
messages,
default_template=render.chat_template,
default_template_content_format=render.chat_template_content_format,
default_template_kwargs=render.default_chat_template_kwargs,
tool_dicts=tool_dicts,
tool_parser=tool_parser,
)
except (ValidationError, ValueError) as e:
return self.create_error_response(e)
Comment thread
DarkLight1337 marked this conversation as resolved.
Outdated
all_conversations.append(conversation)
all_engine_prompts.append(engine_prompts[0])

Expand All @@ -114,10 +118,13 @@ async def create_batch_chat_completion(
"""
tokenizer = self.renderer.tokenizer
assert tokenizer is not None
single_requests = [
request.to_chat_completion_request(messages)
for messages in request.messages
]
try:
single_requests = [
request.to_chat_completion_request(messages)
for messages in request.messages
]
except (ValidationError, ValueError) as e:
return self.create_error_response(e)

reasoning_parser: ReasoningParser | None = None
if self.reasoning_parser_cls:
Expand Down Expand Up @@ -149,19 +156,22 @@ async def create_batch_chat_completion(
generators: list[AsyncGenerator[RequestOutput, None]] = []
for i, engine_prompt in enumerate(engine_prompts):
sub_request_id = f"{request_id}_{i}"
max_tokens = get_max_tokens(
max_model_len,
request.max_completion_tokens
if request.max_completion_tokens is not None
else request.max_tokens,
self._extract_prompt_len(engine_prompt),
self.default_sampling_params,
self.override_max_tokens,
)
single_request = single_requests[i]
sampling_params = single_request.to_sampling_params(
max_tokens, self.default_sampling_params
)
try:
max_tokens = get_max_tokens(
max_model_len,
request.max_completion_tokens
if request.max_completion_tokens is not None
else request.max_tokens,
self._extract_prompt_len(engine_prompt),
self.default_sampling_params,
self.override_max_tokens,
)
single_request = single_requests[i]
sampling_params = single_request.to_sampling_params(
max_tokens, self.default_sampling_params
)
except ValueError as e:
return self.create_error_response(e)
self._log_inputs(
sub_request_id,
engine_prompt,
Expand Down Expand Up @@ -218,14 +228,14 @@ async def chat_completion_full_generator_batch(
``check_batch_mode`` validator, so neither needs to be handled here.
"""
created_time = int(time.time())
role = self.get_chat_request_role(request) # type: ignore[arg-type]

final_results: dict[int, RequestOutput] = {}
try:
async for prompt_idx, res in merge_async_iterators(*generators):
final_results[prompt_idx] = res
except asyncio.CancelledError:
return self.create_error_response("Client disconnected")
except ValueError as e:
return self.create_error_response(e)

choices: list[ChatCompletionResponseChoice] = []
total_prompt_tokens = 0
Expand Down Expand Up @@ -275,6 +285,12 @@ async def chat_completion_full_generator_batch(
reasoning = None
content = output.text

role = (
self.response_role
if request.add_generation_prompt
else request.messages[prompt_idx][-1]["role"]
)

message = ChatMessage(role=role, reasoning=reasoning, content=content)

if request.echo:
Expand Down
4 changes: 3 additions & 1 deletion vllm/entrypoints/openai/chat_completion/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,9 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
- The ``n`` parameter must be 1 (or omitted).
"""

messages: list[list[ChatCompletionMessageParam]] = Field(..., min_length=1)
messages: list[Annotated[list[ChatCompletionMessageParam], Field(min_length=1)]] = (
Field(..., min_length=1)
)
model: str | None = None

# Shared sampling / generation fields — mirror ChatCompletionRequest.
Expand Down
2 changes: 2 additions & 0 deletions vllm/entrypoints/openai/completion/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ async def _create_completion(
)
except asyncio.CancelledError:
return self.create_error_response("Client disconnected")
except ValueError as e:
return self.create_error_response(e)

# When user requests streaming but we don't stream, we still need to
# return a streaming response with a single event.
Expand Down
10 changes: 10 additions & 0 deletions vllm/entrypoints/openai/engine/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from http import HTTPStatus
from typing import Any, ClassVar, Generic, Protocol, TypeAlias, TypeVar

import msgspec
from fastapi import Request
from openai.types.responses import ToolChoiceFunction
from pydantic import ConfigDict, TypeAdapter, ValidationError
Expand Down Expand Up @@ -426,6 +427,15 @@ async def _with_kv_transfer_rejection_cleanup(
"""Wrap a `create_*` coroutine so that, if it raises or returns an
ErrorResponse (i.e. the request never reached the engine), the KV
connector is notified to free any pinned remote-prefill blocks."""
if request.kv_transfer_params is not None:
try:
msgspec.msgpack.encode(request.kv_transfer_params)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this cause additional overhead? From my understanding this is called even if no error occurs in the request

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it. The cleanup wrapper now only handles the KV rejection cleanup path again. So, initially, the "eager" msgpack encode was meant as a quick validation guard, but indeed it is not the right place for that check cause it also runs on successful requests.

except (OverflowError, TypeError, ValueError) as e:
close = getattr(awaitable, "close", None)
if close is not None:
close()
return self.create_error_response(e) # type: ignore[return-value]

kv_transfer_params = self.has_kv_connector and request.kv_transfer_params
if not kv_transfer_params or not kv_transfer_params.get("do_remote_prefill"):
return await awaitable
Expand Down
2 changes: 1 addition & 1 deletion vllm/entrypoints/serve/disagg/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class GenerateRequest(BaseModel):
"through out the inference process and return in response."
),
)
token_ids: list[int]
token_ids: list[int] = Field(min_length=1)
"""The token ids to generate text from."""

@field_validator("token_ids")
Expand Down
26 changes: 19 additions & 7 deletions vllm/entrypoints/serve/disagg/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ async def serve_tokens(
if raw_request:
raw_request.state.request_metadata = request_metadata

sampling_params = request.sampling_params
max_num_seqs = self.engine_client.vllm_config.scheduler_config.max_num_seqs
if sampling_params.n > max_num_seqs:
return self.create_error_response(
f"sampling_params.n must be at most the server's max_num_seqs "
f"({max_num_seqs}), got {sampling_params.n}."
)

engine_input: EngineInput
if features := request.features:
# Convert PlaceholderRangeInfo → PlaceholderRange per modality.
Expand Down Expand Up @@ -155,16 +163,20 @@ async def serve_tokens(
cache_salt=request.cache_salt,
)
else:
(engine_input,) = await self.openai_serving_render.preprocess_completion(
request,
prompt_input=request.token_ids,
prompt_embeds=None,
skip_mm_cache=True,
)
try:
(
engine_input,
) = await self.openai_serving_render.preprocess_completion(
request,
prompt_input=request.token_ids,
prompt_embeds=None,
skip_mm_cache=True,
)
except ValueError as e:
return self.create_error_response(e)

# Schedule the request and get the result generator.
result_generator: AsyncGenerator[RequestOutput, None] | None = None
sampling_params = request.sampling_params

# Apply server-side ``max_tokens`` defaulting when the client did
# not set it, matching the OpenAI-compat endpoints. ``SamplingParams``
Expand Down
49 changes: 29 additions & 20 deletions vllm/entrypoints/serve/render/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,23 +247,29 @@ async def render_chat(
if error_check_ret is not None:
return error_check_ret

conversation, engine_inputs = await self.preprocess_chat(
request,
request.messages,
default_template=self.chat_template,
default_template_content_format=self.chat_template_content_format,
default_template_kwargs=self.default_chat_template_kwargs,
tool_dicts=tool_dicts,
tool_parser=tool_parser,
skip_mm_cache=skip_mm_cache,
reasoning_parser=self.reasoning_parser,
)
try:
conversation, engine_inputs = await self.preprocess_chat(
request,
request.messages,
default_template=self.chat_template,
default_template_content_format=self.chat_template_content_format,
default_template_kwargs=self.default_chat_template_kwargs,
tool_dicts=tool_dicts,
tool_parser=tool_parser,
skip_mm_cache=skip_mm_cache,
reasoning_parser=self.reasoning_parser,
)
except ValueError as e:
return self.create_error_response(e)
else:
# For GPT-OSS.
should_include_tools = tool_dicts is not None
conversation, engine_inputs = self._make_request_with_harmony(
request, should_include_tools
)
try:
conversation, engine_inputs = self._make_request_with_harmony(
request, should_include_tools
)
except ValueError as e:
return self.create_error_response(e)

return conversation, engine_inputs

Expand Down Expand Up @@ -346,12 +352,15 @@ async def render_completion(
"prompt_logprobs is not compatible with prompt embeds."
)

engine_inputs = await self.preprocess_completion(
request,
prompt_input=request.prompt,
prompt_embeds=request.prompt_embeds,
skip_mm_cache=skip_mm_cache,
)
try:
engine_inputs = await self.preprocess_completion(
request,
prompt_input=request.prompt,
prompt_embeds=request.prompt_embeds,
skip_mm_cache=skip_mm_cache,
)
except ValueError as e:
return self.create_error_response(e)

return engine_inputs

Expand Down
Loading