main
py 2,405 lines 73.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5 from typing import Any
6
7 import llguidance
8 import pytest
9 from mistral_common.exceptions import InvalidMessageStructureException
10 from mistral_common.guidance.grammar_factory import GrammarFactory
11 from mistral_common.tokens.tokenizers.base import SpecialTokenPolicy, SpecialTokens
12
13 from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
14 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
15 from vllm.exceptions import VLLMValidationError
16 from vllm.tokenizers.mistral import (
17 MistralTokenizer,
18 _validate_apply_chat_template_args,
19 validate_request_params,
20 )
21
22
23 def test_validate_apply_chat_template_args():
24 # add_generation_prompt with assistant last message → error
25 messages: list[ChatCompletionMessageParam] = [
26 {"role": "assistant", "content": "Hello"}
27 ]
28 with pytest.raises(ValueError):
29 _validate_apply_chat_template_args(messages, add_generation_prompt=True)
30
31 # add_generation_prompt with user last message → ok
32 messages = [{"role": "user", "content": "Hello"}]
33 _validate_apply_chat_template_args(messages, add_generation_prompt=True)
34
35 # both add_generation_prompt and continue_final_message → error
36 with pytest.raises(ValueError):
37 _validate_apply_chat_template_args(
38 messages,
39 add_generation_prompt=True,
40 continue_final_message=True,
41 )
42
43 # continue_final_message with assistant last message → ok
44 messages = [{"role": "assistant", "content": "Hello"}]
45 _validate_apply_chat_template_args(messages, continue_final_message=True)
46
47 # continue_final_message with user last message → error
48 messages = [{"role": "user", "content": "Hello"}]
49 with pytest.raises(ValueError):
50 _validate_apply_chat_template_args(messages, continue_final_message=True)
51
52
53 @pytest.fixture(scope="module")
54 def mistral_tokenizer(request) -> MistralTokenizer:
55 return MistralTokenizer.from_pretrained(request.param)
56
57
58 @pytest.mark.parametrize(
59 "mistral_tokenizer",
60 ["mistralai/Mistral-7B-Instruct-v0.3", "mistralai/Magistral-Small-2509"],
61 indirect=True,
62 )
63 class TestMistralTokenizer:
64 def test_all_special_tokens(self, mistral_tokenizer: MistralTokenizer):
65 if mistral_tokenizer.is_tekken:
66 assert mistral_tokenizer.all_special_tokens == [
67 "<unk>",
68 "<s>",
69 "</s>",
70 "[INST]",
71 "[/INST]",
72 "[AVAILABLE_TOOLS]",
73 "[/AVAILABLE_TOOLS]",
74 "[TOOL_RESULTS]",
75 "[/TOOL_RESULTS]",
76 "[TOOL_CALLS]",
77 "[IMG]",
78 "<pad>",
79 "[IMG_BREAK]",
80 "[IMG_END]",
81 "[PREFIX]",
82 "[MIDDLE]",
83 "[SUFFIX]",
84 "[SYSTEM_PROMPT]",
85 "[/SYSTEM_PROMPT]",
86 "[TOOL_CONTENT]",
87 ] + [f"<SPECIAL_{i}>" for i in range(20, 32)] + [
88 "[ARGS]",
89 "[CALL_ID]",
90 "[THINK]",
91 "[/THINK]",
92 ] + [f"<SPECIAL_{i}>" for i in range(36, 1000)]
93 else:
94 assert mistral_tokenizer.all_special_tokens == [
95 "<s>",
96 "</s>",
97 "[INST]",
98 "[/INST]",
99 "[TOOL_CALLS]",
100 "[AVAILABLE_TOOLS]",
101 "[/AVAILABLE_TOOLS]",
102 "[TOOL_RESULTS]",
103 "[/TOOL_RESULTS]",
104 ] + [f"[control_{i}]" for i in range(8, 769)]
105
106 def get_vocab(self, mistral_tokenizer: MistralTokenizer):
107 assert (
108 mistral_tokenizer.get_vocab()
109 == mistral_tokenizer.transformers_tokenizer.get_vocab()
110 )
111
112 def test_get_added_vocab(self, mistral_tokenizer: MistralTokenizer):
113 assert mistral_tokenizer.get_added_vocab() == {}
114
115 def test_encode(self, mistral_tokenizer: MistralTokenizer):
116 token_ids = (
117 [1, 22177, 4304, 2662]
118 if mistral_tokenizer.is_tekken
119 else [1, 23325, 2294, 1686]
120 )
121
122 assert mistral_tokenizer.encode("Hello world !") == token_ids
123 assert mistral_tokenizer.encode("Hello world !", max_length=3) == token_ids[:-1]
124 assert (
125 mistral_tokenizer.encode("Hello world !", truncation=True, max_length=3)
126 == token_ids[:-1]
127 )
128 assert (
129 mistral_tokenizer.encode("Hello world !", truncation=False, max_length=3)
130 == token_ids
131 )
132
133 assert (
134 mistral_tokenizer.encode("Hello world !", add_special_tokens=True)
135 == token_ids
136 )
137 assert (
138 mistral_tokenizer.encode(
139 "Hello world !", add_special_tokens=True, max_length=3
140 )
141 == token_ids[:-1]
142 )
143 assert (
144 mistral_tokenizer.encode(
145 "Hello world !", add_special_tokens=True, truncation=False, max_length=3
146 )
147 == token_ids
148 )
149 assert (
150 mistral_tokenizer.encode("Hello world !", add_special_tokens=False)
151 == token_ids[1:]
152 )
153 assert mistral_tokenizer.encode("", add_special_tokens=False) == []
154
155 def test_call(self, mistral_tokenizer: MistralTokenizer):
156 token_ids = (
157 [1, 22177, 4304, 2662]
158 if mistral_tokenizer.is_tekken
159 else [1, 23325, 2294, 1686]
160 )
161 attn_mask = [1 for _ in range(len(token_ids))]
162
163 # Test 1: no special tokens
164 assert mistral_tokenizer("Hello world !", add_special_tokens=False) == {
165 "attention_mask": attn_mask[1:],
166 "input_ids": token_ids[1:],
167 }
168 # Test 2: special tokens
169 assert mistral_tokenizer("Hello world !", add_special_tokens=True) == {
170 "attention_mask": attn_mask,
171 "input_ids": token_ids,
172 }
173 # Test 3: special tokens + truncation
174 assert mistral_tokenizer(
175 "Hello world !", add_special_tokens=True, truncation=True, max_length=3
176 ) == {
177 "attention_mask": attn_mask[:-1],
178 "input_ids": token_ids[:-1],
179 }
180 # Test 4: special tokens + no truncation + max length
181 assert mistral_tokenizer(
182 "Hello world !", add_special_tokens=True, max_length=3
183 ) == {
184 "attention_mask": attn_mask,
185 "input_ids": token_ids,
186 }
187 # Test 5: empty string
188 assert mistral_tokenizer("", add_special_tokens=False) == {
189 "attention_mask": [],
190 "input_ids": [],
191 }
192
193 with pytest.raises(
194 ValueError,
195 match=(r"`text_pair` is not supported by `MistralTokenizer.__call__`."),
196 ):
197 mistral_tokenizer("Hello world !", "invalid pair")
198
199 @pytest.mark.parametrize(
200 "openai_request,add_generation_prompt,continue_final_message,expected_output,decoded_expected_output",
201 [
202 (
203 {
204 "messages": [
205 {
206 "role": "user",
207 "content": "Hello world !",
208 }
209 ],
210 },
211 True,
212 False,
213 ([1, 3, 23325, 2294, 1686, 4], [1, 3, 22177, 4304, 2662, 4]),
214 ("<s>[INST]▁Hello▁world▁![/INST]", ("<s>[INST]Hello world ![/INST]")),
215 ),
216 (
217 {
218 "messages": [
219 {
220 "role": "system",
221 "content": "I am an AI",
222 },
223 {
224 "role": "user",
225 "content": "Hello world !",
226 },
227 ],
228 },
229 True,
230 False,
231 (
232 [1, 3, 1083, 1605, 1164, 16875, 781, 781, 16998, 2294, 1686, 4],
233 [1, 17, 1073, 1855, 1420, 26554, 18, 3, 22177, 4304, 2662, 4],
234 ),
235 (
236 "<s>[INST]▁I▁am▁an▁AI<0x0A><0x0A>Hello▁world▁![/INST]",
237 (
238 "<s>[SYSTEM_PROMPT]I am an AI[/SYSTEM_PROMPT][INST]Hello world ![/INST]" # noqa: E501
239 ),
240 ),
241 ),
242 (
243 {
244 "messages": [
245 {
246 "role": "system",
247 "content": "I am an AI",
248 },
249 {
250 "role": "user",
251 "content": "Hello world !",
252 },
253 ],
254 "tools": [
255 {
256 "type": "function",
257 "function": {
258 "name": "get_weather",
259 "description": "Gets the current weather in a city.",
260 "parameters": {
261 "type": "object",
262 "properties": {
263 "city": {
264 "type": "string",
265 "description": "The city name",
266 }
267 },
268 "required": ["city"],
269 },
270 },
271 }
272 ],
273 },
274 True,
275 False,
276 (
277 [
278 1,
279 6,
280 1501,
281 7567,
282 1891,
283 2032,
284 1113,
285 3396,
286 1316,
287 1113,
288 3396,
289 2032,
290 10598,
291 1629,
292 2032,
293 1113,
294 1295,
295 29498,
296 1537,
297 1991,
298 1316,
299 1113,
300 7286,
301 2032,
302 1113,
303 2226,
304 29481,
305 1040,
306 2636,
307 8854,
308 1065,
309 1032,
310 3758,
311 9959,
312 1113,
313 12206,
314 2032,
315 10598,
316 1891,
317 2032,
318 1113,
319 3582,
320 1316,
321 1113,
322 11491,
323 2032,
324 10598,
325 19141,
326 2032,
327 10598,
328 1891,
329 2032,
330 1113,
331 2195,
332 1316,
333 1113,
334 7286,
335 2032,
336 1113,
337 1782,
338 3758,
339 1909,
340 29507,
341 11549,
342 1113,
343 11661,
344 2032,
345 8135,
346 19141,
347 3010,
348 1743,
349 10925,
350 7,
351 3,
352 1083,
353 1605,
354 1164,
355 16875,
356 781,
357 781,
358 16998,
359 2294,
360 1686,
361 4,
362 ],
363 [
364 1,
365 17,
366 1073,
367 1855,
368 1420,
369 26554,
370 18,
371 5,
372 1091,
373 19227,
374 4994,
375 2811,
376 1429,
377 5165,
378 1897,
379 1429,
380 5165,
381 2811,
382 16753,
383 2391,
384 2811,
385 1429,
386 1689,
387 1095,
388 45629,
389 1897,
390 1429,
391 14653,
392 2811,
393 1429,
394 1071,
395 3083,
396 1278,
397 3519,
398 17253,
399 1294,
400 1261,
401 5970,
402 39249,
403 1429,
404 26204,
405 2811,
406 16753,
407 4994,
408 2811,
409 1429,
410 6371,
411 1897,
412 1429,
413 48649,
414 2811,
415 16753,
416 29363,
417 2811,
418 16753,
419 4994,
420 2811,
421 1429,
422 3607,
423 1897,
424 1429,
425 14653,
426 2811,
427 1429,
428 1784,
429 5970,
430 2564,
431 1034,
432 47579,
433 1429,
434 15760,
435 2811,
436 12161,
437 29363,
438 4964,
439 2821,
440 27028,
441 6,
442 3,
443 22177,
444 4304,
445 2662,
446 4,
447 ],
448 ),
449 (
450 '<s>[AVAILABLE_TOOLS]▁[{"type":▁"function",▁"function":▁{"name":▁"get_weather",▁"description":▁"Gets▁the▁current▁weather▁in▁a▁city.",▁"parameters":▁{"type":▁"object",▁"properties":▁{"city":▁{"type":▁"string",▁"description":▁"The▁city▁name"}},▁"required":▁["city"]}}}][/AVAILABLE_TOOLS][INST]▁I▁am▁an▁AI<0x0A><0x0A>Hello▁world▁![/INST]',
451 (
452 '<s>[SYSTEM_PROMPT]I am an AI[/SYSTEM_PROMPT][AVAILABLE_TOOLS][{"type": "function", "function": {"name": "get_weather", "description": "Gets the current weather in a city.", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "The city name"}}, "required": ["city"]}}}][/AVAILABLE_TOOLS][INST]Hello world ![/INST]' # noqa: E501
453 ),
454 ),
455 ),
456 (
457 {
458 "messages": [
459 {
460 "role": "system",
461 "content": "I am an AI",
462 },
463 {
464 "role": "user",
465 "content": "Hello world !",
466 },
467 {
468 "role": "assistant",
469 "content": "",
470 "tool_calls": [
471 {
472 "id": "123456789",
473 "type": "function",
474 "function": {
475 "name": "get_weather",
476 "arguments": '{"city": "Paris"}',
477 },
478 }
479 ],
480 },
481 {
482 "role": "tool",
483 "tool_call_id": "123456789",
484 "content": '{"temperature": 20, "unit": "celsius"}',
485 },
486 ],
487 "tools": [
488 {
489 "type": "function",
490 "function": {
491 "name": "get_weather",
492 "description": "Gets the current weather in a city.",
493 "parameters": {
494 "type": "object",
495 "properties": {
496 "city": {
497 "type": "string",
498 "description": "The city name",
499 }
500 },
501 "required": ["city"],
502 },
503 },
504 }
505 ],
506 },
507 True,
508 False,
509 (
510 [
511 1,
512 6,
513 1501,
514 7567,
515 1891,
516 2032,
517 1113,
518 3396,
519 1316,
520 1113,
521 3396,
522 2032,
523 10598,
524 1629,
525 2032,
526 1113,
527 1295,
528 29498,
529 1537,
530 1991,
531 1316,
532 1113,
533 7286,
534 2032,
535 1113,
536 2226,
537 29481,
538 1040,
539 2636,
540 8854,
541 1065,
542 1032,
543 3758,
544 9959,
545 1113,
546 12206,
547 2032,
548 10598,
549 1891,
550 2032,
551 1113,
552 3582,
553 1316,
554 1113,
555 11491,
556 2032,
557 10598,
558 19141,
559 2032,
560 10598,
561 1891,
562 2032,
563 1113,
564 2195,
565 1316,
566 1113,
567 7286,
568 2032,
569 1113,
570 1782,
571 3758,
572 1909,
573 29507,
574 11549,
575 1113,
576 11661,
577 2032,
578 8135,
579 19141,
580 3010,
581 1743,
582 10925,
583 7,
584 3,
585 1083,
586 1605,
587 1164,
588 16875,
589 781,
590 781,
591 16998,
592 2294,
593 1686,
594 4,
595 5,
596 1501,
597 7567,
598 1629,
599 2032,
600 1113,
601 1295,
602 29498,
603 1537,
604 1991,
605 1316,
606 1113,
607 17452,
608 2032,
609 10598,
610 19141,
611 2032,
612 1113,
613 4684,
614 1046,
615 8474,
616 1113,
617 1081,
618 2032,
619 1113,
620 29508,
621 29518,
622 29538,
623 29549,
624 29550,
625 29552,
626 29555,
627 29551,
628 29542,
629 29507,
630 10925,
631 2,
632 8,
633 10598,
634 4557,
635 2032,
636 10598,
637 29475,
638 17329,
639 2032,
640 29473,
641 29518,
642 29502,
643 29493,
644 1113,
645 6074,
646 2032,
647 1113,
648 29485,
649 1958,
650 3938,
651 8474,
652 1113,
653 3613,
654 29498,
655 1081,
656 2032,
657 1113,
658 29508,
659 29518,
660 29538,
661 29549,
662 29550,
663 29552,
664 29555,
665 29551,
666 29542,
667 18163,
668 9,
669 ],
670 [
671 1,
672 17,
673 1073,
674 1855,
675 1420,
676 26554,
677 18,
678 5,
679 1091,
680 19227,
681 4994,
682 2811,
683 1429,
684 5165,
685 1897,
686 1429,
687 5165,
688 2811,
689 16753,
690 2391,
691 2811,
692 1429,
693 1689,
694 1095,
695 45629,
696 1897,
697 1429,
698 14653,
699 2811,
700 1429,
701 1071,
702 3083,
703 1278,
704 3519,
705 17253,
706 1294,
707 1261,
708 5970,
709 39249,
710 1429,
711 26204,
712 2811,
713 16753,
714 4994,
715 2811,
716 1429,
717 6371,
718 1897,
719 1429,
720 48649,
721 2811,
722 16753,
723 29363,
724 2811,
725 16753,
726 4994,
727 2811,
728 1429,
729 3607,
730 1897,
731 1429,
732 14653,
733 2811,
734 1429,
735 1784,
736 5970,
737 2564,
738 1034,
739 47579,
740 1429,
741 15760,
742 2811,
743 12161,
744 29363,
745 4964,
746 2821,
747 27028,
748 6,
749 3,
750 22177,
751 4304,
752 2662,
753 4,
754 9,
755 1689,
756 1095,
757 45629,
758 32,
759 19227,
760 29363,
761 2811,
762 1429,
763 42572,
764 46005,
765 2,
766 7,
767 19227,
768 113824,
769 2811,
770 1032,
771 1050,
772 1048,
773 1044,
774 1429,
775 8979,
776 2811,
777 1429,
778 1099,
779 79092,
780 46005,
781 8,
782 ],
783 ),
784 (
785 '<s>[AVAILABLE_TOOLS]▁[{"type":▁"function",▁"function":▁{"name":▁"get_weather",▁"description":▁"Gets▁the▁current▁weather▁in▁a▁city.",▁"parameters":▁{"type":▁"object",▁"properties":▁{"city":▁{"type":▁"string",▁"description":▁"The▁city▁name"}},▁"required":▁["city"]}}}][/AVAILABLE_TOOLS][INST]▁I▁am▁an▁AI<0x0A><0x0A>Hello▁world▁![/INST][TOOL_CALLS]▁[{"name":▁"get_weather",▁"arguments":▁{"city":▁"Paris"},▁"id":▁"123456789"}]</s>[TOOL_RESULTS]▁{"content":▁{"temperature":▁20,▁"unit":▁"celsius"},▁"call_id":▁"123456789"}[/TOOL_RESULTS]',
786 (
787 '<s>[SYSTEM_PROMPT]I am an AI[/SYSTEM_PROMPT][AVAILABLE_TOOLS][{"type": "function", "function": {"name": "get_weather", "description": "Gets the current weather in a city.", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "The city name"}}, "required": ["city"]}}}][/AVAILABLE_TOOLS][INST]Hello world ![/INST][TOOL_CALLS]get_weather[ARGS]{"city": "Paris"}</s>[TOOL_RESULTS]{"temperature": 20, "unit": "celsius"}[/TOOL_RESULTS]' # noqa: E501
788 ),
789 ),
790 ),
791 (
792 {
793 "messages": [
794 {
795 "role": "user",
796 "content": "Hello world !",
797 },
798 {
799 "role": "assistant",
800 "content": "Hello ",
801 },
802 ],
803 },
804 False,
805 True,
806 (
807 [1, 3, 23325, 2294, 1686, 4, 23325],
808 [1, 3, 22177, 4304, 2662, 4, 22177],
809 ),
810 (
811 "<s>[INST]▁Hello▁world▁![/INST]▁Hello",
812 "<s>[INST]Hello world ![/INST]Hello",
813 ),
814 ),
815 ],
816 )
817 def test_apply_chat_template(
818 self,
819 mistral_tokenizer: MistralTokenizer,
820 openai_request: dict[str, Any],
821 add_generation_prompt: bool,
822 continue_final_message: bool,
823 expected_output: tuple[list[int], list[int]],
824 decoded_expected_output: tuple[str, str],
825 ):
826 actual_output = mistral_tokenizer.apply_chat_template(
827 openai_request["messages"],
828 tools=openai_request.get("tools", []),
829 add_generation_prompt=add_generation_prompt,
830 continue_final_message=continue_final_message,
831 )
832 decoded_actual_output = mistral_tokenizer.tokenizer.decode(
833 actual_output, SpecialTokenPolicy.KEEP
834 )
835
836 assert actual_output == expected_output[mistral_tokenizer.is_tekken]
837 assert (
838 decoded_actual_output
839 == decoded_expected_output[mistral_tokenizer.is_tekken]
840 )
841
842 def test_apply_chat_template_error(self, mistral_tokenizer: MistralTokenizer):
843 messages: list[ChatCompletionMessageParam] = [
844 {"role": "user", "content": "Hello world !"}
845 ]
846
847 with pytest.raises(ValueError):
848 mistral_tokenizer.apply_chat_template(
849 messages,
850 tools=[],
851 add_generation_prompt=True,
852 continue_final_message=True,
853 )
854
855 with pytest.raises(ValueError):
856 mistral_tokenizer.apply_chat_template(
857 messages,
858 tools=[],
859 add_generation_prompt=False,
860 continue_final_message=True,
861 )
862
863 messages = [
864 {"role": "user", "content": "Hello world !"},
865 {"role": "assistant", "content": "Hello "},
866 ]
867 with pytest.raises(ValueError):
868 mistral_tokenizer.apply_chat_template(
869 messages,
870 tools=[],
871 add_generation_prompt=True,
872 continue_final_message=False,
873 )
874
875 messages = [
876 {"role": "user", "content": "Hello world !"},
877 {"role": "assistant", "content": "Hello "},
878 ]
879 with pytest.raises(InvalidMessageStructureException):
880 mistral_tokenizer.apply_chat_template(
881 messages,
882 tools=[],
883 add_generation_prompt=False,
884 continue_final_message=False,
885 )
886
887 @pytest.mark.parametrize(
888 "skip_special_tokens,expected_tokens",
889 (
890 (
891 False,
892 (
893 "<s>[INST]▁Hello▁world▁![/INST]▁Hello</s>",
894 "<s>[INST]Hello world ![/INST]Hello</s>",
895 ),
896 ),
897 (True, ("Hello world ! Hello", "Hello world !Hello")),
898 ),
899 )
900 def test_decode(
901 self,
902 mistral_tokenizer: MistralTokenizer,
903 skip_special_tokens: bool,
904 expected_tokens: tuple[str, str],
905 ):
906 ids = (
907 [1, 3, 23325, 2294, 1686, 4, 23325, 2],
908 [1, 3, 22177, 4304, 2662, 4, 22177, 2],
909 )
910 assert (
911 mistral_tokenizer.decode(
912 ids[mistral_tokenizer.is_tekken],
913 skip_special_tokens=skip_special_tokens,
914 )
915 == expected_tokens[mistral_tokenizer.is_tekken]
916 )
917
918 def test_decode_empty(
919 self,
920 mistral_tokenizer: MistralTokenizer,
921 ):
922 assert (
923 mistral_tokenizer.decode(
924 [],
925 )
926 == ""
927 )
928
929 def test_decode_int(
930 self,
931 mistral_tokenizer: MistralTokenizer,
932 ):
933 ids = 1
934 assert (
935 mistral_tokenizer.decode(
936 ids,
937 skip_special_tokens=False,
938 )
939 == "<s>"
940 )
941
942 @pytest.mark.parametrize(
943 "skip_special_tokens,expected_tokens",
944 (
945 (
946 False,
947 (
948 ["<s>[INST]▁Hello▁world▁![/INST]▁Hello</s>"],
949 ["<s>[INST]Hello world ![/INST]Hello</s>"],
950 ),
951 ),
952 (True, (["Hello world ! Hello"], ["Hello world !Hello"])),
953 ),
954 )
955 def test_batch_decode(
956 self,
957 mistral_tokenizer: MistralTokenizer,
958 skip_special_tokens: bool,
959 expected_tokens: tuple[str, str],
960 ):
961 ids = (
962 [[1, 3, 23325, 2294, 1686, 4, 23325, 2]],
963 [[1, 3, 22177, 4304, 2662, 4, 22177, 2]],
964 )
965 assert (
966 mistral_tokenizer.batch_decode(
967 ids[mistral_tokenizer.is_tekken],
968 skip_special_tokens=skip_special_tokens,
969 )
970 == expected_tokens[mistral_tokenizer.is_tekken]
971 )
972
973 def test_batch_decode_empty(
974 self,
975 mistral_tokenizer: MistralTokenizer,
976 ):
977 assert mistral_tokenizer.batch_decode(
978 [[]],
979 ) == [""]
980
981 def test_convert_tokens_to_string(self, mistral_tokenizer: MistralTokenizer):
982 tokens = (
983 [
984 "<s>",
985 "[AVAILABLE_TOOLS]",
986 "▁[",
987 '{"',
988 "type",
989 '":',
990 '▁"',
991 "function",
992 '",',
993 '▁"',
994 "function",
995 '":',
996 '▁{"',
997 "name",
998 '":',
999 '▁"',
1000 "get",
1001 "_",
1002 "we",
1003 "ather",
1004 '",',
1005 '▁"',
1006 "description",
1007 '":',
1008 '▁"',
1009 "Get",
1010 "s",
1011 "▁the",
1012 "▁current",
1013 "▁weather",
1014 "▁in",
1015 "▁a",
1016 "▁city",
1017 '.",',
1018 '▁"',
1019 "parameters",
1020 '":',
1021 '▁{"',
1022 "type",
1023 '":',
1024 '▁"',
1025 "object",
1026 '",',
1027 '▁"',
1028 "properties",
1029 '":',
1030 '▁{"',
1031 "city",
1032 '":',
1033 '▁{"',
1034 "type",
1035 '":',
1036 '▁"',
1037 "string",
1038 '",',
1039 '▁"',
1040 "description",
1041 '":',
1042 '▁"',
1043 "The",
1044 "▁city",
1045 "▁name",
1046 '"',
1047 "}},",
1048 '▁"',
1049 "required",
1050 '":',
1051 '▁["',
1052 "city",
1053 '"]',
1054 "}}",
1055 "}]",
1056 "[/AVAILABLE_TOOLS]",
1057 "[INST]",
1058 "▁I",
1059 "▁am",
1060 "▁an",
1061 "▁AI",
1062 "<0x0A>",
1063 "<0x0A>",
1064 "Hello",
1065 "▁world",
1066 "▁!",
1067 "[/INST]",
1068 "[TOOL_CALLS]",
1069 "▁[",
1070 '{"',
1071 "name",
1072 '":',
1073 '▁"',
1074 "get",
1075 "_",
1076 "we",
1077 "ather",
1078 '",',
1079 '▁"',
1080 "arguments",
1081 '":',
1082 '▁{"',
1083 "city",
1084 '":',
1085 '▁"',
1086 "Par",
1087 "is",
1088 '"},',
1089 '▁"',
1090 "id",
1091 '":',
1092 '▁"',
1093 "1",
1094 "2",
1095 "3",
1096 "4",
1097 "5",
1098 "6",
1099 "7",
1100 "8",
1101 "9",
1102 '"',
1103 "}]",
1104 "</s>",
1105 "[TOOL_RESULTS]",
1106 '▁{"',
1107 "content",
1108 '":',
1109 '▁{"',
1110 "t",
1111 "emperature",
1112 '":',
1113 "▁",
1114 "2",
1115 "0",
1116 ",",
1117 '▁"',
1118 "unit",
1119 '":',
1120 '▁"',
1121 "c",
1122 "els",
1123 "ius",
1124 '"},',
1125 '▁"',
1126 "call",
1127 "_",
1128 "id",
1129 '":',
1130 '▁"',
1131 "1",
1132 "2",
1133 "3",
1134 "4",
1135 "5",
1136 "6",
1137 "7",
1138 "8",
1139 "9",
1140 '"}',
1141 "[/TOOL_RESULTS]",
1142 ],
1143 [
1144 "<s>",
1145 "[SYSTEM_PROMPT]",
1146 "I",
1147 " am",
1148 " an",
1149 " AI",
1150 "[/SYSTEM_PROMPT]",
1151 "[AVAILABLE_TOOLS]",
1152 "[",
1153 '{"',
1154 "type",
1155 '":',
1156 ' "',
1157 "function",
1158 '",',
1159 ' "',
1160 "function",
1161 '":',
1162 ' {"',
1163 "name",
1164 '":',
1165 ' "',
1166 "get",
1167 "_",
1168 "weather",
1169 '",',
1170 ' "',
1171 "description",
1172 '":',
1173 ' "',
1174 "G",
1175 "ets",
1176 " the",
1177 " current",
1178 " weather",
1179 " in",
1180 " a",
1181 " city",
1182 '.",',
1183 ' "',
1184 "parameters",
1185 '":',
1186 ' {"',
1187 "type",
1188 '":',
1189 ' "',
1190 "object",
1191 '",',
1192 ' "',
1193 "properties",
1194 '":',
1195 ' {"',
1196 "city",
1197 '":',
1198 ' {"',
1199 "type",
1200 '":',
1201 ' "',
1202 "string",
1203 '",',
1204 ' "',
1205 "description",
1206 '":',
1207 ' "',
1208 "The",
1209 " city",
1210 " name",
1211 '"',
1212 "}},",
1213 ' "',
1214 "required",
1215 '":',
1216 ' ["',
1217 "city",
1218 '"]',
1219 "}}",
1220 "}]",
1221 "[/AVAILABLE_TOOLS]",
1222 "[INST]",
1223 "Hello",
1224 " world",
1225 " !",
1226 "[/INST]",
1227 "[TOOL_CALLS]",
1228 "get",
1229 "_",
1230 "weather",
1231 "[ARGS]",
1232 '{"',
1233 "city",
1234 '":',
1235 ' "',
1236 "Paris",
1237 '"}',
1238 "</s>",
1239 "[TOOL_RESULTS]",
1240 '{"',
1241 "temperature",
1242 '":',
1243 " ",
1244 "2",
1245 "0",
1246 ",",
1247 ' "',
1248 "unit",
1249 '":',
1250 ' "',
1251 "c",
1252 "elsius",
1253 '"}',
1254 "[/TOOL_RESULTS]",
1255 ],
1256 )
1257
1258 expected_strings = (
1259 '[{"type": "function", "function": {"name": "get_weather", "description": "Gets the current weather in a city.", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "The city name"}}, "required": ["city"]}}}] I am an AI\n\nHello world ![TOOL_CALLS][{"name": "get_weather", "arguments": {"city": "Paris"}, "id": "123456789"}] {"content": {"temperature": 20, "unit": "celsius"}, "call_id": "123456789"}', # noqa: E501
1260 # v11+ tool-call decode emits the explicit [ARGS] separator between
1261 # the function name and its JSON arguments (get_weather[ARGS]{...}).
1262 'I am an AI[{"type": "function", "function": {"name": "get_weather", "description": "Gets the current weather in a city.", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "The city name"}}, "required": ["city"]}}}]Hello world ![TOOL_CALLS]get_weather[ARGS]{"city": "Paris"}{"temperature": 20, "unit": "celsius"}', # noqa: E501
1263 )
1264
1265 assert (
1266 mistral_tokenizer.convert_tokens_to_string(
1267 tokens[mistral_tokenizer.is_tekken]
1268 )
1269 == expected_strings[mistral_tokenizer.is_tekken]
1270 )
1271
1272 assert mistral_tokenizer.convert_tokens_to_string([]) == ""
1273
1274 @pytest.mark.parametrize(
1275 "skip_special_tokens,tuple_expected_tokens",
1276 (
1277 (
1278 True,
1279 (
1280 [
1281 "▁[",
1282 '{"',
1283 "type",
1284 '":',
1285 '▁"',
1286 "function",
1287 '",',
1288 '▁"',
1289 "function",
1290 '":',
1291 '▁{"',
1292 "name",
1293 '":',
1294 '▁"',
1295 "get",
1296 "_",
1297 "we",
1298 "ather",
1299 '",',
1300 '▁"',
1301 "description",
1302 '":',
1303 '▁"',
1304 "Get",
1305 "s",
1306 "▁the",
1307 "▁current",
1308 "▁weather",
1309 "▁in",
1310 "▁a",
1311 "▁city",
1312 '.",',
1313 '▁"',
1314 "parameters",
1315 '":',
1316 '▁{"',
1317 "type",
1318 '":',
1319 '▁"',
1320 "object",
1321 '",',
1322 '▁"',
1323 "properties",
1324 '":',
1325 '▁{"',
1326 "city",
1327 '":',
1328 '▁{"',
1329 "type",
1330 '":',
1331 '▁"',
1332 "string",
1333 '",',
1334 '▁"',
1335 "description",
1336 '":',
1337 '▁"',
1338 "The",
1339 "▁city",
1340 "▁name",
1341 '"',
1342 "}},",
1343 '▁"',
1344 "required",
1345 '":',
1346 '▁["',
1347 "city",
1348 '"]',
1349 "}}",
1350 "}]",
1351 "▁I",
1352 "▁am",
1353 "▁an",
1354 "▁AI",
1355 "<0x0A>",
1356 "<0x0A>",
1357 "Hello",
1358 "▁world",
1359 "▁!",
1360 "[TOOL_CALLS]",
1361 "▁[",
1362 '{"',
1363 "name",
1364 '":',
1365 '▁"',
1366 "get",
1367 "_",
1368 "we",
1369 "ather",
1370 '",',
1371 '▁"',
1372 "arguments",
1373 '":',
1374 '▁{"',
1375 "city",
1376 '":',
1377 '▁"',
1378 "Par",
1379 "is",
1380 '"},',
1381 '▁"',
1382 "id",
1383 '":',
1384 '▁"',
1385 "1",
1386 "2",
1387 "3",
1388 "4",
1389 "5",
1390 "6",
1391 "7",
1392 "8",
1393 "9",
1394 '"',
1395 "}]",
1396 '▁{"',
1397 "content",
1398 '":',
1399 '▁{"',
1400 "t",
1401 "emperature",
1402 '":',
1403 "▁",
1404 "2",
1405 "0",
1406 ",",
1407 '▁"',
1408 "unit",
1409 '":',
1410 '▁"',
1411 "c",
1412 "els",
1413 "ius",
1414 '"},',
1415 '▁"',
1416 "call",
1417 "_",
1418 "id",
1419 '":',
1420 '▁"',
1421 "1",
1422 "2",
1423 "3",
1424 "4",
1425 "5",
1426 "6",
1427 "7",
1428 "8",
1429 "9",
1430 '"}',
1431 ],
1432 [
1433 "I",
1434 " am",
1435 " an",
1436 " AI",
1437 "[",
1438 '{"',
1439 "type",
1440 '":',
1441 ' "',
1442 "function",
1443 '",',
1444 ' "',
1445 "function",
1446 '":',
1447 ' {"',
1448 "name",
1449 '":',
1450 ' "',
1451 "get",
1452 "_",
1453 "weather",
1454 '",',
1455 ' "',
1456 "description",
1457 '":',
1458 ' "',
1459 "G",
1460 "ets",
1461 " the",
1462 " current",
1463 " weather",
1464 " in",
1465 " a",
1466 " city",
1467 '.",',
1468 ' "',
1469 "parameters",
1470 '":',
1471 ' {"',
1472 "type",
1473 '":',
1474 ' "',
1475 "object",
1476 '",',
1477 ' "',
1478 "properties",
1479 '":',
1480 ' {"',
1481 "city",
1482 '":',
1483 ' {"',
1484 "type",
1485 '":',
1486 ' "',
1487 "string",
1488 '",',
1489 ' "',
1490 "description",
1491 '":',
1492 ' "',
1493 "The",
1494 " city",
1495 " name",
1496 '"',
1497 "}},",
1498 ' "',
1499 "required",
1500 '":',
1501 ' ["',
1502 "city",
1503 '"]',
1504 "}}",
1505 "}]",
1506 "Hello",
1507 " world",
1508 " !",
1509 "[TOOL_CALLS]",
1510 "get",
1511 "_",
1512 "weather",
1513 "[ARGS]",
1514 '{"',
1515 "city",
1516 '":',
1517 ' "',
1518 "Paris",
1519 '"}',
1520 '{"',
1521 "temperature",
1522 '":',
1523 " ",
1524 "2",
1525 "0",
1526 ",",
1527 ' "',
1528 "unit",
1529 '":',
1530 ' "',
1531 "c",
1532 "elsius",
1533 '"}',
1534 ],
1535 ),
1536 ),
1537 (
1538 False,
1539 (
1540 [
1541 "<s>",
1542 "[AVAILABLE_TOOLS]",
1543 "▁[",
1544 '{"',
1545 "type",
1546 '":',
1547 '▁"',
1548 "function",
1549 '",',
1550 '▁"',
1551 "function",
1552 '":',
1553 '▁{"',
1554 "name",
1555 '":',
1556 '▁"',
1557 "get",
1558 "_",
1559 "we",
1560 "ather",
1561 '",',
1562 '▁"',
1563 "description",
1564 '":',
1565 '▁"',
1566 "Get",
1567 "s",
1568 "▁the",
1569 "▁current",
1570 "▁weather",
1571 "▁in",
1572 "▁a",
1573 "▁city",
1574 '.",',
1575 '▁"',
1576 "parameters",
1577 '":',
1578 '▁{"',
1579 "type",
1580 '":',
1581 '▁"',
1582 "object",
1583 '",',
1584 '▁"',
1585 "properties",
1586 '":',
1587 '▁{"',
1588 "city",
1589 '":',
1590 '▁{"',
1591 "type",
1592 '":',
1593 '▁"',
1594 "string",
1595 '",',
1596 '▁"',
1597 "description",
1598 '":',
1599 '▁"',
1600 "The",
1601 "▁city",
1602 "▁name",
1603 '"',
1604 "}},",
1605 '▁"',
1606 "required",
1607 '":',
1608 '▁["',
1609 "city",
1610 '"]',
1611 "}}",
1612 "}]",
1613 "[/AVAILABLE_TOOLS]",
1614 "[INST]",
1615 "▁I",
1616 "▁am",
1617 "▁an",
1618 "▁AI",
1619 "<0x0A>",
1620 "<0x0A>",
1621 "Hello",
1622 "▁world",
1623 "▁!",
1624 "[/INST]",
1625 "[TOOL_CALLS]",
1626 "▁[",
1627 '{"',
1628 "name",
1629 '":',
1630 '▁"',
1631 "get",
1632 "_",
1633 "we",
1634 "ather",
1635 '",',
1636 '▁"',
1637 "arguments",
1638 '":',
1639 '▁{"',
1640 "city",
1641 '":',
1642 '▁"',
1643 "Par",
1644 "is",
1645 '"},',
1646 '▁"',
1647 "id",
1648 '":',
1649 '▁"',
1650 "1",
1651 "2",
1652 "3",
1653 "4",
1654 "5",
1655 "6",
1656 "7",
1657 "8",
1658 "9",
1659 '"',
1660 "}]",
1661 "</s>",
1662 "[TOOL_RESULTS]",
1663 '▁{"',
1664 "content",
1665 '":',
1666 '▁{"',
1667 "t",
1668 "emperature",
1669 '":',
1670 "▁",
1671 "2",
1672 "0",
1673 ",",
1674 '▁"',
1675 "unit",
1676 '":',
1677 '▁"',
1678 "c",
1679 "els",
1680 "ius",
1681 '"},',
1682 '▁"',
1683 "call",
1684 "_",
1685 "id",
1686 '":',
1687 '▁"',
1688 "1",
1689 "2",
1690 "3",
1691 "4",
1692 "5",
1693 "6",
1694 "7",
1695 "8",
1696 "9",
1697 '"}',
1698 "[/TOOL_RESULTS]",
1699 ],
1700 [
1701 "<s>",
1702 "[SYSTEM_PROMPT]",
1703 "I",
1704 " am",
1705 " an",
1706 " AI",
1707 "[/SYSTEM_PROMPT]",
1708 "[AVAILABLE_TOOLS]",
1709 "[",
1710 '{"',
1711 "type",
1712 '":',
1713 ' "',
1714 "function",
1715 '",',
1716 ' "',
1717 "function",
1718 '":',
1719 ' {"',
1720 "name",
1721 '":',
1722 ' "',
1723 "get",
1724 "_",
1725 "weather",
1726 '",',
1727 ' "',
1728 "description",
1729 '":',
1730 ' "',
1731 "G",
1732 "ets",
1733 " the",
1734 " current",
1735 " weather",
1736 " in",
1737 " a",
1738 " city",
1739 '.",',
1740 ' "',
1741 "parameters",
1742 '":',
1743 ' {"',
1744 "type",
1745 '":',
1746 ' "',
1747 "object",
1748 '",',
1749 ' "',
1750 "properties",
1751 '":',
1752 ' {"',
1753 "city",
1754 '":',
1755 ' {"',
1756 "type",
1757 '":',
1758 ' "',
1759 "string",
1760 '",',
1761 ' "',
1762 "description",
1763 '":',
1764 ' "',
1765 "The",
1766 " city",
1767 " name",
1768 '"',
1769 "}},",
1770 ' "',
1771 "required",
1772 '":',
1773 ' ["',
1774 "city",
1775 '"]',
1776 "}}",
1777 "}]",
1778 "[/AVAILABLE_TOOLS]",
1779 "[INST]",
1780 "Hello",
1781 " world",
1782 " !",
1783 "[/INST]",
1784 "[TOOL_CALLS]",
1785 "get",
1786 "_",
1787 "weather",
1788 "[ARGS]",
1789 '{"',
1790 "city",
1791 '":',
1792 ' "',
1793 "Paris",
1794 '"}',
1795 "</s>",
1796 "[TOOL_RESULTS]",
1797 '{"',
1798 "temperature",
1799 '":',
1800 " ",
1801 "2",
1802 "0",
1803 ",",
1804 ' "',
1805 "unit",
1806 '":',
1807 ' "',
1808 "c",
1809 "elsius",
1810 '"}',
1811 "[/TOOL_RESULTS]",
1812 ],
1813 ),
1814 ),
1815 ),
1816 )
1817 def test_convert_ids_to_tokens(
1818 self,
1819 mistral_tokenizer: MistralTokenizer,
1820 skip_special_tokens: bool,
1821 tuple_expected_tokens: tuple[list[str], list[str]],
1822 ):
1823 tuple_ids = (
1824 [
1825 1,
1826 6,
1827 1501,
1828 7567,
1829 1891,
1830 2032,
1831 1113,
1832 3396,
1833 1316,
1834 1113,
1835 3396,
1836 2032,
1837 10598,
1838 1629,
1839 2032,
1840 1113,
1841 1295,
1842 29498,
1843 1537,
1844 1991,
1845 1316,
1846 1113,
1847 7286,
1848 2032,
1849 1113,
1850 2226,
1851 29481,
1852 1040,
1853 2636,
1854 8854,
1855 1065,
1856 1032,
1857 3758,
1858 9959,
1859 1113,
1860 12206,
1861 2032,
1862 10598,
1863 1891,
1864 2032,
1865 1113,
1866 3582,
1867 1316,
1868 1113,
1869 11491,
1870 2032,
1871 10598,
1872 19141,
1873 2032,
1874 10598,
1875 1891,
1876 2032,
1877 1113,
1878 2195,
1879 1316,
1880 1113,
1881 7286,
1882 2032,
1883 1113,
1884 1782,
1885 3758,
1886 1909,
1887 29507,
1888 11549,
1889 1113,
1890 11661,
1891 2032,
1892 8135,
1893 19141,
1894 3010,
1895 1743,
1896 10925,
1897 7,
1898 3,
1899 1083,
1900 1605,
1901 1164,
1902 16875,
1903 781,
1904 781,
1905 16998,
1906 2294,
1907 1686,
1908 4,
1909 5,
1910 1501,
1911 7567,
1912 1629,
1913 2032,
1914 1113,
1915 1295,
1916 29498,
1917 1537,
1918 1991,
1919 1316,
1920 1113,
1921 17452,
1922 2032,
1923 10598,
1924 19141,
1925 2032,
1926 1113,
1927 4684,
1928 1046,
1929 8474,
1930 1113,
1931 1081,
1932 2032,
1933 1113,
1934 29508,
1935 29518,
1936 29538,
1937 29549,
1938 29550,
1939 29552,
1940 29555,
1941 29551,
1942 29542,
1943 29507,
1944 10925,
1945 2,
1946 8,
1947 10598,
1948 4557,
1949 2032,
1950 10598,
1951 29475,
1952 17329,
1953 2032,
1954 29473,
1955 29518,
1956 29502,
1957 29493,
1958 1113,
1959 6074,
1960 2032,
1961 1113,
1962 29485,
1963 1958,
1964 3938,
1965 8474,
1966 1113,
1967 3613,
1968 29498,
1969 1081,
1970 2032,
1971 1113,
1972 29508,
1973 29518,
1974 29538,
1975 29549,
1976 29550,
1977 29552,
1978 29555,
1979 29551,
1980 29542,
1981 18163,
1982 9,
1983 ],
1984 [
1985 1,
1986 17,
1987 1073,
1988 1855,
1989 1420,
1990 26554,
1991 18,
1992 5,
1993 1091,
1994 19227,
1995 4994,
1996 2811,
1997 1429,
1998 5165,
1999 1897,
2000 1429,
2001 5165,
2002 2811,
2003 16753,
2004 2391,
2005 2811,
2006 1429,
2007 1689,
2008 1095,
2009 45629,
2010 1897,
2011 1429,
2012 14653,
2013 2811,
2014 1429,
2015 1071,
2016 3083,
2017 1278,
2018 3519,
2019 17253,
2020 1294,
2021 1261,
2022 5970,
2023 39249,
2024 1429,
2025 26204,
2026 2811,
2027 16753,
2028 4994,
2029 2811,
2030 1429,
2031 6371,
2032 1897,
2033 1429,
2034 48649,
2035 2811,
2036 16753,
2037 29363,
2038 2811,
2039 16753,
2040 4994,
2041 2811,
2042 1429,
2043 3607,
2044 1897,
2045 1429,
2046 14653,
2047 2811,
2048 1429,
2049 1784,
2050 5970,
2051 2564,
2052 1034,
2053 47579,
2054 1429,
2055 15760,
2056 2811,
2057 12161,
2058 29363,
2059 4964,
2060 2821,
2061 27028,
2062 6,
2063 3,
2064 22177,
2065 4304,
2066 2662,
2067 4,
2068 9,
2069 1689,
2070 1095,
2071 45629,
2072 32,
2073 19227,
2074 29363,
2075 2811,
2076 1429,
2077 42572,
2078 46005,
2079 2,
2080 7,
2081 19227,
2082 113824,
2083 2811,
2084 1032,
2085 1050,
2086 1048,
2087 1044,
2088 1429,
2089 8979,
2090 2811,
2091 1429,
2092 1099,
2093 79092,
2094 46005,
2095 8,
2096 ],
2097 )
2098
2099 ids = tuple_ids[mistral_tokenizer.is_tekken]
2100 expected_tokens = tuple_expected_tokens[mistral_tokenizer.is_tekken]
2101 actual_tokens = mistral_tokenizer.convert_ids_to_tokens(
2102 ids, skip_special_tokens=skip_special_tokens
2103 )
2104 assert actual_tokens == expected_tokens
2105
2106 assert mistral_tokenizer.convert_ids_to_tokens([]) == []
2107
2108 def test_grammar_factory(self, mistral_tokenizer: MistralTokenizer) -> None:
2109 # works in this case cause Mistral 7B is < v11 and SPM
2110 if not mistral_tokenizer.is_tekken:
2111 with pytest.raises(AttributeError):
2112 mistral_tokenizer.grammar_factory # noqa: B018
2113 return
2114 factory = mistral_tokenizer.grammar_factory
2115 assert isinstance(factory, GrammarFactory)
2116
2117 # Test caching
2118 factory_2 = mistral_tokenizer.grammar_factory
2119 assert factory is factory_2
2120
2121 def test_llg_tokenizer(self, mistral_tokenizer: MistralTokenizer) -> None:
2122 if not mistral_tokenizer.is_tekken:
2123 with pytest.raises(ValueError):
2124 mistral_tokenizer.llg_tokenizer # noqa: B018
2125 return
2126
2127 llg_tokenizer = mistral_tokenizer.llg_tokenizer
2128 assert isinstance(llg_tokenizer, llguidance.LLTokenizer)
2129
2130 # Test caching
2131 llg_tokenizer_2 = mistral_tokenizer.llg_tokenizer
2132 assert llg_tokenizer is llg_tokenizer_2
2133
2134 @pytest.mark.parametrize(
2135 "messages,tools,tekken_expected_substrings,spm_expected_substrings",
2136 [
2137 pytest.param(
2138 [{"role": "user", "content": "Hello"}],
2139 [{"type": "function", "function": {"name": "do_nothing"}}],
2140 ["do_nothing", '"description": ""', '"parameters": {}'],
2141 ["do_nothing", '"description":▁""', '"parameters":▁{}'],
2142 id="tool_without_description_and_parameters",
2143 ),
2144 pytest.param(
2145 [
2146 {"role": "user", "content": "Do nothing"},
2147 {
2148 "role": "assistant",
2149 "content": "",
2150 "tool_calls": [
2151 {
2152 "id": "123456789",
2153 "type": "function",
2154 "function": {
2155 "name": "do_nothing",
2156 "arguments": None,
2157 },
2158 }
2159 ],
2160 },
2161 {
2162 "role": "tool",
2163 "tool_call_id": "123456789",
2164 "content": "done",
2165 },
2166 ],
2167 [{"type": "function", "function": {"name": "do_nothing"}}],
2168 ["do_nothing"],
2169 ["do_nothing"],
2170 id="tool_call_with_none_arguments",
2171 ),
2172 ],
2173 )
2174 def test_apply_chat_template_tool_optional_fields(
2175 self,
2176 mistral_tokenizer: MistralTokenizer,
2177 messages: list[ChatCompletionMessageParam],
2178 tools: list[dict[str, Any]],
2179 tekken_expected_substrings: list[str],
2180 spm_expected_substrings: list[str],
2181 ) -> None:
2182 output = mistral_tokenizer.apply_chat_template(
2183 messages,
2184 tools=tools,
2185 add_generation_prompt=True,
2186 )
2187 decoded = mistral_tokenizer.tokenizer.decode(output, SpecialTokenPolicy.KEEP)
2188
2189 expected = (
2190 tekken_expected_substrings
2191 if mistral_tokenizer.is_tekken
2192 else spm_expected_substrings
2193 )
2194 for substring in expected:
2195 assert substring in decoded
2196
2197 def test_apply_chat_template_tools_not_mutated(
2198 self, mistral_tokenizer: MistralTokenizer
2199 ) -> None:
2200 messages: list[ChatCompletionMessageParam] = [
2201 {"role": "user", "content": "Hello"},
2202 ]
2203 tools: list[dict[str, Any]] = [
2204 {
2205 "type": "function",
2206 "function": {
2207 "name": "get_weather",
2208 "description": "Gets weather.",
2209 "parameters": {
2210 "type": "object",
2211 "properties": {
2212 "city": {"type": "string"},
2213 },
2214 },
2215 },
2216 },
2217 ]
2218 original_tools = copy.deepcopy(tools)
2219
2220 mistral_tokenizer.apply_chat_template(
2221 messages,
2222 tools=tools,
2223 add_generation_prompt=True,
2224 )
2225
2226 assert tools == original_tools
2227
2228 @pytest.mark.parametrize(
2229 "reasoning_key",
2230 ["reasoning", "reasoning_content"],
2231 )
2232 def test_apply_chat_template_reasoning_assistant(
2233 self, mistral_tokenizer: MistralTokenizer, reasoning_key: str
2234 ) -> None:
2235 if not mistral_tokenizer.is_tekken:
2236 pytest.skip("Reasoning tokens only supported on tekken tokenizers")
2237
2238 messages: list[dict[str, Any]] = [
2239 {"role": "user", "content": "What is 2+2?"},
2240 {
2241 "role": "assistant",
2242 "content": "4",
2243 reasoning_key: "2+2 equals 4",
2244 },
2245 {"role": "user", "content": "Are you sure?"},
2246 ]
2247
2248 output = mistral_tokenizer.apply_chat_template(
2249 messages,
2250 add_generation_prompt=True,
2251 )
2252 decoded = mistral_tokenizer.tokenizer.decode(output, SpecialTokenPolicy.KEEP)
2253
2254 assert "[THINK]2+2 equals 4[/THINK]" in decoded
2255
2256
2257 def test_convert_ids_to_tokens_pre_args_tekken():
2258 """convert_ids_to_tokens must not raise on pre-[ARGS] Tekken tokenizers.
2259
2260 Tekken tokenizers before v11 (e.g. Ministral-8B-Instruct-2410) have no
2261 [ARGS] special token. This guards the is_special([ARGS]) gate in
2262 MistralTokenizer.convert_ids_to_tokens, which must skip [ARGS] rather than
2263 call get_special_token(args) unconditionally (which raises on Tekken).
2264 """
2265 tokenizer = MistralTokenizer.from_pretrained("mistralai/Ministral-8B-Instruct-2410")
2266 assert tokenizer.is_tekken
2267 assert not tokenizer.tokenizer.is_special(SpecialTokens.args)
2268
2269 ids = tokenizer.encode("Hello world !", add_special_tokens=False)
2270 tokens = tokenizer.convert_ids_to_tokens(ids, skip_special_tokens=True)
2271 assert len(tokens) > 0
2272
2273
2274 # ---------------------------------------------------------------------------
2275 # validate_request_params – reasoning_effort validation (no tokenizer needed)
2276 # ---------------------------------------------------------------------------
2277
2278
2279 @pytest.mark.parametrize("reasoning_effort", [None, "none", "high"])
2280 def test_validate_request_params_valid_effort(reasoning_effort: str | None) -> None:
2281 request = ChatCompletionRequest(
2282 model="test-model",
2283 messages=[],
2284 reasoning_effort=reasoning_effort,
2285 )
2286 assert validate_request_params(request) is None
2287
2288
2289 def test_validate_request_params_rejects_unsupported_effort() -> None:
2290 request = ChatCompletionRequest(
2291 model="test-model",
2292 messages=[],
2293 reasoning_effort="medium",
2294 )
2295 with pytest.raises(VLLMValidationError, match="reasoning_effort") as exc_info:
2296 validate_request_params(request)
2297 assert exc_info.value.parameter == "reasoning_effort"
2298
2299
2300 def test_validate_request_params_rejects_chat_template() -> None:
2301 request = ChatCompletionRequest(
2302 model="test-model",
2303 messages=[],
2304 chat_template="{{ messages }}",
2305 )
2306 with pytest.raises(VLLMValidationError, match="chat_template") as exc_info:
2307 validate_request_params(request)
2308 assert exc_info.value.parameter == "chat_template"
2309
2310
2311 # ---------------------------------------------------------------------------
2312 # apply_chat_template reasoning_effort passthrough (v15 tokenizer)
2313 # ---------------------------------------------------------------------------
2314
2315 _PASSTHROUGH_MESSAGES = [{"role": "user", "content": "Hello"}]
2316
2317
2318 @pytest.fixture(scope="module")
2319 def v15_mistral_tokenizer() -> MistralTokenizer:
2320 """Load the v15 Mistral tokenizer; skip if unavailable."""
2321 try:
2322 return MistralTokenizer.from_pretrained("mistralai/Mistral-Small-4-119B-2603")
2323 except Exception:
2324 pytest.skip("v15 tokenizer unavailable")
2325 raise AssertionError("unreachable")
2326
2327
2328 @pytest.fixture(scope="module")
2329 def v13_mistral_tokenizer() -> MistralTokenizer:
2330 """Load the v13 Mistral tokenizer; skip if unavailable."""
2331 try:
2332 return MistralTokenizer.from_pretrained("mistralai/Magistral-Small-2509")
2333 except Exception:
2334 pytest.skip("v13 tokenizer unavailable")
2335 raise AssertionError("unreachable")
2336
2337
2338 def test_v15_apply_chat_template_passes_reasoning_effort_high(
2339 monkeypatch: pytest.MonkeyPatch,
2340 v15_mistral_tokenizer: MistralTokenizer,
2341 ) -> None:
2342 captured_kwargs: list[dict] = []
2343
2344 def fake_apply_chat_template(**kwargs):
2345 captured_kwargs.append(kwargs)
2346 return [0]
2347
2348 monkeypatch.setattr(
2349 v15_mistral_tokenizer.transformers_tokenizer,
2350 "apply_chat_template",
2351 fake_apply_chat_template,
2352 )
2353
2354 v15_mistral_tokenizer.apply_chat_template(
2355 messages=_PASSTHROUGH_MESSAGES,
2356 reasoning_effort="high",
2357 )
2358
2359 assert captured_kwargs[-1]["reasoning_effort"] == "high"
2360
2361
2362 def test_v15_apply_chat_template_passes_reasoning_effort_none_by_default(
2363 monkeypatch: pytest.MonkeyPatch,
2364 v15_mistral_tokenizer: MistralTokenizer,
2365 ) -> None:
2366 captured_kwargs: list[dict] = []
2367
2368 def fake_apply_chat_template(**kwargs):
2369 captured_kwargs.append(kwargs)
2370 return [0]
2371
2372 monkeypatch.setattr(
2373 v15_mistral_tokenizer.transformers_tokenizer,
2374 "apply_chat_template",
2375 fake_apply_chat_template,
2376 )
2377
2378 v15_mistral_tokenizer.apply_chat_template(messages=_PASSTHROUGH_MESSAGES)
2379
2380 assert "reasoning_effort" in captured_kwargs[-1]
2381 assert captured_kwargs[-1]["reasoning_effort"] is None
2382
2383
2384 def test_pre_v15_apply_chat_template_omits_reasoning_effort(
2385 monkeypatch: pytest.MonkeyPatch,
2386 v13_mistral_tokenizer: MistralTokenizer,
2387 ) -> None:
2388 captured_kwargs: list[dict] = []
2389
2390 def fake_apply_chat_template(**kwargs):
2391 captured_kwargs.append(kwargs)
2392 return [0]
2393
2394 monkeypatch.setattr(
2395 v13_mistral_tokenizer.transformers_tokenizer,
2396 "apply_chat_template",
2397 fake_apply_chat_template,
2398 )
2399
2400 v13_mistral_tokenizer.apply_chat_template(
2401 messages=_PASSTHROUGH_MESSAGES,
2402 reasoning_effort="high",
2403 )
2404
2405 assert "reasoning_effort" not in captured_kwargs[-1]