diff --git a/tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py b/tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py index 1c349e8d67..aa90618a90 100644 --- a/tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py +++ b/tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py @@ -422,6 +422,31 @@ async def test_stop_string_workflow(client, tokenizer, messages): assert generate_res == completions_res +@pytest.mark.asyncio +@pytest.mark.skipif( + envs.VLLM_USE_RUST_FRONTEND, + reason="--tokens-only is not supported by the Rust frontend", +) +@pytest.mark.parametrize("server", [["--tokens-only"]], indirect=True) +async def test_stop_strings_rejected_when_tokens_only(client): + """--tokens-only forces detokenize=False after request validation, so + stop strings must be rejected explicitly rather than silently ignored.""" + sampling_params = {"max_tokens": 5, "stop": ["never"]} + payload = { + "model": MODEL_NAME, + "token_ids": [1, 2, 3], + "sampling_params": sampling_params, + "stream": False, + } + resp = await client.post(GEN_ENDPOINT, json=payload) + assert resp.status_code == 400 + assert "stop strings" in resp.json()["error"]["message"] + + sampling_params["stop"] = None + resp = await client.post(GEN_ENDPOINT, json=payload) + resp.raise_for_status() + + @pytest.mark.asyncio @pytest.mark.parametrize( "server", diff --git a/vllm/entrypoints/scale_out/token_in_token_out/serving.py b/vllm/entrypoints/scale_out/token_in_token_out/serving.py index a8a2804a06..7813c7facc 100644 --- a/vllm/entrypoints/scale_out/token_in_token_out/serving.py +++ b/vllm/entrypoints/scale_out/token_in_token_out/serving.py @@ -134,6 +134,16 @@ class ServingTokens(GenerateBaseServing): f"sampling_params.n must be at most the server's max_num_seqs " f"({max_num_seqs}), got {sampling_params.n}." ) + if self.force_no_detokenize and sampling_params.stop: + # SamplingParams rejects stop with detokenize=False at request + # validation, but this server forces detokenize=False afterwards, + # so the combination must be rejected here or stop strings are + # silently never applied. + return self.create_error_response( + "stop strings are not supported on a --tokens-only server " + "because detokenization is disabled. Check stop strings on " + "the coordinator, or use stop_token_ids." + ) try: msgspec.msgpack.encode( (