fix(openai): reject non-numeric logprobs with 400 instead of 500 (#49144)

Signed-off-by: Chenglun Hu <chenglunhu@gmail.com>

hcl committed Jul 20, 2026 at 18:04 UTC 530ee36a0d99869804c0af68b41007072b609480
4 files changed +49
tests/entrypoints/openai/chat_completion/test_chat_error.py
+12
@@ -515,3 +515,15 @@ def test_structured_outputs_structural_tag_invalid(structural_tag):
515 messages=[{"role": "user", "content": "hello"}],
516 structured_outputs={"structural_tag": structural_tag},
517 )
518 +
519 +
520 +@pytest.mark.parametrize("field_name", ["prompt_logprobs", "top_logprobs"])
521 +def test_non_numeric_logprobs_rejected(field_name):
522 + """A non-numeric logprobs value must be a clean 400 validation error, not a
523 + TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
524 + with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
525 + ChatCompletionRequest(
526 + model=MODEL_NAME,
527 + messages=[{"role": "user", "content": "hello"}],
528 + **{field_name: "2"},
529 + )
tests/entrypoints/openai/completion/test_completion_error.py
+13
@@ -610,3 +610,16 @@ class TestCompletionPromptListLimit:
610 max_tokens=1,
611 )
612 assert len(request.prompt_embeds) == 5
613 +
614 +
615 +@pytest.mark.parametrize("field_name", ["prompt_logprobs", "logprobs"])
616 +def test_non_numeric_logprobs_rejected(field_name):
617 + """A non-numeric logprobs value must be a clean 400 validation error, not a
618 + TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
619 + with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
620 + CompletionRequest(
621 + model=MODEL_NAME,
622 + prompt="Test prompt",
623 + max_tokens=10,
624 + **{field_name: "2"},
625 + )
vllm/entrypoints/openai/chat_completion/protocol.py
+12
@@ -757,6 +757,18 @@ class ChatCompletionRequest(OpenAIBaseModel):
757 parameter="logprob_token_ids",
758 )
759
760 + # These fields are integers, but `mode="before"` runs on the raw
761 + # request data, so a non-numeric value (e.g. a JSON string) would
762 + # reach the comparisons below and raise TypeError -> HTTP 500. Reject
763 + # it here so the client gets a clean 400 instead.
764 + for field_name in ("prompt_logprobs", "top_logprobs"):
765 + field_value = data.get(field_name)
766 + if field_value is not None and not isinstance(field_value, (int, float)):
767 + raise VLLMValidationError(
768 + f"`{field_name}` must be an integer.",
769 + parameter=field_name,
770 + value=field_value,
771 + )
772 if (prompt_logprobs := data.get("prompt_logprobs")) is not None:
773 if data.get("stream") and (prompt_logprobs > 0 or prompt_logprobs == -1):
774 raise VLLMValidationError(
vllm/entrypoints/openai/completion/protocol.py
+12
@@ -468,6 +468,18 @@ class CompletionRequest(OpenAIBaseModel):
468 parameter="logprob_token_ids",
469 )
470
471 + # These fields are integers, but `mode="before"` runs on the raw
472 + # request data, so a non-numeric value (e.g. a JSON string) would
473 + # reach the comparisons below and raise TypeError -> HTTP 500. Reject
474 + # it here so the client gets a clean 400 instead.
475 + for field_name in ("prompt_logprobs", "logprobs"):
476 + field_value = data.get(field_name)
477 + if field_value is not None and not isinstance(field_value, (int, float)):
478 + raise VLLMValidationError(
479 + f"`{field_name}` must be an integer.",
480 + parameter=field_name,
481 + value=field_value,
482 + )
483 if (prompt_logprobs := data.get("prompt_logprobs")) is not None:
484 if data.get("stream") and (prompt_logprobs > 0 or prompt_logprobs == -1):
485 raise VLLMValidationError(