[Frontend] Support bad_words in the /v1/completions endpoint (#46793)

Signed-off-by: sungbin1015 <sbin@solbox.com> Co-authored-by: Claude <noreply@anthropic.com>

sungbin1015 committed Jul 8, 2026 at 18:51 UTC 04a703e397a310d397bb15cc4d15365312782ff3
2 files changed +39
tests/entrypoints/openai/completion/test_completion.py
+37
@@ -9,6 +9,8 @@ import regex as re
9 from openai import BadRequestError
10
11 from tests.utils import RemoteOpenAIServer
12 +from vllm.entrypoints.openai.completion.protocol import CompletionRequest
13 +from vllm.sampling_params import SamplingParams
14 from vllm.tokenizers import get_tokenizer
15
16 # any model with a chat template should work here
@@ -730,3 +732,38 @@ async def test_invalid_grammar(client: openai.AsyncOpenAI, model_name: str):
732 "structured_outputs": {"grammar": invalid_simplified_sql_grammar}
733 },
734 )
735 +
736 +
737 +# Unit tests for bad_words in CompletionRequest.to_sampling_params()
738 +def test_completion_request_bad_words_to_sampling_params():
739 + """bad_words should be forwarded to SamplingParams (parity with chat)."""
740 + request = CompletionRequest(
741 + model="test-model",
742 + prompt="Hello",
743 + bad_words=["foo", "bar"],
744 + max_tokens=10,
745 + )
746 +
747 + sampling_params = request.to_sampling_params(
748 + max_tokens=10,
749 + default_sampling_params={},
750 + )
751 +
752 + assert isinstance(sampling_params, SamplingParams)
753 + assert sampling_params.bad_words == ["foo", "bar"]
754 +
755 +
756 +def test_completion_request_bad_words_default_empty():
757 + """bad_words defaults to an empty list, matching the chat endpoint."""
758 + request = CompletionRequest(
759 + model="test-model",
760 + prompt="Hello",
761 + max_tokens=10,
762 + )
763 +
764 + assert request.bad_words == []
765 + sampling_params = request.to_sampling_params(
766 + max_tokens=10,
767 + default_sampling_params={},
768 + )
769 + assert sampling_params.bad_words == []
vllm/entrypoints/openai/completion/protocol.py
+2
@@ -95,6 +95,7 @@ class CompletionRequest(OpenAIBaseModel):
95 )
96 allowed_token_ids: list[int] | None = None
97 prompt_logprobs: int | None = None
98 + bad_words: list[str] = Field(default_factory=list)
99 # --8<-- [end:completion-sampling-params]
100
101 # --8<-- [start:completion-extra-params]
@@ -371,6 +372,7 @@ class CompletionRequest(OpenAIBaseModel):
372 structured_outputs=self.structured_outputs,
373 logit_bias=self.logit_bias,
374 allowed_token_ids=self.allowed_token_ids,
375 + bad_words=self.bad_words,
376 extra_args=extra_args or None,
377 skip_clone=True, # Created fresh per request, safe to skip clone
378 repetition_detection=self.repetition_detection,