Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions tests/parser/engine/test_deepseek_v32.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<b><|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<b>", "date": "tomorrow"}

def test_single_tool_streaming(self, mock_tokenizer, mock_request):
text = _func_calls(
_invoke("get_weather", _param("city", "true", "SF")),
Expand Down
65 changes: 65 additions & 0 deletions tests/parser/engine/test_deepseek_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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\n"
f"<{_PARAM_OPEN.format(name='unit', is_str='true')}celsius{_PARAM_CLOSE}"
)
result = json.loads(_dsml_arg_converter(raw, partial=False))
assert result == {
"city": "Tokyo</|DSML|parameter\n",
"unit": "celsius",
}

def test_null_string_false(self):
raw = self._raw(("val", "false", "null"))
result = json.loads(_dsml_arg_converter(raw, partial=False))
Expand All @@ -166,6 +186,51 @@ def test_string_true_not_json_parsed(self):
assert isinstance(result["n"], str)


class TestImplicitParameterClose:
def test_non_streaming_recovers_next_parameter(self, mock_tokenizer, mock_request):
text = (
f"{DSML_TOOL_START}"
f"{DSML_INVOKE_PREFIX}get_weather{DSML_INVOKE_NAME_END}\n"
f"<{_PARAM_OPEN.format(name='location', is_str='true')}Paris "
f"<{_PARAM_OPEN.format(name='date', is_str='true')}"
f"tomorrow{_PARAM_CLOSE}"
f"{DSML_INVOKE_END}{DSML_TOOL_END}"
)

result = DeepSeekV4Parser(mock_tokenizer).extract_tool_calls(text, mock_request)

assert result.tools_called is True
assert json.loads(result.tool_calls[0].function.arguments) == {
"location": "Paris ",
"date": "tomorrow",
}

def test_streaming_split_next_parameter_tag_is_buffered(
self, mock_tokenizer, mock_request
):
chunks = [
DSML_TOOL_START,
f"{DSML_INVOKE_PREFIX}get_weather{DSML_INVOKE_NAME_END}\n",
f"<{_PARAM_OPEN.format(name='location', is_str='true')}"
"Paris a<b><|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<b>",
"date": "tomorrow",
}


# ── Bare </think> absorption and duplicate <think> absorption ─────────


Expand Down
2 changes: 2 additions & 0 deletions vllm/parser/deepseek_v32.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DSML_INVOKE_NAME_END,
DSML_INVOKE_PREFIX,
DSML_PARAM_CLOSE,
DSML_PARAM_START,
_dsml_arg_converter,
_unwrap_wrapper_args,
)
Expand Down Expand Up @@ -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={
Expand Down
5 changes: 4 additions & 1 deletion vllm/parser/deepseek_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@
DSML_INVOKE_PREFIX = f'<{_DSML}invoke name="'
DSML_INVOKE_NAME_END = '">'
DSML_INVOKE_END = f"</{_DSML}invoke>"
DSML_PARAM_START = f"<{_DSML}parameter"
DSML_PARAM_CLOSE = f"</{_DSML}parameter>"

_ESCAPED_DSML = re.escape(_DSML)
_PARAM_RE = re.compile(
rf'<{_ESCAPED_DSML}parameter\s+name="([^"]+)"\s+string="(true|false)">'
rf"(.*?)</{_ESCAPED_DSML}parameter>",
rf"(.*?)"
rf"(?:</{_ESCAPED_DSML}parameter>|(?=<{_ESCAPED_DSML}parameter\s+name=))",
re.DOTALL,
)
_PARTIAL_PARAM_RE = re.compile(
Expand Down Expand Up @@ -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={
Expand Down
Loading