@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a10d69116c
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 3, 05:30 UTC

[Bugfix] Use shared coerce_to_schema_type in DeepSeekV32 tool parser (#43019)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed May 20, 2026 at 10:21 UTC a10d69116cb25c8137eeb3f320add71d4e04fda9
2 files changed +270 -285
tests/tool_parsers/test_deepseekv32_tool_parser.py
+261 -233
@@ -16,7 +16,6 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
16 ChatCompletionToolsParam,
17 FunctionDefinition,
18 )
19 -from vllm.tokenizers import get_tokenizer
19 from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
20
21 # ---------------------------------------------------------------------------
@@ -65,58 +64,6 @@ def build_tool_call(func_name: str, params: dict[str, str]) -> str:
64 return f'{FC_START}\n{INV_START}{func_name}">\n{param_strs}\n{INV_END}\n{FC_END}'
65
66
68 -# ---------------------------------------------------------------------------
69 -# Tests: DeepSeekV32ToolParser._convert_param_value
70 -# ---------------------------------------------------------------------------
71 -
72 -
73 -class TestConvertParamValue:
74 - @pytest.fixture
75 - def parser(self):
76 - return make_parser()
77 -
78 - def test_null(self, parser):
79 - assert parser._convert_param_value("null", "string") is None
80 - assert parser._convert_param_value("NULL", "integer") is None
81 -
82 - def test_string(self, parser):
83 - assert parser._convert_param_value("hello", "string") == "hello"
84 -
85 - def test_integer_valid(self, parser):
86 - assert parser._convert_param_value("42", "integer") == 42
87 -
88 - def test_integer_invalid_falls_back_to_str(self, parser):
89 - assert parser._convert_param_value("abc", "int") == "abc"
90 -
91 - def test_number_float(self, parser):
92 - assert parser._convert_param_value("3.14", "number") == pytest.approx(3.14)
93 -
94 - def test_number_whole_returns_int(self, parser):
95 - assert parser._convert_param_value("5.0", "number") == 5
96 - assert isinstance(parser._convert_param_value("5.0", "number"), int)
97 -
98 - def test_boolean_true(self, parser):
99 - assert parser._convert_param_value("true", "boolean") is True
100 - assert parser._convert_param_value("1", "bool") is True
101 -
102 - def test_boolean_false(self, parser):
103 - assert parser._convert_param_value("false", "boolean") is False
104 - assert parser._convert_param_value("False", "bool") is False
105 -
106 - def test_object_valid_json(self, parser):
107 - assert parser._convert_param_value('{"k": 1}', "object") == {"k": 1}
108 -
109 - def test_object_invalid_json_falls_back(self, parser):
110 - assert parser._convert_param_value("not-json", "object") == "not-json"
111 -
112 - def test_array_valid_json(self, parser):
113 - assert parser._convert_param_value("[1, 2]", "array") == [1, 2]
114 -
115 - def test_unknown_type_tries_json_then_string(self, parser):
116 - assert parser._convert_param_value("123", "unknown") == 123
117 - assert parser._convert_param_value("hello", "unknown") == "hello"
118 -
119 -
67 # ---------------------------------------------------------------------------
68 # Tests: extract_tool_calls (non-streaming)
69 # ---------------------------------------------------------------------------
@@ -331,6 +278,163 @@ class TestExtractToolCalls:
278 args = json.loads(result.tool_calls[0].function.arguments)
279 assert args == {"location": "Beijing"}
280
281 + def test_object_and_array_params(self):
282 + """Object and array schema types must be JSON-parsed, not left as strings."""
283 + tool = ChatCompletionToolsParam(
284 + function=FunctionDefinition(
285 + name="update",
286 + parameters={
287 + "type": "object",
288 + "properties": {
289 + "tags": {"type": "array"},
290 + "meta": {"type": "object"},
291 + },
292 + },
293 + ),
294 + )
295 + parser = make_parser(tools=[tool])
296 + model_output = (
297 + f"{FC_START}\n"
298 + f'{INV_START}update">\n'
299 + f'{PARAM_START}tags" string="false">["a", "b"]{PARAM_END}\n'
300 + f'{PARAM_START}meta" string="false">{{"k": 1}}{PARAM_END}\n'
301 + f"{INV_END}\n"
302 + f"{FC_END}"
303 + )
304 + result = parser.extract_tool_calls(model_output, None)
305 + assert result.tools_called
306 + args = json.loads(result.tool_calls[0].function.arguments)
307 + assert args["tags"] == ["a", "b"]
308 + assert isinstance(args["tags"], list)
309 + assert args["meta"] == {"k": 1}
310 + assert isinstance(args["meta"], dict)
311 +
312 + def test_number_param(self):
313 + """Number (float) schema type must be converted correctly."""
314 + tool = ChatCompletionToolsParam(
315 + function=FunctionDefinition(
316 + name="measure",
317 + parameters={
318 + "type": "object",
319 + "properties": {
320 + "ratio": {"type": "number"},
321 + "whole": {"type": "number"},
322 + },
323 + },
324 + ),
325 + )
326 + parser = make_parser(tools=[tool])
327 + model_output = (
328 + f"{FC_START}\n"
329 + f'{INV_START}measure">\n'
330 + f'{PARAM_START}ratio" string="false">3.14{PARAM_END}\n'
331 + f'{PARAM_START}whole" string="false">5.0{PARAM_END}\n'
332 + f"{INV_END}\n"
333 + f"{FC_END}"
334 + )
335 + result = parser.extract_tool_calls(model_output, None)
336 + args = json.loads(result.tool_calls[0].function.arguments)
337 + assert args["ratio"] == pytest.approx(3.14)
338 + assert args["whole"] == 5
339 + assert isinstance(args["whole"], int)
340 +
341 + def test_multi_typed_schema(self):
342 + """Schema with type: ["integer", "null"] must handle both cases."""
343 + tool = ChatCompletionToolsParam(
344 + function=FunctionDefinition(
345 + name="set_val",
346 + parameters={
347 + "type": "object",
348 + "properties": {
349 + "count": {"type": ["integer", "null"]},
350 + "label": {"type": ["string", "null"]},
351 + },
352 + },
353 + ),
354 + )
355 + parser = make_parser(tools=[tool])
356 + model_output = (
357 + f"{FC_START}\n"
358 + f'{INV_START}set_val">\n'
359 + f'{PARAM_START}count" string="false">42{PARAM_END}\n'
360 + f'{PARAM_START}label" string="false">hello{PARAM_END}\n'
361 + f"{INV_END}\n"
362 + f"{FC_END}"
363 + )
364 + result = parser.extract_tool_calls(model_output, None)
365 + args = json.loads(result.tool_calls[0].function.arguments)
366 + assert args["count"] == 42
367 + assert isinstance(args["count"], int)
368 + assert args["label"] == "hello"
369 +
370 + def test_multi_typed_null_value(self):
371 + """Literal 'null' must become None when the schema includes 'null'."""
372 + tool = ChatCompletionToolsParam(
373 + function=FunctionDefinition(
374 + name="clear",
375 + parameters={
376 + "type": "object",
377 + "properties": {
378 + "value": {"type": ["integer", "null"]},
379 + },
380 + },
381 + ),
382 + )
383 + parser = make_parser(tools=[tool])
384 + model_output = (
385 + f"{FC_START}\n"
386 + f'{INV_START}clear">\n'
387 + f'{PARAM_START}value" string="false">null{PARAM_END}\n'
388 + f"{INV_END}\n"
389 + f"{FC_END}"
390 + )
391 + result = parser.extract_tool_calls(model_output, None)
392 + args = json.loads(result.tool_calls[0].function.arguments)
393 + assert args["value"] is None
394 +
395 + def test_null_not_coerced_without_null_in_schema(self):
396 + """Literal 'null' must stay as a string when the schema is just 'string'."""
397 + tool = ChatCompletionToolsParam(
398 + function=FunctionDefinition(
399 + name="echo",
400 + parameters={
401 + "type": "object",
402 + "properties": {
403 + "text": {"type": "string"},
404 + },
405 + },
406 + ),
407 + )
408 + parser = make_parser(tools=[tool])
409 + model_output = (
410 + f"{FC_START}\n"
411 + f'{INV_START}echo">\n'
412 + f'{PARAM_START}text" string="false">null{PARAM_END}\n'
413 + f"{INV_END}\n"
414 + f"{FC_END}"
415 + )
416 + result = parser.extract_tool_calls(model_output, None)
417 + args = json.loads(result.tool_calls[0].function.arguments)
418 + assert args["text"] == "null"
419 + assert isinstance(args["text"], str)
420 +
421 + def test_no_schema_keeps_strings(self):
422 + """Without a tool schema, all string='false' params default to string."""
423 + parser = make_parser(tools=None)
424 + model_output = (
425 + f"{FC_START}\n"
426 + f'{INV_START}unknown_fn">\n'
427 + f'{PARAM_START}count" string="false">42{PARAM_END}\n'
428 + f'{PARAM_START}flag" string="false">true{PARAM_END}\n'
429 + f"{INV_END}\n"
430 + f"{FC_END}"
431 + )
432 + result = parser.extract_tool_calls(model_output, None)
433 + assert result.tools_called
434 + args = json.loads(result.tool_calls[0].function.arguments)
435 + assert args["count"] == "42"
436 + assert args["flag"] == "true"
437 +
438
439 # ---------------------------------------------------------------------------
440 # Tests: extract_tool_calls_streaming
@@ -713,6 +817,110 @@ class TestExtractToolCallsStreaming:
817 content = "".join(d.content for d in deltas if d.content is not None)
818 assert content == full_text
819
820 + def test_object_and_array_params_streaming(self):
821 + """Streaming: object/array params must be JSON-parsed."""
822 + tool = ChatCompletionToolsParam(
823 + function=FunctionDefinition(
824 + name="update",
825 + parameters={
826 + "type": "object",
827 + "properties": {
828 + "tags": {"type": "array"},
829 + "meta": {"type": "object"},
830 + },
831 + },
832 + ),
833 + )
834 + parser = make_parser(tools=[tool])
835 + full_text = (
836 + f"{FC_START}\n"
837 + f'{INV_START}update">\n'
838 + f'{PARAM_START}tags" string="false">["a", "b"]{PARAM_END}\n'
839 + f'{PARAM_START}meta" string="false">{{"k": 1}}{PARAM_END}\n'
840 + f"{INV_END}\n"
841 + f"{FC_END}"
842 + )
843 + deltas = self._stream(parser, full_text)
844 + args = json.loads(self._reconstruct_args(deltas))
845 + assert args["tags"] == ["a", "b"]
846 + assert args["meta"] == {"k": 1}
847 +
848 + def test_multi_typed_schema_streaming(self):
849 + """Streaming: type: ["integer", "null"] must coerce correctly."""
850 + tool = ChatCompletionToolsParam(
851 + function=FunctionDefinition(
852 + name="set_val",
853 + parameters={
854 + "type": "object",
855 + "properties": {
856 + "count": {"type": ["integer", "null"]},
857 + },
858 + },
859 + ),
860 + )
861 + parser = make_parser(tools=[tool])
862 + full_text = (
863 + f"{FC_START}\n"
864 + f'{INV_START}set_val">\n'
865 + f'{PARAM_START}count" string="false">42{PARAM_END}\n'
866 + f"{INV_END}\n"
867 + f"{FC_END}"
868 + )
869 + deltas = self._stream(parser, full_text)
870 + args = json.loads(self._reconstruct_args(deltas))
871 + assert args["count"] == 42
872 + assert isinstance(args["count"], int)
873 +
874 + def test_multi_typed_null_streaming(self):
875 + """Streaming: 'null' with ["integer", "null"] schema must become None."""
876 + tool = ChatCompletionToolsParam(
877 + function=FunctionDefinition(
878 + name="clear",
879 + parameters={
880 + "type": "object",
881 + "properties": {
882 + "value": {"type": ["integer", "null"]},
883 + },
884 + },
885 + ),
886 + )
887 + parser = make_parser(tools=[tool])
888 + full_text = (
889 + f"{FC_START}\n"
890 + f'{INV_START}clear">\n'
891 + f'{PARAM_START}value" string="false">null{PARAM_END}\n'
892 + f"{INV_END}\n"
893 + f"{FC_END}"
894 + )
895 + deltas = self._stream(parser, full_text)
896 + args = json.loads(self._reconstruct_args(deltas))
897 + assert args["value"] is None
898 +
899 + def test_number_param_streaming(self):
900 + """Streaming: number type must be converted."""
901 + tool = ChatCompletionToolsParam(
902 + function=FunctionDefinition(
903 + name="measure",
904 + parameters={
905 + "type": "object",
906 + "properties": {
907 + "ratio": {"type": "number"},
908 + },
909 + },
910 + ),
911 + )
912 + parser = make_parser(tools=[tool])
913 + full_text = (
914 + f"{FC_START}\n"
915 + f'{INV_START}measure">\n'
916 + f'{PARAM_START}ratio" string="false">3.14{PARAM_END}\n'
917 + f"{INV_END}\n"
918 + f"{FC_END}"
919 + )
920 + deltas = self._stream(parser, full_text)
921 + args = json.loads(self._reconstruct_args(deltas))
922 + assert args["ratio"] == pytest.approx(3.14)
923 +
924
925 class TestDelimiterPreservation:
926 """Regression: fast detokenization skipping DSML delimiters (PR #33964)."""
@@ -794,183 +1002,3 @@ class TestDelimiterPreservation:
1002 assert reconstructor.tool_calls[0].function.name == "search"
1003 streamed_args = json.loads(reconstructor.tool_calls[0].function.arguments)
1004 assert streamed_args == ns_args
797 -
798 -
799 -@pytest.fixture(scope="module")
800 -def deepseekv32_tokenizer():
801 - return get_tokenizer(tokenizer_name="deepseek-ai/DeepSeek-V3.2")
802 -
803 -
804 -@pytest.fixture
805 -def parser(deepseekv32_tokenizer):
806 - return DeepSeekV32ToolParser(deepseekv32_tokenizer)
807 -
808 -
809 -def test_convert_param_value_single_types(parser):
810 - """Test _convert_param_value with single type parameters."""
811 - # Test string type
812 - assert parser._convert_param_value("hello", "string") == "hello"
813 - assert parser._convert_param_value("123", "string") == "123"
814 -
815 - # Test integer type - valid integers
816 - assert parser._convert_param_value("123", "integer") == 123
817 - assert parser._convert_param_value("456", "int") == 456
818 - # Invalid integer should return original string (due to exception catch)
819 - assert parser._convert_param_value("abc", "integer") == "abc"
820 -
821 - # Test float/number type
822 - assert parser._convert_param_value("123.45", "float") == 123.45
823 - assert (
824 - parser._convert_param_value("123.0", "number") == 123
825 - ) # Should be int when whole number
826 - assert parser._convert_param_value("123.5", "number") == 123.5
827 - # Invalid float should return original string
828 - assert parser._convert_param_value("abc", "float") == "abc"
829 -
830 - # Test boolean type - valid boolean values
831 - assert parser._convert_param_value("true", "boolean") is True
832 - assert parser._convert_param_value("false", "bool") is False
833 - assert parser._convert_param_value("1", "boolean") is True
834 - assert parser._convert_param_value("0", "boolean") is False
835 - # Invalid boolean should return original string
836 - assert parser._convert_param_value("yes", "boolean") == "yes"
837 - assert parser._convert_param_value("no", "bool") == "no"
838 -
839 - # Test null value
840 - assert parser._convert_param_value("null", "string") is None
841 - assert parser._convert_param_value("null", "integer") is None
842 -
843 - # Test object/array type (JSON)
844 - assert parser._convert_param_value('{"key": "value"}', "object") == {"key": "value"}
845 - assert parser._convert_param_value("[1, 2, 3]", "array") == [1, 2, 3]
846 - # Invalid JSON should return original string
847 - assert parser._convert_param_value("{invalid}", "object") == "{invalid}"
848 -
849 - # Test fallback for unknown type (tries json.loads, then returns original)
850 - assert parser._convert_param_value('{"key": "value"}', "unknown") == {
851 - "key": "value"
852 - }
853 - assert parser._convert_param_value("plain text", "unknown") == "plain text"
854 -
855 -
856 -def test_convert_param_value_multi_typed_values(parser):
857 - """Test _convert_param_value with multi-typed values (list of types)."""
858 - # Test with list of types where first type succeeds
859 - assert parser._convert_param_value("123", ["integer", "string"]) == 123
860 - assert parser._convert_param_value("true", ["boolean", "string"]) is True
861 - assert parser._convert_param_value('{"x": 1}', ["object", "string"]) == {"x": 1}
862 -
863 - # Test with list of types where first type fails but second succeeds
864 - # "abc" is not a valid integer, so should try string next
865 - assert parser._convert_param_value("abc", ["integer", "string"]) == "abc"
866 -
867 - # Test with list of types where all fail - should return original value
868 - # "invalid json" is not valid JSON, last type is "object" which will fail JSON parse
869 - result = parser._convert_param_value("invalid json", ["integer", "object"])
870 - assert result == "invalid json" # Returns original value after all types fail
871 -
872 - # Test with three types
873 - assert parser._convert_param_value("123.5", ["integer", "float", "string"]) == 123.5
874 - assert parser._convert_param_value("true", ["integer", "boolean", "string"]) is True
875 -
876 - # Test with null in multi-type list
877 - assert parser._convert_param_value("null", ["integer", "string"]) is None
878 - assert parser._convert_param_value("null", ["boolean", "object"]) is None
879 -
880 - # Test nested type conversion - boolean fails, integer succeeds
881 - value = parser._convert_param_value("123", ["boolean", "integer", "string"])
882 - assert value == 123 # Should be integer, not boolean
883 -
884 - # Test that order matters
885 - assert (
886 - parser._convert_param_value("123", ["string", "integer"]) == "123"
887 - ) # String first
888 - assert (
889 - parser._convert_param_value("123", ["integer", "string"]) == 123
890 - ) # Integer first
891 -
892 - # Test with all types failing - returns original value
893 - assert (
894 - parser._convert_param_value("not_a_number", ["integer", "float", "boolean"])
895 - == "not_a_number"
896 - )
897 -
898 -
899 -def test_convert_param_value_stricter_type_checking(parser):
900 - """Test stricter type checking in the updated implementation."""
901 - # Boolean now has stricter validation
902 - assert parser._convert_param_value("true", "boolean") is True
903 - assert parser._convert_param_value("false", "boolean") is False
904 - assert parser._convert_param_value("1", "boolean") is True
905 - assert parser._convert_param_value("0", "boolean") is False
906 -
907 - # These should return original string (not valid boolean values)
908 - assert parser._convert_param_value("yes", "boolean") == "yes"
909 - assert parser._convert_param_value("no", "boolean") == "no"
910 - assert parser._convert_param_value("TRUE", "boolean") is True
911 - assert parser._convert_param_value("FALSE", "boolean") is False
912 -
913 - # Integer and float now raise exceptions for invalid values
914 - assert parser._convert_param_value("123abc", "integer") == "123abc"
915 - assert parser._convert_param_value("123.45.67", "float") == "123.45.67"
916 -
917 - # JSON parsing is stricter - invalid JSON returns original
918 - assert parser._convert_param_value("{invalid: json}", "object") == "{invalid: json}"
919 - assert parser._convert_param_value("[1, 2,", "array") == "[1, 2,"
920 -
921 - # Test multi-type with stricter checking
922 - # "yes" is not valid boolean, but string would accept it
923 - assert parser._convert_param_value("yes", ["boolean", "string"]) == "yes"
924 -
925 - # "123abc" is not valid integer or float, but string accepts it
926 - assert (
927 - parser._convert_param_value("123abc", ["integer", "float", "string"])
928 - == "123abc"
929 - )
930 -
931 -
932 -def test_convert_param_value_edge_cases(parser):
933 - """Test edge cases for _convert_param_value."""
934 - # Empty string
935 - assert parser._convert_param_value("", "string") == ""
936 - assert (
937 - parser._convert_param_value("", "integer") == ""
938 - ) # Invalid int returns original
939 -
940 - # Whitespace - trimmed by conversion functions
941 - assert parser._convert_param_value(" 123 ", "integer") == 123
942 - assert parser._convert_param_value(" true ", "boolean") is True
943 -
944 - # Numeric strings with special characters
945 - assert parser._convert_param_value("123.45.67", "float") == "123.45.67"
946 - assert parser._convert_param_value("123abc", "integer") == "123abc"
947 -
948 - # JSON with whitespace - should parse correctly
949 - assert parser._convert_param_value(' { "key" : "value" } ', "object") == {
950 - "key": "value"
951 - }
952 -
953 - # Invalid JSON returns original
954 - assert parser._convert_param_value("{invalid}", "object") == "{invalid}"
955 - assert parser._convert_param_value("[1, 2,", "array") == "[1, 2,"
956 -
957 -
958 -def test_convert_param_value_checked_helper(parser):
959 - """Test the _convert_param_value_checked helper function indirectly."""
960 - # This tests the behavior through the main function
961 - # Valid conversions should work
962 - assert parser._convert_param_value("123", "integer") == 123
963 - assert parser._convert_param_value("123.45", "float") == 123.45
964 - assert parser._convert_param_value("true", "boolean") is True
965 - assert parser._convert_param_value('{"x": 1}', "object") == {"x": 1}
966 -
967 - # Invalid conversions should return original value (exception caught)
968 - assert parser._convert_param_value("abc", "integer") == "abc"
969 - assert parser._convert_param_value("abc", "float") == "abc"
970 - assert parser._convert_param_value("yes", "boolean") == "yes"
971 - assert parser._convert_param_value("{invalid}", "object") == "{invalid}"
972 -
973 - # Test that null handling works in checked function
974 - assert parser._convert_param_value("null", "integer") is None
975 - assert parser._convert_param_value("null", "boolean") is None
976 - assert parser._convert_param_value("null", "object") is None
vllm/tool_parsers/deepseekv32_tool_parser.py
+9 -52
@@ -26,7 +26,12 @@ from vllm.tool_parsers.abstract_tool_parser import (
26 Tool,
27 ToolParser,
28 )
29 -from vllm.tool_parsers.utils import partial_tag_overlap
29 +from vllm.tool_parsers.utils import (
30 + coerce_to_schema_type,
31 + extract_types_from_schema,
32 + find_tool_properties,
33 + partial_tag_overlap,
34 +)
35
36 logger = init_logger(__name__)
37
@@ -109,41 +114,6 @@ class DeepSeekV32ToolParser(ToolParser):
114 param_dict[param_name] = (param_val, string_attr)
115 return param_dict
116
112 - def _convert_param_value_checked(self, value: str, param_type: str) -> Any:
113 - """Convert parameter value to the correct type."""
114 - if value.lower() == "null":
115 - return None
116 -
117 - param_type = param_type.lower()
118 - if param_type in ["string", "str", "text"]:
119 - return value
120 - elif param_type in ["integer", "int"]:
121 - return int(value)
122 - elif param_type in ["number", "float"]:
123 - val = float(value)
124 - return val if val != int(val) else int(val)
125 - elif param_type in ["boolean", "bool"]:
126 - value = value.strip()
127 - if value.lower() not in ["false", "0", "true", "1"]:
128 - raise ValueError("Invalid boolean value")
129 - return value.lower() in ["true", "1"]
130 - elif param_type in ["object", "array"]:
131 - return json.loads(value)
132 - else:
133 - return json.loads(value)
134 -
135 - def _convert_param_value(self, value: str, param_type: str | list[str]) -> Any:
136 - """Convert parameter value to the correct type."""
137 - if not isinstance(param_type, list):
138 - param_type = [param_type]
139 - for current_type in param_type:
140 - try:
141 - return self._convert_param_value_checked(value, current_type)
142 - except Exception:
143 - continue
144 - # return value as fallback
145 - return value
146 -
117 @staticmethod
118 def _repair_param_dict(
119 param_dict: dict[str, Any],
@@ -172,18 +142,7 @@ class DeepSeekV32ToolParser(ToolParser):
142 param_dict: dict[str, tuple[str, str]],
143 ) -> dict[str, Any]:
144 """Convert raw string param values using the tool schema types."""
175 - param_config: dict = {}
176 - if self.tools:
177 - for tool in self.tools:
178 - if (
179 - hasattr(tool, "function")
180 - and tool.function.name == function_name
181 - and hasattr(tool.function, "parameters")
182 - ):
183 - schema = tool.function.parameters
184 - if isinstance(schema, dict) and "properties" in schema:
185 - param_config = schema["properties"]
186 - break
145 + param_config = find_tool_properties(self.tools, function_name)
146
147 converted: dict[str, Any] = {}
148 for name, (value, string_attr) in param_dict.items():
@@ -191,10 +150,8 @@ class DeepSeekV32ToolParser(ToolParser):
150 converted[name] = value
151 continue
152
194 - param_type = "string"
195 - if name in param_config and isinstance(param_config[name], dict):
196 - param_type = param_config[name].get("type", "string")
197 - converted[name] = self._convert_param_value(value, param_type)
153 + param_types = extract_types_from_schema(param_config.get(name, {}))
154 + converted[name] = coerce_to_schema_type(value, param_types)
155 return self._repair_param_dict(converted, param_config)
156
157 def extract_tool_calls(