main
py 117 lines 3.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from types import SimpleNamespace
5
6 import pytest
7 import xgrammar as xgr
8
9 from vllm.config import StructuredOutputsConfig, VllmConfig
10 from vllm.entrypoints.openai.chat_completion.protocol import (
11 ChatCompletionToolsParam,
12 )
13 from vllm.parser.plamo3 import (
14 BEGIN_TOOL_ARGUMENTS,
15 BEGIN_TOOL_NAME,
16 BEGIN_TOOL_REQUEST,
17 BEGIN_TOOL_REQUESTS,
18 END_TOOL_ARGUMENTS,
19 END_TOOL_NAME,
20 END_TOOL_REQUEST,
21 END_TOOL_REQUESTS,
22 )
23 from vllm.tool_parsers.structural_tag_registry import get_model_structural_tag
24 from vllm.v1.structured_output.backend_types import StructuredOutputOptions
25 from vllm.v1.structured_output.backend_xgrammar import XgrammarBackend
26
27
28 class Plamo3Tokenizer:
29 """Minimal stand-in for PLaMo's remote-code PythonBackend tokenizer."""
30
31 eos_token_id = 0
32
33 def __init__(self, tokens: list[str]) -> None:
34 self._vocab = {token: token_id for token_id, token in enumerate(tokens)}
35
36 def get_vocab(self) -> dict[str, int]:
37 return self._vocab
38
39 def init_xgrammar(self):
40 stop_token_ids = [self.eos_token_id]
41 tokenizer_info = xgr.TokenizerInfo(
42 encoded_vocab=list(self._vocab),
43 vocab_type=xgr.VocabType.BYTE_FALLBACK,
44 vocab_size=len(self._vocab),
45 stop_token_ids=stop_token_ids,
46 )
47 return tokenizer_info, stop_token_ids
48
49
50 def make_vllm_config() -> VllmConfig:
51 config = VllmConfig(
52 structured_outputs_config=StructuredOutputsConfig(backend="xgrammar"),
53 )
54 config.model_config = SimpleNamespace(
55 hf_config=SimpleNamespace(model_type="plamo3")
56 )
57 return config
58
59
60 def test_plamo3_python_tokenizer_compiles_and_accepts_structural_tag():
61 call_tokens = [
62 BEGIN_TOOL_REQUESTS,
63 BEGIN_TOOL_REQUEST,
64 BEGIN_TOOL_NAME,
65 "get_weather",
66 END_TOOL_NAME,
67 BEGIN_TOOL_ARGUMENTS,
68 '{"city":"Tokyo"}',
69 END_TOOL_ARGUMENTS,
70 END_TOOL_REQUEST,
71 END_TOOL_REQUESTS,
72 ]
73 tokenizer = Plamo3Tokenizer(["<eos>", *call_tokens])
74 backend = XgrammarBackend(
75 make_vllm_config(),
76 tokenizer=tokenizer,
77 vocab_size=len(tokenizer.get_vocab()),
78 )
79 tools = [
80 ChatCompletionToolsParam(
81 type="function",
82 function={
83 "name": "get_weather",
84 "parameters": {
85 "type": "object",
86 "properties": {"city": {"type": "string"}},
87 "required": ["city"],
88 },
89 "strict": True,
90 },
91 )
92 ]
93 tag = get_model_structural_tag("plamo3", tools, "required", reasoning=False)
94 assert tag is not None
95
96 grammar = backend.compile_grammar(
97 StructuredOutputOptions.STRUCTURAL_TAG,
98 tag.model_dump_json(),
99 )
100 token_ids = [tokenizer.get_vocab()[token] for token in call_tokens]
101 assert grammar.accept_tokens("plamo3", token_ids)
102
103
104 @pytest.mark.parametrize("value", ['"', "あ"])
105 def test_plamo3_grammar_matches_decoded_bytes(value):
106 pieces = [f"<0x{byte:02X}>" for byte in value.encode("utf-8")]
107 tokenizer = Plamo3Tokenizer(list(dict.fromkeys(["<eos>", value, *pieces])))
108 backend = XgrammarBackend(
109 make_vllm_config(),
110 tokenizer=tokenizer,
111 vocab_size=len(tokenizer.get_vocab()),
112 )
113 for tokens in ([value], pieces):
114 grammar = backend.compile_grammar(StructuredOutputOptions.REGEX, value)
115 assert grammar.accept_tokens(
116 "plamo3", [tokenizer.get_vocab()[token] for token in tokens]
117 )