""" Unit tests for DeepSeek chat template tool call handling. Tests verify that the DeepSeek chat templates (v3, v3.1, v3.2) correctly handle both dict and string types for tool['function']['arguments'] without double-escaping, addressing issue #11700. """ import json import os import unittest from jinja2 import Template class TestDeepSeekChatTemplateToolCalls(unittest.TestCase): """Test DeepSeek chat templates handle tool calls correctly.""" @classmethod def setUpClass(cls): """Load all DeepSeek chat templates.""" base_path = os.path.join( os.path.dirname(__file__), "..", "..", "examples", "chat_template" ) cls.templates = {} template_files = { "v3": "tool_chat_template_deepseekv3.jinja", "v3.1": "tool_chat_template_deepseekv31.jinja", "v3.2": "tool_chat_template_deepseekv32.jinja", } for version, filename in template_files.items(): template_path = os.path.join(base_path, filename) with open(template_path, "r") as f: template_content = f.read() cls.templates[version] = Template(template_content) def _render_template( self, version, messages, tools=None, add_generation_prompt=True ): """Helper method to render a template with given messages and tools.""" template = self.templates[version] # Common template variables context = { "messages": messages, "add_generation_prompt": add_generation_prompt, "bos_token": "<|begin▁of▁sentence|>", } if tools is not None: context["tools"] = tools return template.render(**context) def test_tool_arguments_as_dict(self): """Test that tool arguments as dict are properly JSON-encoded (normal case).""" # This tests the normal case where arguments come from OpenAI API as dict for version in ["v3", "v3.1", "v3.2"]: with self.subTest(version=version): messages = [ {"role": "user", "content": "What's the weather in NYC?"}, { "role": "assistant", "content": None, "tool_calls": [ { "type": "function", "function": { "name": "get_weather", "arguments": { "city": "New York", "unit": "celsius", }, # Dict }, } ], }, ] tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get weather information", "parameters": { "type": "object", "properties": { "city": {"type": "string"}, "unit": {"type": "string"}, }, }, }, } ] output = self._render_template(version, messages, tools) # Should contain properly formatted JSON (not double-escaped) self.assertIn('"city"', output, f"{version}: Should contain city key") self.assertIn( '"New York"', output, f"{version}: Should contain city value" ) # Should NOT contain double-escaped quotes self.assertNotIn( '\\"city\\"', output, f"{version}: Should not double-escape" ) self.assertNotIn( '\\\\"', output, f"{version}: Should not have escaped backslashes" ) def test_tool_arguments_as_string(self): """Test that tool arguments as string are used as-is (multi-round case).""" # This tests the multi-round function calling case from issue #11700 # where arguments might already be JSON strings from previous model output for version in ["v3", "v3.1", "v3.2"]: with self.subTest(version=version): messages = [ {"role": "user", "content": "What's the stock price of NVDA?"}, { "role": "assistant", "content": None, "tool_calls": [ { "type": "function", "function": { "name": "get_stock_info", "arguments": '{"symbol": "NVDA"}', # Already a JSON string }, } ], }, ] tools = [ { "type": "function", "function": { "name": "get_stock_info", "description": "Get stock information", "parameters": { "type": "object", "properties": { "symbol": {"type": "string"}, }, }, }, } ] output = self._render_template(version, messages, tools) # Should contain the JSON string as-is self.assertIn( '{"symbol": "NVDA"}', output, f"{version}: Should contain JSON as-is", ) # Should NOT double-escape (the bug from issue #11700) # Bad output would look like: "{\"symbol\": \"NVDA\"}" or "{\\"symbol\\": \\"NVDA\\"}" self.assertNotIn( '{\\"symbol\\"', output, f"{version}: Should not double-escape" ) self.assertNotIn( '"{\\"symbol', output, f"{version}: Should not wrap and escape" ) # Verify it's not triple-quoted or escaped self.assertNotIn( '""{"', output, f"{version}: Should not have extra quotes" ) def test_multiple_tool_calls_mixed_types(self): """Test multiple tool calls with mixed dict and string argument types.""" # This tests a complex scenario with multiple tools, some with dict args, some with string for version in ["v3", "v3.1", "v3.2"]: with self.subTest(version=version): messages = [ {"role": "user", "content": "Get weather and stock info"}, { "role": "assistant", "content": None, "tool_calls": [ { "type": "function", "function": { "name": "get_weather", "arguments": {"city": "Boston"}, # Dict }, }, { "type": "function", "function": { "name": "get_stock_info", "arguments": '{"symbol": "TSLA"}', # String }, }, ], }, ] tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get weather", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, }, }, }, { "type": "function", "function": { "name": "get_stock_info", "description": "Get stock info", "parameters": { "type": "object", "properties": {"symbol": {"type": "string"}}, }, }, }, ] output = self._render_template(version, messages, tools) # First tool (dict) should be properly JSON-encoded self.assertIn( '"city"', output, f"{version}: First tool should have city key" ) self.assertIn( '"Boston"', output, f"{version}: First tool should have Boston value", ) # Second tool (string) should be used as-is self.assertIn( '{"symbol": "TSLA"}', output, f"{version}: Second tool should use string as-is", ) # Neither should be double-escaped self.assertNotIn( '\\"city\\"', output, f"{version}: First tool should not double-escape", ) self.assertNotIn( '\\"symbol\\"', output, f"{version}: Second tool should not double-escape", ) def test_tool_call_with_content(self): """Test tool calls that also include content text.""" # Some models include explanatory text along with tool calls for version in ["v3", "v3.1", "v3.2"]: with self.subTest(version=version): messages = [ {"role": "user", "content": "What's the weather?"}, { "role": "assistant", "content": "Let me check the weather for you.", "tool_calls": [ { "type": "function", "function": { "name": "get_weather", "arguments": {"city": "Seattle"}, }, } ], }, ] tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get weather", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, }, }, } ] output = self._render_template(version, messages, tools) # Should contain both the content and the tool call self.assertIn( "Let me check the weather", output, f"{version}: Should include content", ) self.assertIn( '"city"', output, f"{version}: Should include tool arguments" ) self.assertNotIn( '\\"city\\"', output, f"{version}: Should not double-escape" ) if __name__ == "__main__": unittest.main()