Skip to content

fix(deepseek-web): parse DeepSeek V4 native DSML markup and bare parameter tool calls - #13930

Merged
diegosouzapw merged 7 commits into
cheaper-inference:release/v3.8.51from
EkoSaputro14:fix/deepseek-v4-dsml-tool-calling
Sep 29, 2026
Merged

diegosouzapw merged 7 commits into
cheaper-inference:release/v3.8.51from
EkoSaputro14:fix/deepseek-v4-dsml-tool-calling

Conversation

@EkoSaputro14

Copy link
Copy Markdown
Contributor

Summary

This PR fixes tool-calling failures and text corruption when using deepseek-web/deepseek-v4-flash or deepseek-web/deepseek-v4-pro.

Root Cause

DeepSeek V4 (particularly on chat.deepseek.com) natively emits DSML (DeepSeek Markup Language) markup or hybrid ad-hoc tags during tool invocations:

  1. DSML Tag Leaks: The model emits <||DSML|| calls>, <||DSML|| invoke name="...">, and <||DSML|| parameter name="..."> using fullwidth vertical bars (|). OmniRoute's TAG_TOKEN_RE only looked for <tool> and <tool_call>, causing the entire DSML block to be treated as plain message content.
  2. Web Disclaimer Leak: DeepSeek appends "This response is AI-generated, for reference only." at the end of outputs.
  3. Hallucinated Closing Parameter Tag: The model frequently produces valid JSON followed by an invalid closing tag: <tool>{"name": "...", "arguments": {...}}</parameter></tool>. Because of the trailing </parameter>, JSON.parse failed with SyntaxError.
  4. Bare Parameter Blocks: On long agentic trajectory prompts (e.g. 30k+ tokens in coding agents), the model occasionally skips the enclosing <tool> or <invoke> wrapper and outputs bare <parameter name="...">...</parameter> blocks directly.

Solution

In open-sse/translator/deepseekWebTools.ts:

  • Stripped web disclaimer string.
  • Stripped outer DSML envelope tags (<||DSML|| calls>, etc.).
  • Normalized <invoke> tags into <tool> and <parameter> tags into canonical parameter format.
  • Stripped trailing hallucinated </parameter> closing tags right after JSON closing braces (}).
  • Auto-wrapped bare <parameter> sequences into <tool>...</tool> so that OmniRoute's existing schema-based nameless-block matcher can map the parameter to the requested tool.

Testing & Verification

Tested against live prompts with Hermes Agent:

  • Verified parallel tool calling with <||DSML|| invoke> (e.g. vision_analyze + terminal).
  • Verified bare <parameter name="code"> correctly maps to execute_code.
  • Verified streaming SSE (stream: true) correctly emits structured tool_calls chunks without XML tag leakage.

Copilot AI lite review requested due to automatic review settings September 17, 2026 00:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved parsing issues could corrupt or leak tool-call content, and regression tests are missing.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates DeepSeek Web tool-call parsing for V4 DSML markup and bare parameter blocks.

Changes:

  • Normalizes DSML tags and supports bare parameters.
  • Handles disclaimers and malformed closing tags.
  • Review identified unresolved parsing issues and missing regression tests.
File summaries
File Summary
open-sse/translator/deepseekWebTools.ts Adds DeepSeek V4 markup normalization and bare-parameter handling.
Review details

Suppressed comments (3)

open-sse/translator/deepseekWebTools.ts:447

  • The native DSML envelope uses function_calls, but this alternation only removes calls and tool_calls. The inner <...invoke> block will still be parsed, while the unmatched <|DSML|function_calls> / closing tags remain in content after the accepted tool range is stripped, so the reported markup leak is not fully fixed. Include function_calls in the envelope alternatives.
    .replace(/<\/?(?:[||]{1,2})DSML(?:[||]{1,2})\s*(?:calls|tool_calls)>/gi, "")

open-sse/translator/deepseekWebTools.ts:451

  • The unconditional .trim() changes the no-tag fallback contract: a normal reply such as " answer " is returned as "answer", whereas parseToolCallsFromText preserves the original text when no tool tags are present. Preserve surrounding text for plain replies and limit trimming to the tool-normalization path if it is required there.
    .trim();

open-sse/translator/deepseekWebTools.ts:446

  • This global replacement also runs inside tool payloads, so a legitimate argument containing this sentence is silently corrupted before JSON parsing (for example, a message value equal to the disclaimer becomes empty). Restrict the cleanup to a trailing provider footer, such as an end-anchored match, rather than replacing every occurrence.
    .replace(/This response is AI-generated, for reference only\./g, "")
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread open-sse/translator/deepseekWebTools.ts Outdated
.replace(/<\/?(?:[||]{1,2})DSML(?:[||]{1,2})\s*(?:calls|tool_calls)>/gi, "")
.replace(/<(\/?)(?:(?:[||]{1,2})DSML(?:[||]{1,2})\s*)?invoke\b/gi, "<$1tool")
.replace(/<(\/?)(?:(?:[||]{1,2})DSML(?:[||]{1,2})\s*)?parameter\b/gi, "<$1parameter")
.replace(/(}\s*)<\/(?:parameter|invoke|arguments|calls|tool_calls)>/gi, "$1")
Comment thread open-sse/translator/deepseekWebTools.ts Outdated
Comment on lines +453 to +455
// If model emitted bare <parameter> tags without enclosing <tool> or <invoke>
if (/<parameter\b/i.test(text) && !/<tool\b/i.test(text)) {
text = text.replace(/((?:<parameter\b[\s\S]*?<\/parameter>\s*)+)/gi, "<tool>$1</tool>");
@EkoSaputro14

Copy link
Copy Markdown
Contributor Author

Note regarding #13345

Cross-referencing #13345 by @espokaos-ops:

While #13345 addresses the core native DSML <calls> / <invoke> / <parameter> envelope translation, this PR was developed against real production agentic coding traces (Hermes Agent with 30k+ token trajectories) and specifically covers three additional edge cases observed in live DeepSeek V4 web traffic:

  1. Bare Parameter Blocks (Omitted Enclosing Tags):
    In heavy multi-turn contexts, the model occasionally skips both <tool> and <invoke> wrappers entirely, emitting bare <parameter name="...">...</parameter> blocks directly (e.g. <parameter name="code">...</parameter>). This PR wraps those bare blocks so OmniRoute's existing nameless-block schema matching (buildSchemaParamMap) can resolve them to the target tool (e.g., execute_code).
  2. Hallucinated Trailing Closers (}}</parameter></tool>):
    The model frequently produces valid JSON followed immediately by a hallucinated closing tag: <tool>{"name": "...", "arguments": {...}}</parameter></tool>. Without stripping (}\s*)<\/(?:parameter|invoke|arguments)>, JSON.parse fails with a SyntaxError and drops the tool call.
  3. Web Disclaimer Leak:
    DeepSeek Web appends "This response is AI-generated, for reference only." at the end of generations, which leaked into the output unless cleaned.

Whether this is reviewed as a standalone patch or incorporated into #13345, these patterns are critical to avoid silent tool drops during agentic coding sessions.

@EkoSaputro14

Copy link
Copy Markdown
Contributor Author

Thank you @copilot-pull-request-reviewer for the thorough and high-quality review!

All points have been addressed in commits 8647b38 and 6910975:

  1. Envelope regex alternation: Added function_calls to envelope cleaning (<|DSML|function_calls>, etc.).
  2. Preserve plain text formatting: Guarded the normalization pipeline with a marker preflight check (/[||]{1,2}DSML[||]{1,2}/i, /<parameter\b/i, disclaimer text, etc.). Plain responses without tool tags are preserved verbatim, maintaining original whitespace and indentation contract.
  3. Anchor disclaimer cleanup: Restricted disclaimer removal to trailing footer matches (/\s*This response is AI-generated, for reference only\.\s*$/i), preventing accidental argument corruption if the disclaimer phrase appears inside tool parameters.
  4. Regression test suite: Added comprehensive automated tests in tests/unit/deepseek-web-v4-dsml.test.ts covering:
    • Plain text whitespace preservation
    • Native DSML <calls> / <invoke> with fullwidth bars
    • Envelope with function_calls
    • Disclaimer inside tool payload vs trailing disclaimer
    • Bare <parameter> auto-wrapping and schema matching
    • Hallucinated }}</parameter></tool> closing tag recovery

All 6 test cases pass cleanly. Ready for re-review!

diegosouzapw and others added 2 commits September 24, 2026 00:38
# Conflicts:
#	open-sse/translator/deepseekWebTools.ts
…cinated closers

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw
diegosouzapw merged commit e19df42 into cheaper-inference:release/v3.8.51 Sep 29, 2026
3 checks passed
diegosouzapw added a commit to espokaos-ops/OmniRoute that referenced this pull request Sep 29, 2026
Reconciles the native DSML parser with the tip's cheaper-inference#13930/cheaper-inference#14446 handling in
deepseekWebTools.ts: the sloppy-closer DSML parser runs on the original text
first; when it yields no calls the tip's normalizing path runs unchanged
(extracted into normalizeDeepSeekMarkup).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@EkoSaputro14
EkoSaputro14 deleted the fix/deepseek-v4-dsml-tool-calling branch October 15, 2026 13:07
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