diff --git a/tests/v1/entrypoints/openai/test_chat_completion.py b/tests/v1/entrypoints/openai/test_chat_completion.py index 522c72b559556..b5aa20448dfcb 100644 --- a/tests/v1/entrypoints/openai/test_chat_completion.py +++ b/tests/v1/entrypoints/openai/test_chat_completion.py @@ -138,3 +138,23 @@ async def test_invalid_grammar(client: openai.AsyncOpenAI, model_name: str): "structured_outputs": {"grammar": invalid_simplified_sql_grammar} }, ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "model_name", + [MODEL_NAME], +) +async def test_empty_grammar(client: openai.AsyncOpenAI, model_name: str) -> None: + prompt = "Say hello" + with pytest.raises((openai.BadRequestError, openai.APIError)): + await client.chat.completions.create( + model=model_name, + messages=[ + { + "role": "user", + "content": prompt, + } + ], + extra_body={"structured_outputs": {"grammar": ""}}, + ) diff --git a/vllm/v1/engine/processor.py b/vllm/v1/engine/processor.py index c49fd1bde8b98..f2d992403e1a8 100644 --- a/vllm/v1/engine/processor.py +++ b/vllm/v1/engine/processor.py @@ -270,6 +270,12 @@ class Processor: raise ValueError( f"Choice '{params.structured_outputs.choice}' cannot be an empty list" # noqa: E501 ) + # Reject empty string grammar early to avoid engine-side crashes + if ( + isinstance(params.structured_outputs.grammar, str) + and params.structured_outputs.grammar.strip() == "" + ): + raise ValueError("structured_outputs.grammar cannot be an empty string") if backend.startswith("xgrammar"): # xgrammar with no fallback