main
py 103 lines 3.94 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import copy
4 from typing import Any
5
6 from transformers import TokenizersBackend
7
8 from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
9
10 from .deepseek_v4_encoding import encode_messages
11 from .hf import HfTokenizer, get_cached_tokenizer
12 from .protocol import TokenizerLike
13
14
15 def get_deepseek_v4_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer:
16 """Wraps a tokenizer to use the custom DeepSeek V4 chat template encoding."""
17 dsv4_tokenizer = copy.copy(tokenizer)
18
19 added_vocab = tokenizer.get_added_vocab()
20
21 class _DeepseekV4Tokenizer(tokenizer.__class__): # type: ignore
22 def apply_chat_template(
23 self,
24 messages: list["ChatCompletionMessageParam"],
25 tools: list[dict[str, Any]] | None = None,
26 **kwargs,
27 ) -> str | list[int]:
28 thinking = kwargs.get("thinking")
29 enable_thinking = kwargs.get("enable_thinking")
30 thinking_enabled = bool(thinking) or bool(enable_thinking)
31 if "thinking" not in kwargs and "enable_thinking" not in kwargs:
32 thinking_enabled = True
33 thinking_mode = "thinking" if thinking_enabled else "chat"
34
35 conversation = kwargs.get("conversation", messages)
36 messages = conversation.copy()
37 if tools is not None and len(tools) > 0:
38 # Match the Rust renderer: request tools attach to the first
39 # system message; synthesize one only when none exists.
40 system_idx = next(
41 (i for i, m in enumerate(messages) if m.get("role") == "system"),
42 None,
43 )
44 if system_idx is None:
45 messages.insert(0, {"role": "system"})
46 system_idx = 0
47 else:
48 messages[system_idx] = copy.copy(messages[system_idx])
49 messages[system_idx]["tools"] = tools # type: ignore[typeddict-unknown-key]
50
51 reasoning_effort = kwargs.get("reasoning_effort")
52 if not isinstance(reasoning_effort, str):
53 reasoning_effort = "high" if thinking_enabled else None
54 elif reasoning_effort == "none":
55 thinking_mode = "chat"
56 reasoning_effort = None
57 elif reasoning_effort == "max":
58 reasoning_effort = "max"
59 elif reasoning_effort in ("low", "minimal", "medium"):
60 reasoning_effort = "low"
61 else:
62 reasoning_effort = "high"
63
64 encode_config = dict(
65 thinking_mode=thinking_mode,
66 drop_thinking=kwargs.get("drop_thinking", True),
67 reasoning_effort=reasoning_effort,
68 )
69
70 prompt_str = encode_messages(messages, **encode_config) # type: ignore
71
72 if kwargs.get("tokenize", True):
73 tokenizer_kwargs = {
74 k: kwargs[k] for k in ("truncation", "max_length") if k in kwargs
75 }
76 return self.encode(
77 prompt_str,
78 add_special_tokens=False,
79 **tokenizer_kwargs,
80 )
81
82 return prompt_str
83
84 def num_special_tokens_to_add(self) -> int:
85 return len(self.encode(""))
86
87 def get_added_vocab(self) -> dict[str, int]:
88 return added_vocab.copy()
89
90 def __reduce__(self):
91 return get_deepseek_v4_tokenizer, (tokenizer,)
92
93 _DeepseekV4Tokenizer.__name__ = f"DSV4{tokenizer.__class__.__name__}"
94
95 dsv4_tokenizer.__class__ = _DeepseekV4Tokenizer
96 return dsv4_tokenizer
97
98
99 class DeepseekV4Tokenizer(TokenizerLike):
100 @classmethod
101 def from_pretrained(cls, *args, **kwargs) -> HfTokenizer:
102 tokenizer = TokenizersBackend.from_pretrained(*args, **kwargs)
103 return get_cached_tokenizer(get_deepseek_v4_tokenizer(tokenizer))