When a local coding agent says what it plans to do but never runs the terminal, the model is only one suspect. DeepSeek V4 has a dedicated message encoding and emits tool intent in DSML. A serving layer must recognize that text and expose an OpenAI-compatible tool_calls object. The client must execute it, append the result, and preserve the assistant state in the next request.
The fastest diagnosis is therefore not “try another quant.” It is: capture one complete tool turn and find the first representation that is wrong. Do not change the model file, parser, streaming mode, prompt, and agent client in the same experiment.
Start with the five boundaries, not the visible symptom
The same “agent did nothing” UI can hide five different failures:
| First bad boundary | What you can observe | Most useful next test |
|---|---|---|
| Model generation | No DSML invoke appears, or its wrapper/name/arguments are malformed | Force one simple tool; shorten context; compare an official artifact with identical sampling |
| Server parser | Raw output has a complete invoke, but the API response has no structured tool_calls | Run the parser reproduction; verify V4 parser/tokenizer flags and installed version |
| Quant/runtime | The artifact does not load, emits different structural tokens, or fails only with one kernel/speculative path | Hold the request and parser fixed; swap one artifact or runtime feature |
| Agent harness | The server returns tool_calls, but the client does not execute them or changes the schema | Log the normalized client event and the exact dispatch decision |
| History replay | The first tool runs, then the next request fails or the agent loses state | Compare the returned assistant message with the message actually appended to history |
This ordering matters. A low-bit artifact can change token probabilities, but it cannot explain a parser-only test that fails without loading any weights. Conversely, fixing a parser cannot recover an invoke block the model never emitted.
DSML is the wire clue, not the client contract
DeepSeek's official V4 model card says the release does not ship a normal Jinja chat template. It provides a dedicated encoding implementation that turns OpenAI-style messages into the model prompt and parses completion text back into messages. Tool calls use DSML wrappers around an invoke and its named parameters.
A simplified raw completion can look like this:
text<|DSML|tool_calls> <|DSML|invoke name="read_file"> <|DSML|parameter name="path" string="true">src/app.ts</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
Your coding agent should normally receive a structured event, not parse those tags itself:
json{ "tool_calls": [{ "type": "function", "function": { "name": "read_file", "arguments": "{\"path\":\"src/app.ts\"}" } }] }
If the raw text lacks the opening wrapper, misspells it, truncates an invoke, or names an undeclared tool, start at model output or its rendering conditions. If raw text is complete but DSML appears in ordinary content, the conversion boundary is the stronger suspect.
That distinction is not theoretical. vLLM issue #48931 provides a parser-level reproduction in which an invoke without the outer start marker is returned as content with no tool_calls. It requires no GPU. A newer report, #51914, describes a malformed opening wrapper on V4-Flash-0731 with vLLM 0.27.1 and DSpark, while explicitly stating that DSpark causality was not established. One report exercises parser recovery; the other begins with malformed generated structure. They should not be merged into “DSML is broken.”
Prove or eliminate the parser before touching weights
Record three artifacts for one request:
- raw completion text before tool parsing;
- the server's final non-streaming JSON response;
- the streamed events, if the failure only occurs with streaming.
Then compare stream=false with stream=true, keeping everything else fixed. Also compare tool_choice="required" with tool_choice="auto" on a prompt that genuinely requires the declared tool. vLLM issue #40801 reported DSML leakage mainly in auto + stream, with the symptom reduced under required or non-streaming in that environment. The issue is closed and versions have moved; use those settings as diagnostic toggles, not permanent folklore.
Confirm that the server actually selected the DeepSeek V4 tokenizer, reasoning parser, and tool parser expected by your installed release. Current vLLM implements V4 reasoning and DSML in a dedicated parser state machine. Older releases used different paths and received fixes for string typing, wrapper unwrapping, and end-of-stream buffering, tracked in #41240. Copying launch flags from a current example does not backport current parser code into an old container.
A useful parser gate needs no coding agent:
pythondef classify(raw_text, api_message): has_invoke = "<|DSML|invoke" in raw_text calls = api_message.get("tool_calls") or [] if has_invoke and not calls: return "DSML reached the server, but structured tool_calls did not" if calls: return "parser boundary passed; inspect client dispatch" return "no tool structure observed; inspect generation and prompt rendering"
Use the runtime's real parser unit API for a formal reproduction. The small classifier is only a trace gate; it is deliberately not a tolerant DSML parser.

Quantization is a controlled variable, not a default verdict
DeepSeek's model card lists V4-Flash's official release as mixed FP4 for MoE experts and FP8 for most other parameters. “Quantized” is therefore not synonymous with “unofficial” or “structurally unsafe.” The compatibility question is whether your exact artifact, tokenizer/encoding revision, and runtime agree.
There are two different quant-related failure classes:
- load/runtime incompatibility: vLLM issue #41604 reports non-canonical V4 quantizations failing at initialization because expected
scale_fmtmetadata is absent. The engine never reaches a tool turn. - generation difference: a different artifact may change whether the model emits every DSML delimiter under a difficult prompt. That requires an inference A/B; it cannot be inferred from the bit count or filename.
For a defensible A/B, fix the model revision, tokenizer and encoding files, runtime build, parser flags, sampling, context, tool schema, prompt, and seed where supported. Change only the artifact. Run enough repeated tool turns to capture an intermittent failure, and compare raw output before the parser. If both artifacts emit identical valid DSML and only one API response loses tool_calls, quantization is not the earliest failing boundary.
Clear the harness only after the second request
Once the server returns a structured call, trace the client's normalized event, dispatch, tool result, and next outbound request. Common harness failures include filtering an unfamiliar finish reason, expecting a different argument shape, rejecting a tool name after aliasing, assigning the wrong call ID, or reconstructing the assistant message with only visible content.
The last case is especially easy to confuse with a local parser defect. DeepSeek's hosted thinking-mode documentation requires reasoning_content from an assistant tool-call turn to be passed back in later requests; omitting it can produce HTTP 400. A local OpenAI-compatible server may enforce a different shape, so do not blindly apply the hosted rule. Instead, compare what the server returned with what the harness replayed, field by field.
The harness passes only when all of these are true:
- the client receives the declared tool name and valid arguments;
- the dispatcher actually invokes that tool once;
- the tool result carries the matching call identity;
- the next request includes the required assistant state and tool result in the server's expected representation;
- the model resumes rather than repeating or printing the call.
If direct curl or a minimal SDK loop completes two turns while the coding agent fails against the same endpoint, stop tuning the quant. The remaining difference is in client normalization, permissions, dispatch, or history construction.

A minimal matrix that produces an actionable bug report
Run a small sequence instead of an unbounded benchmark:
| Toggle | Hold fixed | What a changed result suggests |
|---|---|---|
| Non-streaming → streaming | artifact, prompt, tools, parser, harness | chunk buffering or streaming adapter |
required → auto | everything else | tool-selection or auto parse path |
| Short → long context | same task and tool schema | delimiter omission, truncation, or context-sensitive generation |
| Concurrency 1 → production load | same request set | shared state, scheduling, kernel, or load-sensitive parsing |
| Official artifact → candidate quant | tokenizer, runtime, parser, sampling | artifact-specific loading or generation |
| Minimal loop → coding agent | same server and request semantics | harness normalization, permissions, or replay |
Concurrency deserves its own row. vLLM issue #48089 reports clean sequential runs but malformed output under load in one v0.24.0 setup, including some non-streaming failures. That correlation is a reason to reproduce at concurrency 1 before blaming streaming; it is not a universal failure rate.
Save the exact model repository and revision, quant filename and hash, encoding/tokenizer revision, runtime version, launch flags, request minus secrets, raw completion, parsed response, and client history. A report containing only “Claude Code did not call tools” leaves every layer unresolved.
For the broader model and API contract, see the DeepSeek V4 Pro guide. If the real decision is which model fits a constrained local machine, the local agentic coding model guide owns that task. Here, success is narrower and observable: one tool call remains structured from model text through execution and into the next turn.



