Skip to content

Add DeepSeek-V4 DSML chat template + tolerate truncated invoke close tag (stacked on #1337) - #1760

Closed
nh13 wants to merge 4 commits into
ml-explore:mainfrom
nh13:feat/deepseek-dsml-template
Closed

Add DeepSeek-V4 DSML chat template + tolerate truncated invoke close tag (stacked on #1337)#1760
nh13 wants to merge 4 commits into
ml-explore:mainfrom
nh13:feat/deepseek-dsml-template

Conversation

@nh13

@nh13 nh13 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1337 (@snagnever's deepseek_dsml parser) — that should land first; the commits unique to this PR are the two on top. Also depends on DeepSeek-V4 model support (#1189).

Adds the encode side plus a robustness fix #1337's parser needs in practice.

1. chat_templates/deepseek_dsml.py — DSML chat template. DeepSeek-V4 ships no Jinja template (only the reference Python encoding_dsv4.py), so there's nothing for chat_template_type to load and tools are never rendered into the prompt. This is a Python apply_chat_template reusing the existing deepseek_v32 DSML helpers; only the V4-specific tool wording and tool_calls block tag live here. Byte-exact with the reference encoder for single-turn system+tools+user prompts (chat and thinking modes) and assistant tool-call replay. It maps the server's enable_thinking kwarg onto the reference's thinking_mode, and normalizes a top-level tools= kwarg onto a system message (synthesizing an empty one if absent) so tools aren't silently dropped when no system message is present. Enable with chat_template_type: "deepseek_dsml".

2. Lenient invoke close tag. The 4/5/6-bit MLX quants systematically truncate </|DSML|invoke> to </|DSML|inv>; the strict _INVOKE close misses those calls (they come back as content). Widen it to </|DSML|inv[^>]*>.

Tests: byte-exact chat-template test (skips when the model's encoding/ dir isn't cached) + a close-tag regression test. No model download required.

Marker note: #1501 (merged) replaced the token-based state machine with a text-based one, so the trailing-> marker workaround this branch inherits is no longer needed on main — the markers can return to full form once this rebases forward past #1189.

snagnever and others added 3 commits May 31, 2026 12:55
DeepSeek-V4 (Flash/Pro) emits tool calls in its native DSML format:

  <|DSML|tool_calls>
  <|DSML|invoke name="get_weather">
  <|DSML|parameter name="city" string="true">Paris</|DSML|parameter>
  </|DSML|invoke>
  ...
  </|DSML|tool_calls>

with multiple <|DSML|invoke> per block (native parallel calls). string="true"
means a literal string value, string="false" a JSON value.

Adds mlx_lm/tool_parsers/deepseek_dsml.py (modeled on minimax_m2) and an
_infer_tool_parser entry so the official DSML chat template auto-selects it. The
start/end markers use the "<|DSML|tool_calls" prefix (dropping the trailing ">"):
mlx-lm matches markers by token-id sequence and the ">" merges with the following
byte on this tokenizer (same class of issue as ml-explore#1335); the parser extracts the
invokes/parameters regardless of the leftover ">".

Verified on mlx-community/DeepSeek-V4-Flash-2bit-DQ: 0 -> 39/40 (98%) on a
jdhodges-style tool suite (the no-tool-template baseline scored 0), 8/8 on parallel
multi-tool cases. Adds tests (single, parallel, mixed string/JSON).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DeepSeek-V4-Flash MLX quants (4/5/6-bit) systematically emit the invoke
close tag truncated to </|DSML|inv> instead of the canonical </|DSML|invoke>,
so the strict close in _INVOKE misses the call and it is returned as plain
content. Widen the close to </|DSML|inv[^>]*>, scoped to the tag boundary.
DeepSeek-V4 ships no Jinja chat template, only the reference Python encoder
(encoding_dsv4.py), so there is nothing for chat_template_type to load and tools
are never rendered into the prompt. Add a Python apply_chat_template that reuses
the existing deepseek_v32 DSML helpers; only the V4-specific tool wording and the
tool_calls block tag live here. Byte-exact with the reference encoder for
single-turn system+tools+user prompts (chat and thinking modes) and assistant
tool-call replay.

- Map the server's enable_thinking kwarg onto the reference thinking_mode.
- Normalize a top-level tools kwarg onto a system message (synthesizing an empty
  one if absent) so tools are not dropped when no system message is present.

Enable with chat_template_type: "deepseek_dsml" (paired with the deepseek_dsml
tool parser).
encode_arguments_to_dsml json.loads() the arguments (the OpenAI JSON string
form), but agent loops replay their own prior tool_calls with arguments already
parsed as a dict, which crashed multi-turn rendering with "the JSON object must
be str, bytes or bytearray, not dict". Re-serialize a dict before encoding.
@zcbenz

zcbenz commented Aug 21, 2026

Copy link
Copy Markdown
Member

Thanks for the PR but at the moment the number of PRs is way beyond our capacity to review so I'm closing the non-essential ones so we can actually work on this repo.

@zcbenz zcbenz closed this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants