diff --git a/tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py b/tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py index 22e17a14dcd..b415fa116da 100644 --- a/tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py +++ b/tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py @@ -76,3 +76,60 @@ async def test_chat_logit_bias_invalid(client): assert error.status_code == 400 assert str(invalid_token_id) in error_message assert str(vocab_size) in error_message + + +@pytest.mark.asyncio +async def test_chat_logit_bias_non_integer_key(client): + """Test that a non-integer logit_bias key is rejected with a clean, + informative error instead of a raw 'invalid literal for int()' message.""" + with pytest.raises(openai.BadRequestError) as excinfo: + await client.chat.completions.create( + model=MODEL_NAME, + messages=[{"role": "user", "content": "Testing invalid logit bias key"}], + max_tokens=5, + logit_bias={"not_a_token_id": 50}, + ) + + error = excinfo.value + error_message = str(error) + + assert error.status_code == 400 + assert "not_a_token_id" in error_message + assert "logit_bias" in error_message + + +@pytest.mark.asyncio +async def test_chat_logit_bias_non_numeric_value(client): + """Test that a non-numeric logit_bias value is rejected with a message + that names the specific offending token, not just a generic TypeError.""" + with pytest.raises(openai.BadRequestError) as excinfo: + await client.chat.completions.create( + model=MODEL_NAME, + messages=[{"role": "user", "content": "Testing invalid logit bias value"}], + max_tokens=5, + logit_bias={"1": "not_a_number"}, + ) + + error = excinfo.value + error_message = str(error) + + assert error.status_code == 400 + assert "logit_bias" in error_message + + +@pytest.mark.asyncio +async def test_chat_logit_bias_multiple_non_integer_keys(client): + """Test that ALL invalid logit_bias keys are reported together, + not just the first one encountered.""" + with pytest.raises(openai.BadRequestError) as excinfo: + await client.chat.completions.create( + model=MODEL_NAME, + messages=[{"role": "user", "content": "Testing multiple bad keys"}], + max_tokens=5, + logit_bias={"bad1": 50.0, "bad2": 20.0}, + ) + + error_message = str(excinfo.value) + assert excinfo.value.status_code == 400 + assert "bad1" in error_message + assert "bad2" in error_message diff --git a/vllm/sampling_params.py b/vllm/sampling_params.py index c8c5c4d80bd..2138ff7f95c 100644 --- a/vllm/sampling_params.py +++ b/vllm/sampling_params.py @@ -385,12 +385,31 @@ class SamplingParams( repetition_detection: RepetitionDetectionParams | None = None, ) -> "SamplingParams": if logit_bias is not None: - # Convert token_id to integer - # Clamp the bias between -100 and 100 per OpenAI API spec - logit_bias = { - int(token): min(100.0, max(-100.0, bias)) - for token, bias in logit_bias.items() - } + # Fast path uses a dict comprehension; on failure we iterate once + # to identify the exact offending entry for the error message. + try: + logit_bias = { + int(token): min(100.0, max(-100.0, bias)) + for token, bias in logit_bias.items() + } + except (ValueError, TypeError): + invalid_keys = [] + converted_logit_bias = {} + for token, bias in logit_bias.items(): + try: + token_id = int(token) + except (ValueError, TypeError): + invalid_keys.append(token) + continue + converted_logit_bias[token_id] = min(100.0, max(-100.0, bias)) + if invalid_keys: + raise VLLMValidationError( + f"logit_bias contains key(s) that cannot be " + f"converted to integer token IDs: {invalid_keys!r}", + parameter="logit_bias", + value=invalid_keys, + ) from None + logit_bias = converted_logit_bias return SamplingParams( n=1 if n is None else n,