Skip to content

Commit 8556a53

Browse files
authored
fix: DSML token leakage in DeepSeek-V4 and 3.2 (#1647)
Signed-off-by: AlpinDale <alpindale@gmail.com>
1 parent 7e582b7 commit 8556a53

1 file changed

Lines changed: 24 additions & 23 deletions

File tree

aphrodite/tool_parsers/deepseekv32_tool_parser.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Tool,
2727
ToolParser,
2828
)
29+
from aphrodite.tool_parsers.utils import partial_tag_overlap
2930

3031
logger = init_logger(__name__)
3132

@@ -54,8 +55,8 @@ def __init__(self, tokenizer: TokenizerLike, tools: list[Tool] | None = None):
5455
self.tool_call_start_token: str = "<|DSML|function_calls>"
5556

5657
# Streaming state
57-
self.is_tool_call_started: bool = False
5858
self.current_tool_index: int = 0
59+
self._sent_content_idx: int = 0
5960

6061
# Regex patterns for complete parsing
6162
self.tool_call_complete_regex = re.compile(r"<|DSML|function_calls>(.*?)</|DSML|function_calls>", re.DOTALL)
@@ -202,7 +203,7 @@ def extract_tool_calls(
202203
def _reset_streaming_state(self):
203204
"""Reset all streaming state."""
204205
self.current_tool_index = 0
205-
self.is_tool_call_started = False
206+
self._sent_content_idx = 0
206207
self.prev_tool_call_arr.clear()
207208
self.streamed_args_for_tool.clear()
208209

@@ -245,6 +246,24 @@ def _extract_delta_tool_calls(
245246

246247
return delta_tool_calls
247248

249+
def _extract_content(self, current_text: str) -> str | None:
250+
"""Return unsent non-tool-call text, or None.
251+
252+
Holds back any suffix that could be a partial start marker
253+
so that split markers are never leaked as content.
254+
"""
255+
if self.tool_call_start_token not in current_text:
256+
overlap = partial_tag_overlap(current_text, self.tool_call_start_token)
257+
sendable_idx = len(current_text) - overlap
258+
else:
259+
sendable_idx = current_text.index(self.tool_call_start_token)
260+
261+
if sendable_idx > self._sent_content_idx:
262+
content = current_text[self._sent_content_idx : sendable_idx]
263+
self._sent_content_idx = sendable_idx
264+
return content
265+
return None
266+
248267
def extract_tool_calls_streaming(
249268
self,
250269
previous_text: str,
@@ -266,29 +285,11 @@ def extract_tool_calls_streaming(
266285
if not previous_text:
267286
self._reset_streaming_state()
268287

269-
# Detect whether we've entered the tool-call region.
270-
# Use current_text (not delta_text) since the start token may
271-
# be split across chunks.
272-
content_before = None
273-
if self.is_tool_call_started:
274-
pass
275-
elif self.tool_call_start_token in current_text:
276-
# Tool-call region found, capture any plain text before it.
277-
self.is_tool_call_started = True
278-
start_idx = current_text.index(self.tool_call_start_token)
279-
content_before = current_text[len(previous_text) : start_idx] or None
280-
else:
281-
# Still in plain-text region, forward as content.
282-
return DeltaMessage(content=delta_text) if delta_text else None
283-
284-
# Inside tool-call region: emit any newly completed invokes.
288+
content = self._extract_content(current_text)
285289
delta_tool_calls = self._extract_delta_tool_calls(current_text, request)
286290

287-
if delta_tool_calls or content_before:
288-
return DeltaMessage(
289-
content=content_before,
290-
tool_calls=delta_tool_calls,
291-
)
291+
if delta_tool_calls or content:
292+
return DeltaMessage(content=content, tool_calls=delta_tool_calls)
292293

293294
# Empty delta with token ids means EOS or closing tag; return
294295
# non-None so the serving framework can finalize finish_reason.

0 commit comments

Comments
 (0)