Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 41 additions & 10 deletions tensorrt_llm/serve/openai_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,17 +654,30 @@ class CompletionRequest(OpenAIBaseModel):
stream: Optional[bool] = False
stream_options: Optional[StreamOptions] = None
suffix: Optional[str] = None
temperature: Optional[float] = None
top_p: Optional[float] = None
temperature: Optional[float] = Field(
default=None, ge=0.0, description="Sampling temperature; must be >= 0.")
top_p: Optional[float] = Field(
default=None,
ge=0.0,
le=1.0,
description="Nucleus sampling probability; must be in [0, 1].")
user: Optional[str] = None
lora_request: Optional[LoRARequest] = None
prompt_ignore_length: Optional[int] = 0

# doc: begin-completion-sampling-params
use_beam_search: bool = False
top_k: int = 0
top_k: int = Field(
default=0,
ge=0,
description="Top-k sampling; must be >= 0 (0 disables top-k).")
top_p_min: float = 0.0
min_p: float = 0.0
min_p: float = Field(
default=0.0,
ge=0.0,
le=1.0,
description="Minimum token probability scaled by the top token's "
"probability; must be in [0, 1] (0 disables min-p).")
repetition_penalty: float = 1.0
# Unset by default so the engine picks its own beam-search defaults, as it
# does for requests coming through the Python API. Sending concrete values
Expand Down Expand Up @@ -1092,8 +1105,13 @@ class ChatCompletionRequest(OpenAIBaseModel):
stop: Optional[Union[str, List[str]]] = Field(default_factory=list)
stream: Optional[bool] = False
stream_options: Optional[StreamOptions] = None
temperature: Optional[float] = None
top_p: Optional[float] = None
temperature: Optional[float] = Field(
default=None, ge=0.0, description="Sampling temperature; must be >= 0.")
top_p: Optional[float] = Field(
default=None,
ge=0.0,
le=1.0,
description="Nucleus sampling probability; must be in [0, 1].")
tools: Optional[List[ChatCompletionToolsParam]] = None
tool_choice: Optional[Union[Literal["none", "auto", "required"],
ChatCompletionNamedToolChoiceParam]] = "none"
Expand Down Expand Up @@ -1121,9 +1139,17 @@ class ChatCompletionRequest(OpenAIBaseModel):
# doc: begin-chat-completion-sampling-params
best_of: Optional[int] = None
use_beam_search: bool = False
top_k: int = 0
top_k: int = Field(
default=0,
ge=0,
description="Top-k sampling; must be >= 0 (0 disables top-k).")
top_p_min: float = 0.0
min_p: float = 0.0
min_p: float = Field(
default=0.0,
ge=0.0,
le=1.0,
description="Minimum token probability scaled by the top token's "
"probability; must be in [0, 1] (0 disables min-p).")
repetition_penalty: float = 1.0
# Unset by default so the engine picks its own beam-search defaults, as it
# does for requests coming through the Python API. Sending concrete values
Expand Down Expand Up @@ -1623,12 +1649,17 @@ def _materialize_lazy_item_content(cls, value):
"priority"] = "auto"
store: Optional[bool] = True
stream: Optional[bool] = False
temperature: Optional[float] = None
temperature: Optional[float] = Field(
default=None, ge=0.0, description="Sampling temperature; must be >= 0.")
text: Optional[ResponseTextConfig] = None
tool_choice: ToolChoice = "auto"
tools: list[Tool] = Field(default_factory=list)
top_logprobs: Optional[int] = 0
top_p: Optional[float] = None
top_p: Optional[float] = Field(
default=None,
ge=0.0,
le=1.0,
description="Nucleus sampling probability; must be in [0, 1].")
truncation: Optional[Literal["auto", "disabled"]] = "disabled"
user: Optional[str] = None

Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ l0_cpu:
- unittest/llmapi/apps/test_responses_usage.py
- unittest/llmapi/apps/test_serving_extensions.py
- unittest/llmapi/apps/test_tool_parsers.py
- unittest/llmapi/apps/test_openai_protocol_validation.py
- unittest/llmapi/apps/test_web_search_shared.py
- unittest/llmapi/test_bench_async.py
- unittest/llmapi/test_config_overrides.py
Expand Down
98 changes: 98 additions & 0 deletions tests/unittest/llmapi/apps/test_openai_protocol_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Boundary validation for trtllm-serve request models (issue #6329).

Invalid sampling parameters are rejected at the pydantic boundary with a
field-specific ValidationError (surfaced to clients as HTTP 400 by the
server's RequestValidationError handler). SamplingParams._validate already
rejects these values later with a 400; the benefit here is the earlier,
field-named error and schema-visible bounds, not a crash fix.
"""

import pytest
from pydantic import ValidationError

from tensorrt_llm.serve.openai_protocol import (
ChatCompletionRequest,
CompletionRequest,
ResponsesRequest,
)


def _completion(**kw):
return CompletionRequest(model="m", prompt="hi", **kw)


def _chat(**kw):
return ChatCompletionRequest(model="m", messages=[{"role": "user", "content": "hi"}], **kw)


def _responses(**kw):
return ResponsesRequest(model="m", input="hi", **kw)


def _rejected_field(build, **kw):
"""Return the set of top-level field names that failed validation."""
with pytest.raises(ValidationError) as exc_info:
build(**kw)
return {err["loc"][0] for err in exc_info.value.errors()}


# top_k only exists on completion / chat requests.
BUILDERS_WITH_TOP_K = [_completion, _chat]
ALL_BUILDERS = [_completion, _chat, _responses]


@pytest.mark.parametrize("build", ALL_BUILDERS)
def test_valid_sampling_params_accepted(build):
# Boundary-valid values must pass unchanged.
req = build(temperature=0.0, top_p=1.0)
assert req.temperature == 0.0
assert req.top_p == 1.0


@pytest.mark.parametrize("build", ALL_BUILDERS)
def test_negative_temperature_rejected(build):
assert "temperature" in _rejected_field(build, temperature=-0.1)


@pytest.mark.parametrize("build", ALL_BUILDERS)
@pytest.mark.parametrize("bad_top_p", [-0.1, 1.1, 2.0])
def test_out_of_range_top_p_rejected(build, bad_top_p):
assert "top_p" in _rejected_field(build, top_p=bad_top_p)


@pytest.mark.parametrize("build", BUILDERS_WITH_TOP_K)
@pytest.mark.parametrize("bad_min_p", [-0.1, 1.1])
def test_out_of_range_min_p_rejected(build, bad_min_p):
assert "min_p" in _rejected_field(build, min_p=bad_min_p)


@pytest.mark.parametrize("build", BUILDERS_WITH_TOP_K)
def test_negative_top_k_rejected(build):
# top_k = -1 is vLLM's default; TRT-LLM's backend requires top_k >= 0, so
# reject it cleanly at the boundary rather than crashing.
assert "top_k" in _rejected_field(build, top_k=-1)


@pytest.mark.parametrize("build", BUILDERS_WITH_TOP_K)
@pytest.mark.parametrize("edge_min_p", [0.0, 1.0])
def test_min_p_boundaries_accepted(build, edge_min_p):
assert build(min_p=edge_min_p).min_p == edge_min_p


@pytest.mark.parametrize("build", BUILDERS_WITH_TOP_K)
def test_top_k_zero_accepted(build):
assert build(top_k=0).top_k == 0
Loading