diff --git a/tests/parser/engine/test_deepseek_v32.py b/tests/parser/engine/test_deepseek_v32.py index 7825f5add836..85706a02dc8e 100644 --- a/tests/parser/engine/test_deepseek_v32.py +++ b/tests/parser/engine/test_deepseek_v32.py @@ -181,6 +181,25 @@ def test_ignores_thinking_kwargs(self, mock_tokenizer): class TestStreaming: + def test_split_parameter_start_is_buffered(self, mock_tokenizer, mock_request): + chunks = [ + DSML_FUNC_START, + f"{DSML_INVOKE_PREFIX}edit{DSML_INVOKE_NAME_END}", + f"<{_PARAM_OPEN.format(name='expr', is_str='true')}a<|DSML|parameter", + ' name="date" string="true">tomorrow', + _PARAM_CLOSE, + DSML_INVOKE_END, + DSML_FUNC_END, + ] + + results = simulate_tool_streaming( + DeepSeekV32Parser(mock_tokenizer), mock_request, chunks + ) + + arguments = collect_tool_arguments(results) + assert "<|DSML|parameter" not in arguments + assert json.loads(arguments) == {"expr": "a", "date": "tomorrow"} + def test_single_tool_streaming(self, mock_tokenizer, mock_request): text = _func_calls( _invoke("get_weather", _param("city", "true", "SF")), diff --git a/tests/parser/engine/test_deepseek_v4.py b/tests/parser/engine/test_deepseek_v4.py index ba5ebf76ed67..873a1e6bd2af 100644 --- a/tests/parser/engine/test_deepseek_v4.py +++ b/tests/parser/engine/test_deepseek_v4.py @@ -154,6 +154,26 @@ def test_partial_value_with_angle_bracket_and_complete_param(self): assert result["city"] == "Tokyo" assert result["expr"] == "x<5" + def test_next_parameter_prefix_implicitly_closes_previous(self): + raw = ( + f"<{_PARAM_OPEN.format(name='city', is_str='true')}Tokyo\n" + "<|DSML|parameter name=" + ) + result = json.loads(_dsml_arg_converter(raw, partial=False)) + assert result == {"city": "Tokyo\n"} + + def test_implicit_close_preserves_malformed_text(self): + raw = ( + f"<{_PARAM_OPEN.format(name='city', is_str='true')}" + "Tokyo<|DSML|parameter", + ' name="date" string="true">tomorrow', + _PARAM_CLOSE, + DSML_INVOKE_END, + DSML_TOOL_END, + ] + + results = simulate_tool_streaming( + DeepSeekV4Parser(mock_tokenizer), mock_request, chunks + ) + + arguments = collect_tool_arguments(results) + assert "<|DSML|param" not in arguments + assert json.loads(arguments) == { + "location": "Paris a", + "date": "tomorrow", + } + + # ── Bare absorption and duplicate absorption ───────── diff --git a/vllm/parser/deepseek_v32.py b/vllm/parser/deepseek_v32.py index 0d9ac9f53ce1..e59bd062190e 100644 --- a/vllm/parser/deepseek_v32.py +++ b/vllm/parser/deepseek_v32.py @@ -26,6 +26,7 @@ DSML_INVOKE_NAME_END, DSML_INVOKE_PREFIX, DSML_PARAM_CLOSE, + DSML_PARAM_START, _dsml_arg_converter, _unwrap_wrapper_args, ) @@ -58,6 +59,7 @@ def deepseek_v32_config() -> ParserEngineConfig: "INVOKE_PREFIX": DSML_INVOKE_PREFIX, "INVOKE_NAME_END": DSML_INVOKE_NAME_END, "INVOKE_END": DSML_INVOKE_END, + "PARAM_START": DSML_PARAM_START, "PARAM_CLOSE": DSML_PARAM_CLOSE, }, token_id_terminals={ diff --git a/vllm/parser/deepseek_v4.py b/vllm/parser/deepseek_v4.py index 9955d938545f..7f383d69568e 100644 --- a/vllm/parser/deepseek_v4.py +++ b/vllm/parser/deepseek_v4.py @@ -47,12 +47,14 @@ DSML_INVOKE_PREFIX = f'<{_DSML}invoke name="' DSML_INVOKE_NAME_END = '">' DSML_INVOKE_END = f"" +DSML_PARAM_START = f"<{_DSML}parameter" DSML_PARAM_CLOSE = f"" _ESCAPED_DSML = re.escape(_DSML) _PARAM_RE = re.compile( rf'<{_ESCAPED_DSML}parameter\s+name="([^"]+)"\s+string="(true|false)">' - rf"(.*?)", + rf"(.*?)" + rf"(?:|(?=<{_ESCAPED_DSML}parameter\s+name=))", re.DOTALL, ) _PARTIAL_PARAM_RE = re.compile( @@ -134,6 +136,7 @@ def deepseek_v4_config(thinking: bool = False) -> ParserEngineConfig: "INVOKE_PREFIX": DSML_INVOKE_PREFIX, "INVOKE_NAME_END": DSML_INVOKE_NAME_END, "INVOKE_END": DSML_INVOKE_END, + "PARAM_START": DSML_PARAM_START, "PARAM_CLOSE": DSML_PARAM_CLOSE, }, token_id_terminals={