DeepSeek V4 Harness
When you call DeepSeek V4-Pro or V4-Flash via the OpenAI-compatible API, you MUST follow the 10 contract rules below. Violating any one of them causes a documented production bug (the upstream issue and our reproduction probe are cited per rule).
The 10 contract rules (ranked by blast radius)
C1 · Disable thinking unless the task is genuinely reasoning-heavy
deepseek-v4-pro defaults to thinking=enabled. Every call then burns 30-300 reasoning_tokens even on trivial prompts.
client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
extra_body={"thinking": {"type": "disabled"}}, # ← saves money on every non-reasoning call
)
For TypeScript / openai-node, put thinking at the top level of the request, not inside extra_body (the JS SDK passes unknown top-level keys through):
await openai.chat.completions.create({
model: "deepseek-v4-pro",
messages,
thinking: { type: "disabled" },
} as any);
C2 · In multi-turn loops, preserve reasoning_content on assistant messages
If thinking IS enabled and you re-send a prior assistant message that has tool_calls, you must include the original reasoning_content field. Otherwise the next request returns:
HTTP 400: The reasoning_content in the thinking mode must be passed back to the API.
(Reproduced in reports/probes/probe_2_reasoning_lifecycle.py 3/3 trials on V4-Pro and V4-Flash, 2026-05-09.)
msg = response.choices[0].message
history.append({
"role": "assistant",
"content": msg.content,
"tool_calls": _serialize_tool_calls(msg.tool_calls),
"reasoning_content": getattr(msg, "reasoning_content", None), # ← REQUIRED
})
When a NEW user turn arrives, you MAY strip reasoning_content from prior assistant messages — DeepSeek doesn't require it across user-turn boundaries, and keeping it bloats the prefix-cache key.
C3 · Always set max_tokens (default 4096)
Without an output cap, reasoning_content can stream 8000+ chunks (probes/probe_9_reasoning_runaway.py measured 26 KB / 84 s on a self-doubt prompt) and downstream Electron clients (ChatWise, Cherry Studio) crash with RangeError: Invalid string length once their string buffer hits V8's 512 MB ceiling.
C4 · Streaming: aggregate parallel tool_calls by tc.index, not list order
DeepSeek interleaves chunks across parallel tool calls (probe_7 100% interleave on 3/3 V4-Pro and V4-Flash trials). Use a dict[int, slot]:
tool_call_acc: dict[int, dict] = {}
for chunk in stream:
for tc in (chunk.choices[0].delta.tool_calls or []):
slot = tool_call_acc.setdefault(tc.index, {"id": None, "name": None, "arguments": ""})
if tc.id: slot["id"] = tc.id
if tc.function and tc.function.name: slot["name"] = tc.function.name
if tc.function and tc.function.arguments: slot["arguments"] += tc.function.arguments
C5 · Streaming: list buffer + "".join, NOT state += chunk
DeepSeek streams 1-3 chars per reasoning chunk. state.text += chunk is O(n²) string allocation:
buf = []
for chunk in stream:
if c := (chunk.choices[0].delta.content or ""):
buf.append(c)
final = "".join(buf)
C6 · Tolerate empty stream chunks
DeepSeek emits ~3 chunks per response with choices == []. Check truthiness before indexing:
for chunk in stream:
choices = chunk.choices or []
if not choices:
if chunk.usage is not None: usage = chunk.usage
continue
...
C7 · Cap context length under 1,048,576 tokens
The V4-Pro / V4-Flash hard ceiling is exactly 2^20 = 1,048,576 tokens (probe_6b validated). The server enforces len(messages_tokens) + max_tokens <= 1,048,576 and returns 400 if exceeded.
C8 · Cache awareness — do NOT inject volatile content into the system prompt
DeepSeek's prefix cache buckets in 256-token blocks and gives a 50× discount on hits. To maximise:
- Do NOT prune or summarise history aggressively (every prune = cache miss)
- Do NOT inject "current date: 2026-05-09" or similar into the cached prefix
- Do read both
prompt_cache_hit_tokens(DeepSeek-native) ANDprompt_tokens_details.cached_tokens(OpenAI-shape) — both are returned
C9 · Avoid /beta endpoint when calling V4 with tools
/beta silently remaps deepseek-v4-pro → legacy deepseek-reasoner, which rejects specific tool_choice={"type":"function","function":{"name":"..."}}. Use https://api.deepseek.com for tool-using flows.
C10 · strict: true is empirically OK on V4 (despite #1069 still being open)
You MAY enable function.strict=true and additionalProperties=false on V4-Pro / V4-Flash — the historic JSON-corruption bug (deepseek-ai/DeepSeek-V3#1069) was not reproducible in 32 trials on 2026-05-09. Still validate JSON post-hoc with jsonschema.
Drop-in implementation
If the user's environment allows installing third-party libraries, use any of:
| form | install | command |
|---|---|---|
| Python lib | pip install deepseek-harness |
from deepseek_harness import DeepSeekHarness |
| Python CLI | pip install deepseek-harness-cli |
dsh chat, dsh doctor, dsh validate |
| MCP server | npx -y @deepseek-harness/mcp |
wire into Claude/Cursor/Cline/ChatWise MCP config |
If they want zero deps beyond openai, copy scripts/safe_init.py (in this skill folder) into their project. It implements all 10 rules in 200 lines.
Reproduction commands
Every claim in this skill is backed by a runnable probe in reports/probes/:
# Reproduce the reasoning_content 400:
python reports/probes/probe_2_reasoning_lifecycle.py --n 3
# Expected: 3/3 phase-B BadRequestError with the verbatim error message above.
# Confirm tool-call leakage rate at 0% on official endpoint:
python reports/probes/probe_3_tool_call_leakage.py --n 30
# Map cache prefix sensitivity:
python reports/probes/probe_5_cache_prefix_sensitivity.py --n 8
See reports/REPORT_2026-05-09.md for the full 16-finding report.