[Bugfix] Reject negative token ids as out-of-vocabulary (#51795)
Signed-off-by: Junhao Shen <junshen@nvidia.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Vadim Gimpelson <156319763+vadiklyutiy@users.noreply.github.com>
Junhao Shen committed
Aug 14, 2026 at 09:07 UTC
b216db3ed0f1262dc6b146a637b33159868433e9
2 files changed
+21
tests/entrypoints/pooling/embed/test_online.py
+11
@@ -89,6 +89,17 @@ def hf_model(hf_runner):
89
yield hf_model
90
91
92
+@pytest.mark.asyncio
93
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
94
+async def test_negative_token_ids(client: openai.AsyncOpenAI, model_name: str):
95
+ # A negative token id is out of vocabulary just like an over-large one, but
96
+ # is not caught by the upper-bound check. Unlike the OpenAI completion
97
+ # schema, the pooling schema does not constrain token ids to be
98
+ # non-negative, so the request reaches the shared engine-level validation.
99
+ with pytest.raises(openai.BadRequestError, match=".*out of vocabulary.*"):
100
+ await client.embeddings.create(model=model_name, input=[-1])
101
+
102
+
103
@pytest.mark.asyncio
104
@pytest.mark.parametrize("model_name", [MODEL_NAME])
105
async def test_basic(
vllm/v1/engine/input_processor.py
+10
@@ -483,6 +483,7 @@ class InputProcessor:
483
484
if prompt_ids and tokenizer is not None:
485
max_input_id = max(prompt_ids, default=0)
486
+ min_input_id = min(prompt_ids, default=0)
487
488
# NOTE: tokenizer.max_token_id is the tokenizer’s vocab size while
489
# self.model_config.get_vocab_size() is the model’s vocab size.
@@ -495,6 +496,15 @@ class InputProcessor:
496
# Here we take the max of the two to determine if a token id is
497
# truly out-of-vocabulary.
498
model_vocab_size = model_config.get_vocab_size()
499
+ # A negative id is out of vocabulary just like an over-large one,
500
+ # but is not caught by the upper-bound check below. Reject it here
501
+ # so it is not used as an embedding index downstream. This
502
+ # validation path is shared by generate, embedding and pooling
503
+ # requests, so the check covers all three.
504
+ if min_input_id < 0:
505
+ raise VLLMValidationError(
506
+ f"Token id {min_input_id} is out of vocabulary"
507
+ )
508
if max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1):
509
raise VLLMValidationError(
510
f"Token id {max_input_id} is out of vocabulary"