|
|
@@ -1,8 +1,3 @@
|
|
|
-# SPDX-License-Identifier: Apache-2.0
|
|
|
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
-# ruff: noqa
|
|
|
-# fmt: off
|
|
|
-
|
|
|
"""
|
|
|
DeepSeek-V4 Encoding
|
|
|
|
|
|
@@ -13,8 +8,7 @@ with tool calling, thinking mode, and quick instruction task support.
|
|
|
from typing import Any, Dict, List, Union, Optional, Tuple
|
|
|
import copy
|
|
|
import json
|
|
|
-
|
|
|
-import regex as re
|
|
|
+import re
|
|
|
|
|
|
# ============================================================
|
|
|
# Special Tokens
|
|
|
@@ -50,7 +44,7 @@ user_msg_template: str = "{content}"
|
|
|
latest_reminder_msg_template: str = "{content}"
|
|
|
assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
|
|
|
assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
|
|
|
-thinking_template: str = "{reasoning}"
|
|
|
+thinking_template: str = "{reasoning_content}"
|
|
|
|
|
|
response_format_template: str = (
|
|
|
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
|
|
|
@@ -67,7 +61,10 @@ tool_output_template: str = (
|
|
|
"<tool_result>{content}</tool_result>"
|
|
|
)
|
|
|
|
|
|
-REASONING_EFFORT_PROMPTS = {
|
|
|
+# Reasoning effort levels. In thinking mode, the prompt for the selected level is
|
|
|
+# prepended at the very beginning of the conversation. `low` is the default and
|
|
|
+# adds nothing.
|
|
|
+REASONING_EFFORT_PROMPTS: Dict[str, str] = {
|
|
|
"low": "",
|
|
|
"high": (
|
|
|
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
|
|
|
@@ -80,6 +77,7 @@ REASONING_EFFORT_PROMPTS = {
|
|
|
"Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n"
|
|
|
),
|
|
|
}
|
|
|
+DEFAULT_REASONING_EFFORT = "low"
|
|
|
|
|
|
TOOLS_TEMPLATE = """## Tools
|
|
|
|
|
|
@@ -116,7 +114,7 @@ def to_json(value: Any) -> str:
|
|
|
"""Serialize a value to JSON string."""
|
|
|
try:
|
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
- except Exception:
|
|
|
+ except:
|
|
|
return json.dumps(value, ensure_ascii=True)
|
|
|
|
|
|
|
|
|
@@ -150,12 +148,12 @@ def tool_calls_to_openai_format(tool_calls):
|
|
|
]
|
|
|
|
|
|
|
|
|
-def encode_arguments_to_dsml(tool_call: Dict[str, Any]) -> str:
|
|
|
+def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
|
|
|
"""
|
|
|
Encode tool call arguments into DSML parameter format.
|
|
|
|
|
|
Args:
|
|
|
- tool_call: Dict with "name" and "arguments" keys.
|
|
|
+ tool_call: Dict with "name" and "arguments" (JSON string) keys.
|
|
|
|
|
|
Returns:
|
|
|
DSML-formatted parameter string.
|
|
|
@@ -163,10 +161,10 @@ def encode_arguments_to_dsml(tool_call: Dict[str, Any]) -> str:
|
|
|
p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
|
|
|
P_dsml_strs = []
|
|
|
|
|
|
- if isinstance(tool_call["arguments"], str):
|
|
|
+ try:
|
|
|
arguments = json.loads(tool_call["arguments"])
|
|
|
- else:
|
|
|
- arguments = tool_call["arguments"]
|
|
|
+ except Exception as err:
|
|
|
+ arguments = {"arguments": tool_call["arguments"]}
|
|
|
|
|
|
for k, v in arguments.items():
|
|
|
p_dsml_str = p_dsml_template.format(
|
|
|
@@ -246,7 +244,8 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
|
|
|
messages: Full list of messages in the conversation.
|
|
|
thinking_mode: Either "chat" or "thinking".
|
|
|
drop_thinking: Whether to drop reasoning content from earlier turns.
|
|
|
- reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
|
|
+ reasoning_effort: Reasoning effort level, one of "low", "high", "max".
|
|
|
+ None is treated as "low".
|
|
|
|
|
|
Returns:
|
|
|
Encoded string for this message.
|
|
|
@@ -263,7 +262,7 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
|
|
|
tools = msg.get("tools")
|
|
|
response_format = msg.get("response_format")
|
|
|
tool_calls = msg.get("tool_calls")
|
|
|
- reasoning = msg.get("reasoning")
|
|
|
+ reasoning_content = msg.get("reasoning_content")
|
|
|
wo_eos = msg.get("wo_eos", False)
|
|
|
|
|
|
if tools:
|
|
|
@@ -272,9 +271,10 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
|
|
|
tool_calls = tool_calls_from_openai_format(tool_calls)
|
|
|
|
|
|
# Reasoning effort prefix (only at index 0 in thinking mode; "low" adds nothing)
|
|
|
- assert reasoning_effort is None or reasoning_effort in REASONING_EFFORT_PROMPTS, \
|
|
|
- f"Invalid reasoning effort: {reasoning_effort}"
|
|
|
- if index == 0 and thinking_mode == "thinking" and reasoning_effort in ("high", "max"):
|
|
|
+ reasoning_effort = reasoning_effort or DEFAULT_REASONING_EFFORT
|
|
|
+ assert reasoning_effort in REASONING_EFFORT_PROMPTS, \
|
|
|
+ f"Invalid reasoning effort: {reasoning_effort}, expected one of {list(REASONING_EFFORT_PROMPTS)}"
|
|
|
+ if index == 0 and thinking_mode == "thinking":
|
|
|
prompt += REASONING_EFFORT_PROMPTS[reasoning_effort]
|
|
|
|
|
|
if role == "system":
|
|
|
@@ -351,14 +351,14 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
|
|
|
)
|
|
|
|
|
|
summary_content = content or ""
|
|
|
- reasoning = reasoning or ""
|
|
|
+ rc = reasoning_content or ""
|
|
|
|
|
|
# Check if previous message has a task - if so, this is a task output (no thinking)
|
|
|
prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
|
|
|
|
|
|
if thinking_mode == "thinking" and not prev_has_task:
|
|
|
if not drop_thinking or index > last_user_idx:
|
|
|
- thinking_part = thinking_template.format(reasoning=reasoning) + thinking_end_token
|
|
|
+ thinking_part = thinking_template.format(reasoning_content=rc) + thinking_end_token
|
|
|
else:
|
|
|
thinking_part = ""
|
|
|
|
|
|
@@ -539,10 +539,11 @@ def encode_messages(
|
|
|
messages: List of message dicts to encode.
|
|
|
thinking_mode: Either "chat" or "thinking".
|
|
|
context: Optional preceding context messages (already encoded prefix).
|
|
|
- drop_thinking: If True, drop reasoning from earlier assistant turns
|
|
|
+ drop_thinking: If True, drop reasoning_content from earlier assistant turns
|
|
|
(only keep reasoning for messages after the last user message).
|
|
|
add_default_bos_token: Whether to prepend BOS token at conversation start.
|
|
|
- reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
|
|
+ reasoning_effort: Reasoning effort level, one of "low", "high", "max".
|
|
|
+ Only takes effect in thinking mode. None is treated as "low".
|
|
|
|
|
|
Returns:
|
|
|
The encoded prompt string.
|
|
|
@@ -589,12 +590,12 @@ def encode_messages(
|
|
|
|
|
|
def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
"""
|
|
|
- Drop reasoning and non-essential messages before the last user message.
|
|
|
+ Drop reasoning_content and non-essential messages before the last user message.
|
|
|
|
|
|
Behavior:
|
|
|
- Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
|
|
|
- Messages at or after the last user index are always kept.
|
|
|
- - Assistant messages before the last user get reasoning removed.
|
|
|
+ - Assistant messages before the last user get reasoning_content removed.
|
|
|
- Developer messages before the last user are dropped entirely.
|
|
|
"""
|
|
|
last_user_idx = find_last_user_index(messages)
|
|
|
@@ -607,7 +608,7 @@ def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, An
|
|
|
result.append(msg)
|
|
|
elif role == "assistant":
|
|
|
msg = copy.copy(msg)
|
|
|
- msg.pop("reasoning", None)
|
|
|
+ msg.pop("reasoning_content", None)
|
|
|
result.append(msg)
|
|
|
# developer and other roles before last_user_idx are dropped
|
|
|
|
|
|
@@ -659,9 +660,9 @@ def parse_tool_calls(index: int, text: str) -> Tuple[int, Optional[str], List[Di
|
|
|
tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
|
|
|
|
|
|
while index < len(text):
|
|
|
- index, content_before, stop_token = _read_until_stop(index, text, [f"<{dsml_token}invoke", tool_calls_end_token])
|
|
|
- if content_before != ">\n":
|
|
|
- raise ValueError(f"Tool call format error: expected '>\\n' but got '{content_before}'")
|
|
|
+ index, _, stop_token = _read_until_stop(index, text, [f"<{dsml_token}invoke", tool_calls_end_token])
|
|
|
+ if _ != ">\n":
|
|
|
+ raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
|
|
|
|
|
|
if stop_token == tool_calls_end_token:
|
|
|
break
|
|
|
@@ -705,7 +706,7 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
|
|
|
|
|
|
This function takes the raw text output from the model (a single assistant turn)
|
|
|
and extracts:
|
|
|
- - reasoning (thinking block)
|
|
|
+ - reasoning_content (thinking block)
|
|
|
- content (summary/response)
|
|
|
- tool_calls (if any)
|
|
|
|
|
|
@@ -717,11 +718,10 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
|
|
|
thinking_mode: Either "chat" or "thinking".
|
|
|
|
|
|
Returns:
|
|
|
- Dict with keys: "role", "content", "reasoning", "tool_calls".
|
|
|
+ Dict with keys: "role", "content", "reasoning_content", "tool_calls".
|
|
|
tool_calls are in OpenAI format.
|
|
|
"""
|
|
|
- summary_content, reasoning = "", ""
|
|
|
- tool_calls: List[Dict[str, str]] = []
|
|
|
+ summary_content, reasoning_content, tool_calls = "", "", []
|
|
|
index, stop_token = 0, None
|
|
|
tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
|
|
|
|
|
|
@@ -730,37 +730,31 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
|
|
|
|
|
|
if is_thinking:
|
|
|
index, content_delta, stop_token = _read_until_stop(index, text, [thinking_end_token, tool_calls_start_token])
|
|
|
- reasoning = content_delta
|
|
|
- if stop_token != thinking_end_token:
|
|
|
- raise ValueError("Invalid thinking format: missing </think>")
|
|
|
+ reasoning_content = content_delta
|
|
|
+ assert stop_token == thinking_end_token, "Invalid thinking format: missing </think>"
|
|
|
|
|
|
index, content_delta, stop_token = _read_until_stop(index, text, [eos_token, tool_calls_start_token])
|
|
|
summary_content = content_delta
|
|
|
if stop_token == tool_calls_start_token:
|
|
|
is_tool_calling = True
|
|
|
else:
|
|
|
- if stop_token != eos_token:
|
|
|
- raise ValueError("Invalid format: missing EOS token")
|
|
|
+ assert stop_token == eos_token, "Invalid format: missing EOS token"
|
|
|
|
|
|
if is_tool_calling:
|
|
|
index, stop_token, tool_calls = parse_tool_calls(index, text)
|
|
|
|
|
|
index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
|
|
|
- if tool_ends_text:
|
|
|
- raise ValueError("Unexpected content after tool calls")
|
|
|
+ assert not tool_ends_text, "Unexpected content after tool calls"
|
|
|
|
|
|
- if len(text) != index or stop_token not in [eos_token, None]:
|
|
|
- raise ValueError("Unexpected content at end")
|
|
|
+ assert len(text) == index and stop_token in [eos_token, None], "Unexpected content at end"
|
|
|
|
|
|
for sp_token in [bos_token, eos_token, thinking_start_token, thinking_end_token, dsml_token]:
|
|
|
- if sp_token in summary_content or sp_token in reasoning:
|
|
|
- raise ValueError(f"Unexpected special token '{sp_token}' in content")
|
|
|
+ assert sp_token not in summary_content and sp_token not in reasoning_content, \
|
|
|
+ f"Unexpected special token '{sp_token}' in content"
|
|
|
|
|
|
return {
|
|
|
"role": "assistant",
|
|
|
"content": summary_content,
|
|
|
- "reasoning": reasoning,
|
|
|
+ "reasoning_content": reasoning_content,
|
|
|
"tool_calls": tool_calls_to_openai_format(tool_calls)
|
|
|
}
|
|
|
-
|
|
|
-# fmt: on
|