fix(security): bound tokenizer work when explicit truncation_side is set (#47007)

Signed-off-by: jperezde <jperezde@redhat.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Juan Pérez de Algaba
2026-06-30 23:08:51 +08:00
committed by GitHub
co-authored by mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent db808b3961
commit c231d1f290
2 changed files with 94 additions and 9 deletions
+81
View File
@@ -64,12 +64,14 @@ class DummyTokenizer:
def __post_init__(self) -> None:
self._captured_encode_kwargs: dict = {}
self._captured_text_len: int = 0
def decode(self, tokens: list[int]):
return str(tokens)
def encode(self, text: str, **kwargs):
self._captured_encode_kwargs = kwargs
self._captured_text_len = len(text)
in_length = len(text)
truncation = kwargs.get("truncation")
@@ -366,6 +368,85 @@ class TestRenderPrompt:
assert results[0]["prompt_token_ids"] == tokens
assert results[0]["prompt"] == "[1, 2, 3, 4]"
def test_explicit_side_tokenizer_unbounded(self):
renderer = _build_renderer(MockModelConfig())
prompts = renderer.render_prompts(
_preprocess_prompt(renderer.model_config, "x" * 500)
)
results = renderer.tokenize_prompts(
prompts,
TokenizeParams(
max_total_tokens=100,
truncate_prompt_tokens=4,
truncation_side="left",
),
)
assert len(results) == 1
assert len(results[0]["prompt_token_ids"]) == 4
kwargs = renderer.tokenizer._captured_encode_kwargs
assert kwargs["truncation"] is False
def test_explicit_side_left_text(self):
renderer = _build_renderer(MockModelConfig())
prompts = renderer.render_prompts(
_preprocess_prompt(renderer.model_config, "x" * 50)
)
results = renderer.tokenize_prompts(
prompts,
TokenizeParams(
max_total_tokens=100,
truncate_prompt_tokens=5,
truncation_side="left",
),
)
assert len(results) == 1
assert len(results[0]["prompt_token_ids"]) == 5
assert results[0]["prompt_token_ids"] == list(range(45, 50))
def test_explicit_side_right_text(self):
renderer = _build_renderer(MockModelConfig())
prompts = renderer.render_prompts(
_preprocess_prompt(renderer.model_config, "x" * 50)
)
results = renderer.tokenize_prompts(
prompts,
TokenizeParams(
max_total_tokens=100,
truncate_prompt_tokens=5,
truncation_side="right",
),
)
assert len(results) == 1
assert len(results[0]["prompt_token_ids"]) == 5
assert results[0]["prompt_token_ids"] == list(range(5))
def test_explicit_side_text_pretokenization_guard(self):
renderer = _build_renderer(MockModelConfig(), max_chars_per_token=1)
prompts = renderer.render_prompts(
_preprocess_prompt(renderer.model_config, "x" * 500)
)
results = renderer.tokenize_prompts(
prompts,
TokenizeParams(
max_total_tokens=100,
truncate_prompt_tokens=4,
truncation_side="left",
),
)
assert len(results) == 1
assert len(results[0]["prompt_token_ids"]) == 4
assert renderer.tokenizer._captured_text_len <= 100
class TestRenderEmbedPrompt:
def _create_test_embed_bytes(self, tensor: torch.Tensor) -> bytes:
+13 -9
View File
@@ -314,10 +314,11 @@ class TokenizeParams:
# while still failing `self._token_len_check` as expected by users
max_length = self.max_input_tokens + 1
# Explicit truncation-side overrides require the full token sequence so
# we can slice from the requested side in _token_truncation. Disable
# tokenizer-level truncation because generation tokenizers default to
# left truncation while callers may request right truncation.
# Explicit truncation-side overrides require the full token sequence
# so we can slice from the requested side in _token_truncation.
# Disable tokenizer-level truncation because its default side may
# differ from the requested side. The defense against unbounded
# tokenization lives in _text_len_check (character-level pre-trim).
if self.truncation_side is not None and self.truncate_prompt_tokens is not None:
return dict(
truncation=False,
@@ -333,15 +334,13 @@ class TokenizeParams:
def _text_len_check(self, tokenizer: TokenizerLike | None, text: str) -> str:
"""Apply length checks to prompt text if necessary."""
max_input_tokens = self.max_input_tokens
if max_input_tokens is None:
if max_input_tokens is None or tokenizer is None:
return text
if self.truncate_prompt_tokens is None and tokenizer is not None:
max_input_chars = max_input_tokens * tokenizer.max_chars_per_token
max_input_chars = max_input_tokens * tokenizer.max_chars_per_token
if self.truncate_prompt_tokens is None:
if len(text) > max_input_chars:
# To save resources, fail the request outright without even
# attempting tokenization
raise VLLMValidationError(
f"This model's maximum context length is "
f"{self.max_total_tokens} tokens. However, you requested "
@@ -354,6 +353,11 @@ class TokenizeParams:
parameter="input_text",
value=len(text),
)
elif self.truncation_side is not None and len(text) > max_input_chars:
if self.truncation_side == "left":
text = text[-max_input_chars:]
else:
text = text[:max_input_chars]
return text