Signed-off-by: CedricHuang <38417461+CedricHwong@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
122 lines
4.6 KiB
Python
122 lines
4.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
import copy
|
|
from typing import Any
|
|
|
|
from transformers import TokenizersBackend
|
|
|
|
from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
|
|
|
|
from .deepseek_v41_encoding import (
|
|
IMAGE_PLACEHOLDER,
|
|
REASONING_EFFORT_MAPPINGS,
|
|
encode_messages,
|
|
)
|
|
from .hf import HfTokenizer, get_cached_tokenizer
|
|
from .protocol import TokenizerLike
|
|
|
|
|
|
def _normalize_messages(
|
|
messages: list[ChatCompletionMessageParam],
|
|
) -> list[dict[str, Any]]:
|
|
result = [dict(message) for message in copy.deepcopy(messages)]
|
|
for message in result:
|
|
role = message.get("role")
|
|
if role not in ("system", "developer", "user", "assistant", "tool"):
|
|
raise ValueError(f"Invalid role: {role}")
|
|
if "reasoning" in message:
|
|
message["reasoning_content"] = message["reasoning"]
|
|
content = message.get("content")
|
|
if isinstance(content, list):
|
|
parts = []
|
|
for block in content:
|
|
part_type = block.get("type")
|
|
if part_type in ("text", "input_text", "output_text"):
|
|
parts.append(block.get("text", ""))
|
|
elif part_type in ("image_url", "input_image", "image_pil"):
|
|
parts.append(IMAGE_PLACEHOLDER)
|
|
else:
|
|
raise ValueError(
|
|
"DeepSeek V4.1 supports text and image content only; "
|
|
f"got {part_type!r}"
|
|
)
|
|
message["content"] = "\n\n".join(parts)
|
|
return result
|
|
|
|
|
|
def get_deepseek_v41_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer:
|
|
"""Wrap an HF tokenizer with the V4.1 prompt encoder."""
|
|
wrapped = copy.copy(tokenizer)
|
|
added_vocab = tokenizer.get_added_vocab()
|
|
|
|
class _DeepseekV41Tokenizer(tokenizer.__class__): # type: ignore
|
|
def apply_chat_template(
|
|
self,
|
|
messages: list[ChatCompletionMessageParam],
|
|
tools: list[dict[str, Any]] | None = None,
|
|
**kwargs,
|
|
) -> str | list[int]:
|
|
# The generic renderer's conversation has already flattened text
|
|
# parts with '\n'. V4.1 encodes the original messages with '\n\n'.
|
|
conversation = _normalize_messages(messages)
|
|
if tools:
|
|
system = next((m for m in conversation if m["role"] == "system"), None)
|
|
if system is None:
|
|
system = {"role": "system", "content": ""}
|
|
conversation.insert(0, system)
|
|
system["tools"] = tools
|
|
|
|
thinking = bool(kwargs.get("thinking") or kwargs.get("enable_thinking"))
|
|
if "thinking" not in kwargs and "enable_thinking" not in kwargs:
|
|
thinking = True
|
|
effort = kwargs.get("reasoning_effort")
|
|
if effort == "none":
|
|
thinking = False
|
|
effort = None
|
|
if effort is None:
|
|
effort = "high"
|
|
if not (
|
|
(type(effort) is int and 1 <= effort <= 100)
|
|
or (isinstance(effort, str) and effort in REASONING_EFFORT_MAPPINGS)
|
|
):
|
|
raise ValueError(
|
|
"DeepSeek V4.1 reasoning_effort must be low, high, xhigh, max, "
|
|
"or an integer within [1, 100] in chat_template_kwargs"
|
|
)
|
|
|
|
prompt = encode_messages(
|
|
conversation,
|
|
thinking_mode="thinking" if thinking else "chat",
|
|
drop_thinking=kwargs.get("drop_thinking", True),
|
|
reasoning_effort=effort,
|
|
)
|
|
if kwargs.get("tokenize", True):
|
|
tokenizer_kwargs = {
|
|
key: kwargs[key]
|
|
for key in ("truncation", "max_length")
|
|
if key in kwargs
|
|
}
|
|
return self.encode(prompt, add_special_tokens=False, **tokenizer_kwargs)
|
|
return prompt
|
|
|
|
def num_special_tokens_to_add(self) -> int:
|
|
return len(self.encode(""))
|
|
|
|
def get_added_vocab(self) -> dict[str, int]:
|
|
return added_vocab.copy()
|
|
|
|
def __reduce__(self):
|
|
return get_deepseek_v41_tokenizer, (tokenizer,)
|
|
|
|
_DeepseekV41Tokenizer.__name__ = f"DSV41{tokenizer.__class__.__name__}"
|
|
wrapped.__class__ = _DeepseekV41Tokenizer
|
|
return wrapped
|
|
|
|
|
|
class DeepseekV41Tokenizer(TokenizerLike):
|
|
@classmethod
|
|
def from_pretrained(cls, *args, **kwargs) -> HfTokenizer:
|
|
tokenizer = TokenizersBackend.from_pretrained(*args, **kwargs)
|
|
return get_cached_tokenizer(get_deepseek_v41_tokenizer(tokenizer))
|