[Bugfix] Attach request-level tools to existing system message in DeepSeek V4 Python renderer (#51856)
Signed-off-by: Jason Yao <wsyjh8@gmail.com> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<|begin▁of▁sentence|>
|
||||
<|begin▁of▁sentence|>You are a helpful assistant.
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -26,7 +26,7 @@ Otherwise, output directly after </think> with tool calls or final response.
|
||||
{"name": "search", "description": "Search the web for information", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "num_results": {"type": "integer", "description": "Number of results to return"}}, "required": ["query"]}}
|
||||
|
||||
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
|
||||
You are a helpful assistant.<|User|>What's the weather in Beijing?<|Assistant|><think>The user wants to know the weather in Beijing. I should use the get_weather tool.</think>
|
||||
<|User|>What's the weather in Beijing?<|Assistant|><think>The user wants to know the weather in Beijing. I should use the get_weather tool.</think>
|
||||
|
||||
<|DSML|tool_calls>
|
||||
<|DSML|invoke name="get_weather">
|
||||
|
||||
@@ -485,3 +485,146 @@ def test_deepseek_v4_image_sentinel_ids_match_tokenizer():
|
||||
]
|
||||
)
|
||||
assert image_sentinel_mask(ids).tolist() == [False, True, True, False, False]
|
||||
|
||||
|
||||
def _request_tools():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather for a city",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _encode_reference(messages):
|
||||
from vllm.tokenizers.deepseek_v4_encoding import encode_messages
|
||||
|
||||
return encode_messages(messages, thinking_mode="thinking", reasoning_effort="low")
|
||||
|
||||
|
||||
def test_deepseek_v4_attaches_request_tools_to_existing_system_message():
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "user", "content": "Weather in Paris?"},
|
||||
]
|
||||
|
||||
prompt = _tokenizer().apply_chat_template(
|
||||
messages,
|
||||
tools=_request_tools(),
|
||||
tokenize=False,
|
||||
thinking=True,
|
||||
reasoning_effort="low",
|
||||
)
|
||||
|
||||
expected = _encode_reference(
|
||||
[{**messages[0], "tools": _request_tools()}, messages[1]]
|
||||
)
|
||||
assert prompt == expected
|
||||
assert prompt.startswith("<|begin▁of▁sentence|>You are helpful.\n\n## Tools")
|
||||
assert prompt.count("## Tools") == 1
|
||||
|
||||
|
||||
def test_deepseek_v4_synthetic_system_only_when_no_system_message():
|
||||
"""Without a system message, request tools still get a synthetic leading
|
||||
system entry."""
|
||||
messages = [{"role": "user", "content": "Weather in Paris?"}]
|
||||
|
||||
prompt = _tokenizer().apply_chat_template(
|
||||
messages,
|
||||
tools=_request_tools(),
|
||||
tokenize=False,
|
||||
thinking=True,
|
||||
reasoning_effort="low",
|
||||
)
|
||||
|
||||
expected = _encode_reference(
|
||||
[{"role": "system", "tools": _request_tools()}, messages[0]]
|
||||
)
|
||||
assert prompt == expected
|
||||
assert prompt.startswith("<|begin▁of▁sentence|>\n\n## Tools")
|
||||
|
||||
|
||||
def test_deepseek_v4_request_tools_override_system_message_tools():
|
||||
message_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "message_level_tool",
|
||||
"description": "Should not be rendered",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": "You are helpful.",
|
||||
"tools": message_tools,
|
||||
}
|
||||
user_message = {"role": "user", "content": "Weather in Paris?"}
|
||||
|
||||
prompt = _tokenizer().apply_chat_template(
|
||||
[system_message, user_message],
|
||||
tools=_request_tools(),
|
||||
tokenize=False,
|
||||
thinking=True,
|
||||
reasoning_effort="low",
|
||||
)
|
||||
|
||||
expected = _encode_reference(
|
||||
[{**system_message, "tools": _request_tools()}, user_message]
|
||||
)
|
||||
assert prompt == expected
|
||||
assert '"name": "get_weather"' in prompt
|
||||
assert "message_level_tool" not in prompt
|
||||
|
||||
|
||||
def test_deepseek_v4_attaches_request_tools_to_first_system_message_only():
|
||||
"""Only the first system message, at any index, gets the request tools."""
|
||||
messages = [
|
||||
{"role": "user", "content": "Hi"},
|
||||
{"role": "system", "content": "First system."},
|
||||
{"role": "system", "content": "Second system."},
|
||||
{"role": "user", "content": "Weather in Paris?"},
|
||||
]
|
||||
|
||||
prompt = _tokenizer().apply_chat_template(
|
||||
messages,
|
||||
tools=_request_tools(),
|
||||
tokenize=False,
|
||||
thinking=True,
|
||||
reasoning_effort="low",
|
||||
)
|
||||
|
||||
expected = _encode_reference(
|
||||
[
|
||||
messages[0],
|
||||
{**messages[1], "tools": _request_tools()},
|
||||
messages[2],
|
||||
messages[3],
|
||||
]
|
||||
)
|
||||
assert prompt == expected
|
||||
assert "First system.\n\n## Tools" in prompt
|
||||
assert prompt.count("## Tools") == 1
|
||||
|
||||
|
||||
def test_deepseek_v4_request_tools_do_not_mutate_caller_messages():
|
||||
import copy
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "user", "content": "Weather in Paris?"},
|
||||
]
|
||||
snapshot = copy.deepcopy(messages)
|
||||
|
||||
_tokenizer().apply_chat_template(messages, tools=_request_tools(), tokenize=False)
|
||||
|
||||
assert messages == snapshot
|
||||
|
||||
@@ -35,8 +35,18 @@ def get_deepseek_v4_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer:
|
||||
conversation = kwargs.get("conversation", messages)
|
||||
messages = conversation.copy()
|
||||
if tools is not None and len(tools) > 0:
|
||||
messages.insert(0, {"role": "system"})
|
||||
messages[0]["tools"] = tools # type: ignore[typeddict-unknown-key]
|
||||
# Match the Rust renderer: request tools attach to the first
|
||||
# system message; synthesize one only when none exists.
|
||||
system_idx = next(
|
||||
(i for i, m in enumerate(messages) if m.get("role") == "system"),
|
||||
None,
|
||||
)
|
||||
if system_idx is None:
|
||||
messages.insert(0, {"role": "system"})
|
||||
system_idx = 0
|
||||
else:
|
||||
messages[system_idx] = copy.copy(messages[system_idx])
|
||||
messages[system_idx]["tools"] = tools # type: ignore[typeddict-unknown-key]
|
||||
|
||||
reasoning_effort = kwargs.get("reasoning_effort")
|
||||
if not isinstance(reasoning_effort, str):
|
||||
|
||||
Reference in New Issue
Block a user