[Bugfix][Frontend] Reject stop strings on --tokens-only servers instead of silently ignoring them (#57058)

Signed-off-by: Shimi Bandiel <shimib@google.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Shimi Bandiel committed Sep 17, 2026 at 15:02 UTC e8a2a0a8bbe675885ced6efddd49c1da0ff0a218
2 files changed +35
tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
+25
@@ -422,6 +422,31 @@ async def test_stop_string_workflow(client, tokenizer, messages):
422 assert generate_res == completions_res
423
424
425 +@pytest.mark.asyncio
426 +@pytest.mark.skipif(
427 + envs.VLLM_USE_RUST_FRONTEND,
428 + reason="--tokens-only is not supported by the Rust frontend",
429 +)
430 +@pytest.mark.parametrize("server", [["--tokens-only"]], indirect=True)
431 +async def test_stop_strings_rejected_when_tokens_only(client):
432 + """--tokens-only forces detokenize=False after request validation, so
433 + stop strings must be rejected explicitly rather than silently ignored."""
434 + sampling_params = {"max_tokens": 5, "stop": ["never"]}
435 + payload = {
436 + "model": MODEL_NAME,
437 + "token_ids": [1, 2, 3],
438 + "sampling_params": sampling_params,
439 + "stream": False,
440 + }
441 + resp = await client.post(GEN_ENDPOINT, json=payload)
442 + assert resp.status_code == 400
443 + assert "stop strings" in resp.json()["error"]["message"]
444 +
445 + sampling_params["stop"] = None
446 + resp = await client.post(GEN_ENDPOINT, json=payload)
447 + resp.raise_for_status()
448 +
449 +
450 @pytest.mark.asyncio
451 @pytest.mark.parametrize(
452 "server",
vllm/entrypoints/scale_out/token_in_token_out/serving.py
+10
@@ -134,6 +134,16 @@ class ServingTokens(GenerateBaseServing):
134 f"sampling_params.n must be at most the server's max_num_seqs "
135 f"({max_num_seqs}), got {sampling_params.n}."
136 )
137 + if self.force_no_detokenize and sampling_params.stop:
138 + # SamplingParams rejects stop with detokenize=False at request
139 + # validation, but this server forces detokenize=False afterwards,
140 + # so the combination must be rejected here or stop strings are
141 + # silently never applied.
142 + return self.create_error_response(
143 + "stop strings are not supported on a --tokens-only server "
144 + "because detokenization is disabled. Check stop strings on "
145 + "the coordinator, or use stop_token_ids."
146 + )
147 try:
148 msgspec.msgpack.encode(
149 (