Skip to content
Closed
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
22 changes: 22 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,28 @@ def test_partial_value_with_angle_bracket_and_complete_param(self):
assert result["city"] == "Tokyo"
assert result["expr"] == "x<5"

def test_partial_closing_tag_not_leaked(self):
# Streaming can stop mid-closing-tag (missing final ">"); the
# fragment must not leak into the argument value.
raw = (
f"<{_PARAM_OPEN.format(name='location', is_str='true')}"
"Paris</|DSML|parameter"
)
result = json.loads(_dsml_arg_converter(raw, partial=True))
assert result == {"location": "Paris"}

def test_partial_closing_tag_truncated_midway(self):
# Truncation may land anywhere inside the closing tag.
raw = f"<{_PARAM_OPEN.format(name='b', is_str='true')}y</|DSM"
result = json.loads(_dsml_arg_converter(raw, partial=True))
assert result == {"b": "y"}

def test_partial_json_param_with_close_remnant(self):
raw = f'<{_PARAM_OPEN.format(name="n", is_str="false")}{{"lo"}}x</|DSM'
result = json.loads(_dsml_arg_converter(raw, partial=True))
# Invalid in-progress JSON stays omitted, remnant must not corrupt it.
assert result == {}

def test_null_string_false(self):
raw = self._raw(("val", "false", "null"))
result = json.loads(_dsml_arg_converter(raw, partial=False))
Expand Down
24 changes: 24 additions & 0 deletions vllm/parser/deepseek_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
DSML_INVOKE_END = f"</{_DSML}invoke>"
DSML_PARAM_CLOSE = f"</{_DSML}parameter>"

# Any non-empty prefix of a DSML tag: while streaming, a partially
# delivered ``</|DSML|parameter>`` (or the next ``<|DSML|...) tag can
# follow an in-progress parameter value; capturing that fragment would
# leak DSML markup into the streamed tool arguments.
_TAG_PREFIXES = frozenset(
DSML_PARAM_CLOSE[:j] for j in range(1, len(DSML_PARAM_CLOSE) + 1)
) | frozenset(f"<{_DSML}"[:j] for j in range(1, len(f"<{_DSML}") + 1))

_ESCAPED_DSML = re.escape(_DSML)
_PARAM_RE = re.compile(
rf'<{_ESCAPED_DSML}parameter\s+name="([^"]+)"\s+string="(true|false)">'
Expand All @@ -62,6 +70,21 @@
)


def _strip_partial_dsml_tag(value: str) -> str:
"""Cut a trailing fragment that may be a partially delivered DSML tag.

During streaming a parameter value can be followed by the beginning of
``</|DSML|parameter>`` (or of the next ``<|DSML|...) tag. Truncation
may happen at any point inside that tag, so instead of matching the
full marker we trim at the earliest offset where the remainder is a
prefix of one of those tags.
"""
for i in range(len(value)):
if value[i:] in _TAG_PREFIXES:
return value[:i]
return value


def _dsml_arg_converter(raw_args: str, partial: bool) -> str:
params: dict[str, object] = {}

Expand All @@ -81,6 +104,7 @@ def _dsml_arg_converter(raw_args: str, partial: bool) -> str:
pm = _PARTIAL_PARAM_RE.search(raw_args, last_end)
if pm:
name, is_str, value = pm.group(1), pm.group(2), pm.group(3)
value = _strip_partial_dsml_tag(value)
if is_str == "true":
params[name] = value
else:
Expand Down
Loading