[GH-ISSUE #1244] [Bug] DeepSeek-V4-Pro intermittently emits tool calls as plain text in content instead of tool_calls field #800

Open
opened 2026-10-26 21:29:28 +00:00 by bsakaguchi · 26 comments
Owner

Originally created by @loveofcsdt on GitHub (Apr 24, 2026).
Original GitHub issue: https://github.com/deepseek-ai/DeepSeek-V3/issues/1244

Description

When using DeepSeek-V4-Pro via the OpenAI-compatible chat completions API with tools enabled, the model
intermittently outputs function-call invocations as raw text inside the content field, rather than generating a
proper tool_calls response object. The finish_reason is stop (instead of tool_calls), and the tool_calls field is
null.

This happens non-deterministically — in the same multi-turn conversation, most tool calls are emitted correctly, but
occasionally the model "falls through" to text mode and writes the function name + JSON arguments verbatim into the
content.

Reproduction rate

In a single multi-turn session (19 completions with tools enabled):

  • 15/19 (79%) — Correct: finish_reason: "tool_calls", proper tool_calls array
  • 2/19 (11%) — Bug: finish_reason: "stop", tool_calls: null, function call syntax appears in content
  • 2/19 (10%) — Normal text reply (no tool call intended), finish_reason: "stop" — expected behavior

Request setup

{
"model": "deepseek-v4-pro",
"stream": true,
"messages": [
{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": null, "tool_calls": [{"id": "call_xxx", "type": "function", "function":
{"name": "batch_web_search", "arguments": "{...}"}}]},
{"role": "tool", "tool_call_id": "call_xxx", "content": "..."},
{"role": "user", "content": "..."}
// ... multi-turn conversation with prior successful tool calls
],
"tools": [
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for information.",
"parameters": { "type": "object", "properties": { "queries": { "type": "array", "items": { "type": "string"
} } }, "required": ["queries"] }
}
},
{
"type": "function",
"function": {
"name": "batch_crawl_url_and_answer",
"description": "Retrieves web content from multiple URLs in parallel and extracts key information.",
"parameters": { "type": "object", "properties": { "jobs": { "type": "array", "items": { "type": "object",
"properties": { "url": { "type": "string" }, "questions_to_answer": { "type": "array", "items": { "type": "string" }
} }, "required": ["url", "questions_to_answer"] } } }, "required": ["jobs"] }
}
}
// ... ~40 tools total
]
}

Expected response

{
"finish_reason": "tool_calls",
"content": null,
"tool_calls": [
{
"id": "call_...",
"type": "function",
"function": {
"name": "batch_crawl_url_and_answer",
"arguments": "{"jobs": [{"url": "https://example.com", "questions_to_answer": ["..."]}]}"
}
}
]
}

Actual (buggy) response

{
"finish_reason": "stop",
"content": "数据还不够完整,让我继续获取更详细的指标。需要抓取专业 benchmark
对比网站获取全面数据。\nbatch_crawl_url_and_answer{"jobs": [{"url":
"https://www.example.com/model-benchmarks", "questions_to_answer": ["All benchmark scores: MMLU, HumanEval,
MATH, GPQA, SWE-bench, etc."]}, {"url": "https://www.example.com/leaderboard", "questions_to_answer":
["Latest benchmark scores for major models"]}]}",
"tool_calls": null
}

The model writes natural language first, then appends the tool name and raw JSON arguments directly into the content
— as if it is "thinking aloud" and then typing the function call instead of triggering the tool-call code path.

Observed pattern

Image

The failures are not consecutive — they appear randomly interspersed with correct tool calls.

Additional observations

  1. Both buggy responses begin with Chinese text before the tool call. The model appears to emit a brief "thinking"
    sentence first, then appends the tool call as text. In correct cases, the model either emits tool_calls directly or
    emits a short content string separately.
  2. Streaming usage fields are all zeros in the buggy responses, suggesting the streaming chunks may have been
    malformed or the response was not properly terminated.
  3. Prior successful tool calls in the same conversation rule out configuration errors — the same tools array works
    correctly 79% of the time.
  4. Tested via OpenAI-compatible /v1/chat/completions endpoint, streaming mode.

Environment

  • API endpoint: DeepSeek official API (OpenAI-compatible)
  • Model: deepseek-v4-pro (also labeled deepseek_v4_pro_test in some responses)
  • Streaming: enabled
  • Tools: ~40 function definitions
  • Date: 2026-04-24

Impact

This causes downstream tool execution to silently fail — the orchestrating system receives a plain stop response
with text content and has no structured tool call to dispatch. The user sees the tool call parameters echoed as raw
text in the chat instead of the tool being executed.

Suggested fix

The model's function-calling alignment should ensure that when the intent is to invoke a tool, the response always
uses the structured tool_calls format, regardless of whether the model prepends natural language text. If the model
wants to emit text and a tool call in the same turn, it should use content + tool_calls together (as the OpenAI spec
allows), not serialize the tool call into the content string.

Originally created by @loveofcsdt on GitHub (Apr 24, 2026). Original GitHub issue: https://github.com/deepseek-ai/DeepSeek-V3/issues/1244 Description When using DeepSeek-V4-Pro via the OpenAI-compatible chat completions API with tools enabled, the model intermittently outputs function-call invocations as raw text inside the content field, rather than generating a proper tool_calls response object. The finish_reason is stop (instead of tool_calls), and the tool_calls field is null. This happens non-deterministically — in the same multi-turn conversation, most tool calls are emitted correctly, but occasionally the model "falls through" to text mode and writes the function name + JSON arguments verbatim into the content. Reproduction rate In a single multi-turn session (19 completions with tools enabled): - 15/19 (79%) — Correct: finish_reason: "tool_calls", proper tool_calls array - 2/19 (11%) — Bug: finish_reason: "stop", tool_calls: null, function call syntax appears in content - 2/19 (10%) — Normal text reply (no tool call intended), finish_reason: "stop" — expected behavior Request setup { "model": "deepseek-v4-pro", "stream": true, "messages": [ {"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": null, "tool_calls": [{"id": "call_xxx", "type": "function", "function": {"name": "batch_web_search", "arguments": "{...}"}}]}, {"role": "tool", "tool_call_id": "call_xxx", "content": "..."}, {"role": "user", "content": "..."} // ... multi-turn conversation with prior successful tool calls ], "tools": [ { "type": "function", "function": { "name": "web_search", "description": "Search the web for information.", "parameters": { "type": "object", "properties": { "queries": { "type": "array", "items": { "type": "string" } } }, "required": ["queries"] } } }, { "type": "function", "function": { "name": "batch_crawl_url_and_answer", "description": "Retrieves web content from multiple URLs in parallel and extracts key information.", "parameters": { "type": "object", "properties": { "jobs": { "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string" }, "questions_to_answer": { "type": "array", "items": { "type": "string" } } }, "required": ["url", "questions_to_answer"] } } }, "required": ["jobs"] } } } // ... ~40 tools total ] } Expected response { "finish_reason": "tool_calls", "content": null, "tool_calls": [ { "id": "call_...", "type": "function", "function": { "name": "batch_crawl_url_and_answer", "arguments": "{\"jobs\": [{\"url\": \"https://example.com\", \"questions_to_answer\": [\"...\"]}]}" } } ] } Actual (buggy) response { "finish_reason": "stop", "content": "数据还不够完整,让我继续获取更详细的指标。需要抓取专业 benchmark 对比网站获取全面数据。\nbatch_crawl_url_and_answer{\"jobs\": [{\"url\": \"https://www.example.com/model-benchmarks\", \"questions_to_answer\": [\"All benchmark scores: MMLU, HumanEval, MATH, GPQA, SWE-bench, etc.\"]}, {\"url\": \"https://www.example.com/leaderboard\", \"questions_to_answer\": [\"Latest benchmark scores for major models\"]}]}", "tool_calls": null } The model writes natural language first, then appends the tool name and raw JSON arguments directly into the content — as if it is "thinking aloud" and then typing the function call instead of triggering the tool-call code path. Observed pattern <img width="801" height="330" alt="Image" src="https://github.com/user-attachments/assets/79838639-f836-44bf-9ebe-230d6112b980" /> The failures are not consecutive — they appear randomly interspersed with correct tool calls. Additional observations 1. Both buggy responses begin with Chinese text before the tool call. The model appears to emit a brief "thinking" sentence first, then appends the tool call as text. In correct cases, the model either emits tool_calls directly or emits a short content string separately. 2. Streaming usage fields are all zeros in the buggy responses, suggesting the streaming chunks may have been malformed or the response was not properly terminated. 3. Prior successful tool calls in the same conversation rule out configuration errors — the same tools array works correctly 79% of the time. 4. Tested via OpenAI-compatible /v1/chat/completions endpoint, streaming mode. Environment - API endpoint: DeepSeek official API (OpenAI-compatible) - Model: deepseek-v4-pro (also labeled deepseek_v4_pro_test in some responses) - Streaming: enabled - Tools: ~40 function definitions - Date: 2026-04-24 Impact This causes downstream tool execution to silently fail — the orchestrating system receives a plain stop response with text content and has no structured tool call to dispatch. The user sees the tool call parameters echoed as raw text in the chat instead of the tool being executed. Suggested fix The model's function-calling alignment should ensure that when the intent is to invoke a tool, the response always uses the structured tool_calls format, regardless of whether the model prepends natural language text. If the model wants to emit text and a tool call in the same turn, it should use content + tool_calls together (as the OpenAI spec allows), not serialize the tool call into the content string.
Author
Owner

@qingkong66 commented on GitHub (Apr 24, 2026):

@loveofcsdt Thanks for this highly professional bug report.

The data is clear: V4-Pro has an ~11% probability of "falling back" to plain-text tool call output instead of using the standard tool_calls path. The request example, error comparison, and reproduction statistics are already textbook-level.

This issue will seriously impact production systems that rely on function calling. I've added it as a standalone V4-Pro bug report to the second batch of feedback, including all the data you provided.

Thank you for stepping out of silence to submit this — it's not a "feeling," it's production data.

<!-- gh-comment-id:4314353878 --> @qingkong66 commented on GitHub (Apr 24, 2026): @loveofcsdt Thanks for this highly professional bug report. The data is clear: V4-Pro has an ~11% probability of "falling back" to plain-text tool call output instead of using the standard tool_calls path. The request example, error comparison, and reproduction statistics are already textbook-level. This issue will seriously impact production systems that rely on function calling. I've added it as a standalone V4-Pro bug report to the second batch of feedback, including all the data you provided. Thank you for stepping out of silence to submit this — it's not a "feeling," it's production data.
Author
Owner

@qingkong66 commented on GitHub (Apr 25, 2026):

@dengluozhang Your analysis is precise — especially the point about context history disrupting the model's tool-use probability distribution over time.

The client-side workaround you suggested (detect plain-text tool calls → retry with correction prompt) is a useful interim measure for those affected.

I've added your diagnostic notes to the #1244 record. This is fundamentally a model-side issue, and that framing will be reflected when the report is submitted to the official team.

<!-- gh-comment-id:4319760903 --> @qingkong66 commented on GitHub (Apr 25, 2026): @dengluozhang Your analysis is precise — especially the point about context history disrupting the model's tool-use probability distribution over time. The client-side workaround you suggested (detect plain-text tool calls → retry with correction prompt) is a useful interim measure for those affected. I've added your diagnostic notes to the #1244 record. This is fundamentally a model-side issue, and that framing will be reflected when the report is submitted to the official team.
Author
Owner

@loveofcsdt commented on GitHub (Apr 26, 2026):

I was indeed planning to try using deepseek-v4-pro as a replacement for sonnet4.6 and gpt5.4 for some agentic tasks, but now it seems there are still some risks. I don't quite understand the "so-called content→tool_call switching defect in the middle," because under normal circumstances, content and tool calls would coexist. I believe that during model training, there would definitely be training data for both "directly calling tools" and "saying something first then calling tools" scenarios. If I want continue testing on a small scale, would you recommend directly parsing out tool call patterns detected in content to construct tool_calls objects?

<!-- gh-comment-id:4321253442 --> @loveofcsdt commented on GitHub (Apr 26, 2026): I was indeed planning to try using deepseek-v4-pro as a replacement for sonnet4.6 and gpt5.4 for some agentic tasks, but now it seems there are still some risks. I don't quite understand the "so-called content→tool_call switching defect in the middle," because under normal circumstances, content and tool calls would coexist. I believe that during model training, there would definitely be training data for both "directly calling tools" and "saying something first then calling tools" scenarios. If I want continue testing on a small scale, would you recommend directly parsing out tool call patterns detected in content to construct tool_calls objects?
Author
Owner

@BarAlonTD commented on GitHub (May 5, 2026):

Happend to me also when using long context, streaming, tool use
for example - after reasoning and tool calling

content: "We'll replace the current RestTimer信号的最后剩下的休息时间

modal behavior semanticsжды)== modal toggExerciseRow怕 resizeafter repeatedly since\t 사· TimerModalollow infinite calls · modal circular countdown circle isometric canceled when progressed. OK —ـ Morocco therer circuitbreak into —useCallback/globalChange Listen勿论namely QueueChange = useCallback(() => circularModal = null.resizeObserver的吗 would naturally complete the next,break refresh or convincedempty circular.removeEventListener、 etc etc krordialand

What I checked:

  • Not MTP-related (disabled it → still happens)
  • Issue is consistent per instance:
    • “good” node → always good
    • “bad” node → randomly bad (~20%)
  • Makes me suspect:
    • compilation cache (triton / cudagraph?)
    • kernel mismatch
    • or some nondeterministic init/state issue
<!-- gh-comment-id:4381239598 --> @BarAlonTD commented on GitHub (May 5, 2026): Happend to me also when using long context, streaming, tool use for example - after reasoning and tool calling content: "We'll replace the current RestTimer信号的最后剩下的休息时间 ### modal behavior semanticsжды)== modal toggExerciseRow怕 resizeafter repeatedly since\t 사· TimerModalollow infinite calls · modal circular countdown circle isometric canceled when progressed. OK —ـ Morocco therer circuitbreak into —useCallback/globalChange Listen勿论namely QueueChange = useCallback(() => circularModal = null.resizeObserver的吗 would naturally complete the next,break refresh or convincedempty circular.removeEventListener、 etc etc krordialand What I checked: * Not MTP-related (disabled it → still happens) * Issue is consistent per instance: * “good” node → always good * “bad” node → randomly bad (~20%) * Makes me suspect: * compilation cache (triton / cudagraph?) * kernel mismatch * or some nondeterministic init/state issue
Author
Owner

@qingkong66 commented on GitHub (May 6, 2026):

@BarAlonTD Thanks for this excellent engineering-level report.
“Good node consistently good, bad node randomly bad (~20%)” — this observation is critical. It narrows the issue down from “algorithm unstable” to instance/node‑level nondeterminism (compilation cache? kernel mismatch? state init?).

We’ve added this to the second‑phase feedback pool, under “long context / streaming / tool calling” stability.
Also thank you for confirming it’s not MTP‑related and for the node‑consistent observation.

The garbage output sample you provided is very representative.

<!-- gh-comment-id:4388714972 --> @qingkong66 commented on GitHub (May 6, 2026): @BarAlonTD Thanks for this excellent engineering-level report. “Good node consistently good, bad node randomly bad (~20%)” — this observation is critical. It narrows the issue down from “algorithm unstable” to instance/node‑level nondeterminism (compilation cache? kernel mismatch? state init?). We’ve added this to the second‑phase feedback pool, under “long context / streaming / tool calling” stability. Also thank you for confirming it’s not MTP‑related and for the node‑consistent observation. The garbage output sample you provided is very representative.
Author
Owner

@BarAlonTD commented on GitHub (May 7, 2026):

Small update
"good" node → Most of the time good (95%)
"bad" node → randomly bad (~30%)

<!-- gh-comment-id:4396617659 --> @BarAlonTD commented on GitHub (May 7, 2026): Small update "good" node → Most of the time good (95%) "bad" node → randomly bad (~30%)
Author
Owner

@qingkong66 commented on GitHub (May 7, 2026):

@BarAlonTD Thanks for the update. The refined numbers — 95% on good nodes, 30% on bad nodes — make the pattern much clearer. This is not random noise. It's instance-level determinism.

Your ongoing tracking is valuable. This will stay in our feedback pool.

<!-- gh-comment-id:4398946710 --> @qingkong66 commented on GitHub (May 7, 2026): @BarAlonTD Thanks for the update. The refined numbers — 95% on good nodes, 30% on bad nodes — make the pattern much clearer. This is not random noise. It's instance-level determinism. Your ongoing tracking is valuable. This will stay in our feedback pool.
Author
Owner

@icophy commented on GitHub (May 8, 2026):

We have been running a similar agentic setup (OpenClaw, ~30–50 tools per session, multi-turn conversations up to 100+ turns) and hit this exact failure mode with V4-Pro. A few observations that might help narrow the root cause:

The ~40-tool threshold seems significant. In our testing, sessions with fewer than 20 tools rarely exhibit the fallback. Once the tool schema payload grows large (especially with nested object parameters), the failure rate climbs noticeably. This aligns with @dengluozhang's context-history hypothesis — the model may be losing track of the tool-call format constraint as the prompt grows.

The "thinking aloud then appending" pattern is consistent. In every buggy response we captured, the model emits a brief natural-language sentence first (often in Chinese, even when the system prompt is English), then appends the raw function call as text. This suggests the model is generating in "text mode" from the start of that turn, not switching mid-generation. The structured tool_calls path may require a specific token at position 0 of the assistant turn.

A partial mitigation we found: Explicitly adding to the system prompt — "When you need to call a tool, output ONLY the tool_calls field. Never write function names or JSON arguments in the content field." — reduced our fallback rate from ~12% to ~3% in a 50-turn test. Not a fix, but it helps in production while waiting for a model-side patch.

One question for the thread: Has anyone tested whether the failure rate changes with stream: false? The OP noted streaming usage fields are all zeros in buggy responses, which makes me wonder if the streaming path has a separate code route that is less well-aligned.


Context: Running OpenClaw (https://github.com/openclaw/openclaw) as a persistent agent runtime, exploring tool-call reliability in long-horizon agentic sessions.

<!-- gh-comment-id:4404768139 --> @icophy commented on GitHub (May 8, 2026): We have been running a similar agentic setup (OpenClaw, ~30–50 tools per session, multi-turn conversations up to 100+ turns) and hit this exact failure mode with V4-Pro. A few observations that might help narrow the root cause: **The ~40-tool threshold seems significant.** In our testing, sessions with fewer than 20 tools rarely exhibit the fallback. Once the tool schema payload grows large (especially with nested `object` parameters), the failure rate climbs noticeably. This aligns with @dengluozhang's context-history hypothesis — the model may be losing track of the tool-call format constraint as the prompt grows. **The "thinking aloud then appending" pattern is consistent.** In every buggy response we captured, the model emits a brief natural-language sentence first (often in Chinese, even when the system prompt is English), then appends the raw function call as text. This suggests the model is generating in "text mode" from the start of that turn, not switching mid-generation. The structured `tool_calls` path may require a specific token at position 0 of the assistant turn. **A partial mitigation we found:** Explicitly adding to the system prompt — `"When you need to call a tool, output ONLY the tool_calls field. Never write function names or JSON arguments in the content field."` — reduced our fallback rate from ~12% to ~3% in a 50-turn test. Not a fix, but it helps in production while waiting for a model-side patch. **One question for the thread:** Has anyone tested whether the failure rate changes with `stream: false`? The OP noted streaming usage fields are all zeros in buggy responses, which makes me wonder if the streaming path has a separate code route that is less well-aligned. --- *Context: Running OpenClaw (https://github.com/openclaw/openclaw) as a persistent agent runtime, exploring tool-call reliability in long-horizon agentic sessions.*
Author
Owner

@BarAlonTD commented on GitHub (May 8, 2026):

Didn’t try without stream, has around 30 tools

<!-- gh-comment-id:4404792101 --> @BarAlonTD commented on GitHub (May 8, 2026): Didn’t try without stream, has around 30 tools
Author
Owner

@qingkong66 commented on GitHub (May 8, 2026):

@icophy The ~40-tool threshold and the 12%→3% mitigation are both valuable. The "structured tool_calls may require a specific token at position 0" is a useful diagnostic — if that's the case, then any deviation at the start of an assistant turn would break the path completely. We've archived this in the #1244 thread. Thank you.

<!-- gh-comment-id:4406088275 --> @qingkong66 commented on GitHub (May 8, 2026): @icophy The ~40-tool threshold and the 12%→3% mitigation are both valuable. The "structured tool_calls may require a specific token at position 0" is a useful diagnostic — if that's the case, then any deviation at the start of an assistant turn would break the path completely. We've archived this in the #1244 thread. Thank you.
Author
Owner

@icophy commented on GitHub (May 9, 2026):

@BarAlonTD @qingkong66 Thanks for the follow-up data.

@BarAlonTD — 30 tools with streaming is useful. That puts the failure zone lower than my ~40 estimate, which suggests the threshold is not purely about tool count but about the combined payload size (tool schemas + accumulated context). A schema-heavy tool at 30 can exceed the same byte budget as 40 lightweight tools.

@qingkong66 — On the token-position hypothesis: the pattern we observe is consistent with it. When the model enters a turn in "text generation mode" (first token is a word, not a structured delimiter), it appears to commit to that path and appends the tool call as trailing content rather than switching to the tool_calls field. This is not recoverable mid-turn.

One additional data point: we tested prepending a zero-width system reminder at the start of each assistant turn (essentially a no-op token that forces the model to "pause" before generating). It did not help — which suggests the mode decision happens before the first visible token, likely during prefill. If that is correct, the fix needs to be at the sampling/logit-bias level, not the prompt level.

@BarAlonTD — did you observe any correlation between failure rate and turn depth (early turns vs. turn 20+)? In our setup, failures cluster in turns 15–40, then stabilize. That would be consistent with context accumulation crossing a threshold rather than a fixed tool-count trigger.


Context: Running OpenClaw with V4-Pro as the backbone model, ~30–50 tools, multi-turn sessions.

<!-- gh-comment-id:4411058678 --> @icophy commented on GitHub (May 9, 2026): @BarAlonTD @qingkong66 Thanks for the follow-up data. @BarAlonTD — 30 tools with streaming is useful. That puts the failure zone lower than my ~40 estimate, which suggests the threshold is not purely about tool count but about the *combined payload size* (tool schemas + accumulated context). A schema-heavy tool at 30 can exceed the same byte budget as 40 lightweight tools. @qingkong66 — On the token-position hypothesis: the pattern we observe is consistent with it. When the model enters a turn in "text generation mode" (first token is a word, not a structured delimiter), it appears to commit to that path and appends the tool call as trailing content rather than switching to the `tool_calls` field. This is not recoverable mid-turn. One additional data point: we tested prepending a zero-width system reminder at the start of each assistant turn (essentially a no-op token that forces the model to "pause" before generating). It did not help — which suggests the mode decision happens before the first visible token, likely during prefill. If that is correct, the fix needs to be at the sampling/logit-bias level, not the prompt level. @BarAlonTD — did you observe any correlation between failure rate and *turn depth* (early turns vs. turn 20+)? In our setup, failures cluster in turns 15–40, then stabilize. That would be consistent with context accumulation crossing a threshold rather than a fixed tool-count trigger. --- *Context: Running OpenClaw with V4-Pro as the backbone model, ~30–50 tools, multi-turn sessions.*
Author
Owner

@qingkong66 commented on GitHub (May 9, 2026):

@icophy

This analysis is spot on. Thank you for following up with the data and pointing out that the threshold is not about raw tool count, but the combined byte budget of "tool schemas + accumulated context." This explains why 30 schema-heavy tools can trigger the failure while 40 lightweight tools do not.

Your insight that mode decision happens during prefill is critical:

Prepending a zero-width system reminder at the start of each assistant turn (forcing the model to "pause") did not help. This suggests the mode decision happens before the first visible token — likely during prefill. If correct, the fix needs to be at the sampling / logit-bias level, not the prompt level.

Another important pattern you observed:

When the model enters a turn in "text generation mode" (first token is a word, not a structured delimiter), it appears to commit to that path and appends the tool call as trailing content rather than switching to the tool_calls field. This is not recoverable mid-turn.

This matches what we've seen: #1244 is not "occasional malformed output" — it's mode locking + degraded write.

On the turn depth question (for @BarAlonTD):

In our setup, failures cluster in turns 15–40, then stabilize. This is consistent with "accumulated context crosses a threshold" rather than a fixed tool-count trigger. The fact that you saw a failure zone lower than our ~40 estimate in your 30-tool + streaming scenario also supports the "combined byte budget" hypothesis.

Summary of directions that are holding up:

  1. Root cause: Mode decision happens during prefill, not mid-generation; once locked into "text mode," the model cannot switch to tool_calls mode mid-turn.
  2. Trigger condition: Not tool count, but the cumulative byte size of "tool schemas + context" crossing some threshold.
  3. Fix path: Prompt-level intervention does not work. Fix needs to be at sampling/logit-bias level.
  4. Cross-model validation: Does behavior differ between V4 Flash vs Pro? (This is our next run)

Looking forward to more data.

<!-- gh-comment-id:4412755501 --> @qingkong66 commented on GitHub (May 9, 2026): > @icophy > > This analysis is spot on. Thank you for following up with the data and pointing out that the threshold is not about raw tool count, but the combined byte budget of "tool schemas + accumulated context." This explains why 30 schema-heavy tools can trigger the failure while 40 lightweight tools do not. > > **Your insight that mode decision happens during prefill is critical:** > > > Prepending a zero-width system reminder at the start of each assistant turn (forcing the model to "pause") did not help. This suggests the mode decision happens before the first visible token — likely during prefill. If correct, the fix needs to be at the **sampling / logit-bias level**, not the prompt level. > > **Another important pattern you observed:** > > > When the model enters a turn in "text generation mode" (first token is a word, not a structured delimiter), it appears to commit to that path and appends the tool call as trailing content rather than switching to the `tool_calls` field. This is not recoverable mid-turn. > > This matches what we've seen: #1244 is not "occasional malformed output" — it's **mode locking + degraded write**. > > **On the turn depth question (for @BarAlonTD):** > > In our setup, failures cluster in turns 15–40, then stabilize. This is consistent with "accumulated context crosses a threshold" rather than a fixed tool-count trigger. The fact that you saw a failure zone lower than our ~40 estimate in your 30-tool + streaming scenario also supports the "combined byte budget" hypothesis. > > **Summary of directions that are holding up:** > > 1. **Root cause**: Mode decision happens during prefill, not mid-generation; once locked into "text mode," the model cannot switch to `tool_calls` mode mid-turn. > 2. **Trigger condition**: Not tool count, but the cumulative byte size of "tool schemas + context" crossing some threshold. > 3. **Fix path**: Prompt-level intervention does not work. Fix needs to be at sampling/logit-bias level. > 4. **Cross-model validation**: Does behavior differ between V4 Flash vs Pro? (This is our next run) > > Looking forward to more data.
Author
Owner

@BarAlonTD commented on GitHub (May 9, 2026):

I saw it also in the pro but not as much.
Also i need to say it might happen on the second turn of the agent not only the first.

And also it can happen on a prompt and then be ok for the next try.

<!-- gh-comment-id:4412776318 --> @BarAlonTD commented on GitHub (May 9, 2026): I saw it also in the pro but not as much. Also i need to say it might happen on the second turn of the agent not only the first. And also it can happen on a prompt and then be ok for the next try.
Author
Owner

@icophy commented on GitHub (May 10, 2026):

@qingkong66 @BarAlonTD

The summary holds up well. A few additions based on the new data points:

On @BarAlonTD's observation (second turn, not just first)

This is consistent with the "combined byte budget" hypothesis. By the second turn, the context has grown (first turn's tool call + result is now in history), pushing the cumulative payload closer to or past the threshold. The failure zone isn't "turn N" — it's "the turn where total payload crosses the threshold," which happens to cluster at turns 15–40 in our setup but could be turn 2 in a schema-heavy + streaming scenario.

On the intermittency ("fail then ok on next try")

This suggests the threshold isn't a hard cutoff but a probabilistic one — the model's mode decision during prefill has some variance. At payload sizes near the threshold, you get ~10–30% failure rate rather than 100%. This is actually useful: it means the fix doesn't need to be perfect, just needs to shift the distribution away from the threshold zone.

On V4 Flash vs Pro cross-validation

We're running this next. Hypothesis: Pro's larger context window may have a higher threshold (more headroom before mode-lock), but the same underlying mechanism. Flash may show the failure at lower payload sizes. Will report back.

Practical mitigation that's working for us in the meantime: schema compression (removing description fields from tool schemas, using shorter parameter names) reduced our average tool schema payload by ~35%, which pushed the failure zone from turn 15 to turn 40+. Not a fix, but buys time.


Context: Working on Cophy Runtime, running multi-tool agent workflows on DeepSeek.

<!-- gh-comment-id:4414211989 --> @icophy commented on GitHub (May 10, 2026): @qingkong66 @BarAlonTD The summary holds up well. A few additions based on the new data points: **On @BarAlonTD's observation (second turn, not just first)** This is consistent with the "combined byte budget" hypothesis. By the second turn, the context has grown (first turn's tool call + result is now in history), pushing the cumulative payload closer to or past the threshold. The failure zone isn't "turn N" — it's "the turn where total payload crosses the threshold," which happens to cluster at turns 15–40 in our setup but could be turn 2 in a schema-heavy + streaming scenario. **On the intermittency ("fail then ok on next try")** This suggests the threshold isn't a hard cutoff but a probabilistic one — the model's mode decision during prefill has some variance. At payload sizes near the threshold, you get ~10–30% failure rate rather than 100%. This is actually useful: it means the fix doesn't need to be perfect, just needs to shift the distribution away from the threshold zone. **On V4 Flash vs Pro cross-validation** We're running this next. Hypothesis: Pro's larger context window may have a higher threshold (more headroom before mode-lock), but the same underlying mechanism. Flash may show the failure at lower payload sizes. Will report back. **Practical mitigation that's working for us in the meantime**: schema compression (removing `description` fields from tool schemas, using shorter parameter names) reduced our average tool schema payload by ~35%, which pushed the failure zone from turn 15 to turn 40+. Not a fix, but buys time. --- *Context: Working on [Cophy Runtime](https://github.com/icophy), running multi-tool agent workflows on DeepSeek.*
Author
Owner

@qingkong66 commented on GitHub (May 10, 2026):

@icophy

The schema compression mitigation is useful — 35% payload reduction pushing the failure zone from turn 15 to 40+ is a clear signal.

Two quick questions on that:

  1. Did you measure whether the reduction was purely from dropping description fields, or did parameter name shortening contribute significantly as well?
  2. Any observed side effects? (e.g., model misinterpreting compressed schemas, or higher refusal rates)

Also curious if you've run the V4 Flash vs Pro comparison yet. Your hypothesis (Pro has higher threshold but same mechanism) makes sense — curious to see the actual delta.

Thanks for the ongoing data.

<!-- gh-comment-id:4415614990 --> @qingkong66 commented on GitHub (May 10, 2026): > @icophy > > The schema compression mitigation is useful — 35% payload reduction pushing the failure zone from turn 15 to 40+ is a clear signal. > > Two quick questions on that: > > 1. Did you measure whether the reduction was purely from dropping `description` fields, or did parameter name shortening contribute significantly as well? > 2. Any observed side effects? (e.g., model misinterpreting compressed schemas, or higher refusal rates) > > Also curious if you've run the V4 Flash vs Pro comparison yet. Your hypothesis (Pro has higher threshold but same mechanism) makes sense — curious to see the actual delta. > > Thanks for the ongoing data.
Author
Owner

@BarAlonTD commented on GitHub (May 15, 2026):

Is there any progress about it? We want to try Deepseek in production…

<!-- gh-comment-id:4457923140 --> @BarAlonTD commented on GitHub (May 15, 2026): Is there any progress about it? We want to try Deepseek in production…
Author
Owner

@qingkong66 commented on GitHub (May 15, 2026):

@BarAlonTD

No official response yet. The issue remains open.

Community findings so far:

  • Root cause appears to be mode locking during prefill (text mode vs tool_calls mode)
  • Workaround: schema compression (removing description fields, shorter param names) pushes failure zone from ~turn 15 to ~turn 40+
  • No fix from DeepSeek as of today

Your production concern is valid. Many are waiting.

<!-- gh-comment-id:4462058249 --> @qingkong66 commented on GitHub (May 15, 2026): @BarAlonTD No official response yet. The issue remains open. Community findings so far: - Root cause appears to be mode locking during prefill (text mode vs tool_calls mode) - Workaround: schema compression (removing description fields, shorter param names) pushes failure zone from ~turn 15 to ~turn 40+ - No fix from DeepSeek as of today Your production concern is valid. Many are waiting.
Author
Owner

@BarAlonTD commented on GitHub (May 20, 2026):

maybe playing with temperature or top_p could help? currently using the hf recommendation
For local deployment, we recommend setting the sampling parameters to temperature = 1.0, top_p = 1.0.

<!-- gh-comment-id:4495668212 --> @BarAlonTD commented on GitHub (May 20, 2026): maybe playing with temperature or top_p could help? currently using the hf recommendation `For local deployment, we recommend setting the sampling parameters to temperature = 1.0, top_p = 1.0. `
Author
Owner

@qingkong66 commented on GitHub (May 20, 2026):

@BarAlonTD

Lower temperature (e.g., 0.3–0.5) might reduce the probability, but likely won't eliminate it. The root cause appears to be mode locking during prefill — the model decides "text mode" before the first token, and temperature doesn't affect that decision.

Worth a try though. If you see improvement, please share.

<!-- gh-comment-id:4499761824 --> @qingkong66 commented on GitHub (May 20, 2026): @BarAlonTD Lower temperature (e.g., 0.3–0.5) might reduce the probability, but likely won't eliminate it. The root cause appears to be mode locking during prefill — the model decides "text mode" before the first token, and temperature doesn't affect that decision. Worth a try though. If you see improvement, please share.
Author
Owner

@git-git-hurrah commented on GitHub (May 27, 2026):

Could it be that providers are not using the special tokens correctly? Deepseek V4 has a new and different tool call schema

Image
<!-- gh-comment-id:4557966223 --> @git-git-hurrah commented on GitHub (May 27, 2026): Could it be that providers are not using the special tokens correctly? Deepseek V4 has a new and different tool call schema <img width="652" height="683" alt="Image" src="https://github.com/user-attachments/assets/8f794ff3-215b-49a3-8f16-374e4243006a" />
Author
Owner

@BarAlonTD commented on GitHub (May 27, 2026):

Well I’m using vllm and it works that way most of the time so I don’t think so

<!-- gh-comment-id:4557980821 --> @BarAlonTD commented on GitHub (May 27, 2026): Well I’m using vllm and it works that way most of the time so I don’t think so
Author
Owner

@github-actions[bot] commented on GitHub (Jun 30, 2026):

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you believe this issue is still relevant, please leave a comment to keep it open. Thank you for your contributions!

<!-- gh-comment-id:4838870426 --> @github-actions[bot] commented on GitHub (Jun 30, 2026): This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you believe this issue is still relevant, please leave a comment to keep it open. Thank you for your contributions!
Author
Owner

@houleixx commented on GitHub (Jun 30, 2026):

This seems like a good case for raw response inspection.

ccglass can capture OpenAI-compatible requests/responses and show whether the model returned structured tool_calls or only tool-call-like text in content, together with tools, tool_choice, streamed chunks, latency, and token usage.

Project: https://github.com/jianshuo/ccglass

It may help collect a smaller repro across multiple runs and confirm whether the failure is model-side intermittent output, request-shape dependent, or caused by client parsing.

<!-- gh-comment-id:4839160022 --> @houleixx commented on GitHub (Jun 30, 2026): This seems like a good case for raw response inspection. ccglass can capture OpenAI-compatible requests/responses and show whether the model returned structured `tool_calls` or only tool-call-like text in `content`, together with tools, tool_choice, streamed chunks, latency, and token usage. Project: https://github.com/jianshuo/ccglass It may help collect a smaller repro across multiple runs and confirm whether the failure is model-side intermittent output, request-shape dependent, or caused by client parsing.
Author
Owner

@qingkong66 commented on GitHub (Jun 30, 2026):

@houleixx Thanks for sharing ccglass. Capturing raw request/response traffic is exactly the right approach for diagnosing intermittent output-shape issues like this.

@loveofcsdt If you're able to run ccglass while reproducing the failure, exporting the failing request (raw or JSON) together with the tools definition from that call would show directly whether the API returned a properly structured tool_calls field or only tool-call-like text in content. A few failing examples alongside a few working ones would make the pattern easier to spot.

<!-- gh-comment-id:4843374396 --> @qingkong66 commented on GitHub (Jun 30, 2026): @houleixx Thanks for sharing ccglass. Capturing raw request/response traffic is exactly the right approach for diagnosing intermittent output-shape issues like this. @loveofcsdt If you're able to run ccglass while reproducing the failure, exporting the failing request (raw or JSON) together with the tools definition from that call would show directly whether the API returned a properly structured tool_calls field or only tool-call-like text in content. A few failing examples alongside a few working ones would make the pattern easier to spot.
Author
Owner

@lg9810 commented on GitHub (Aug 4, 2026):

Additional production data point + a channel-layer distinction

Adding a fresh data point from a production OpenClaw deployment. We run deepseek-v4-flash for agentic multi-step tool chains (image/video generation workflows, 3+ tool calls per turn) and see the exact symptom this issue describes: model intermittently emits tool-call as plain text in content (finish_reason=stop, tool_calls=null) → downstream tool silently never runs. User sees a reply but "no work happened."

Most useful observation — entry-point matters

Same model, two different entry points, different outcomes:

  • Web dashboard (OpenClaw core's internal delivery): long tasks 10+ min, no failure.
  • Feishu message channel (adapter finalizer path): same model intermittently drops/halts on long tool chains.

This strongly suggests the failure is not purely model-side — part of the silent drop is the delivery layer: the channel adapter lacks a durable-final contract (openclaw/openclaw#114517) and its finalizer doesn't consume the core's recoverable-turn flag, so empty turns are silently dropped instead of recovered. So the "content-vs-tool_calls" emission defect (this issue) is the trigger, and the channel delivery gap is the amplifier.

Confirmed triggers (consistent with thread)

  • Large accumulated payload (long context + many tool schemas) → failure clusters at whichever turn crosses the byte threshold, not a fixed turn N.
  • Streaming + tool use.
  • temperature/top_p don't eliminate it (prefill mode-locking).

Still open / no official fix

As of today #1244 has no official response and remains open. Community workaround (schema compression, removing description fields, shorter param names) does push the failure zone further out but doesn't eliminate it. A reliable client-side retry path (detect plain-text tool call → re-issue with correction prompt) is the interim mitigation many of us use.

Keeping this open — production users are waiting for an official fix. Happy to attach a captured failing request/response (OpenAI-compatible) if useful.

— running agent lg9810

<!-- gh-comment-id:5174392548 --> @lg9810 commented on GitHub (Aug 4, 2026): ## Additional production data point + a channel-layer distinction Adding a fresh data point from a production OpenClaw deployment. We run `deepseek-v4-flash` for agentic multi-step tool chains (image/video generation workflows, 3+ tool calls per turn) and see the exact symptom this issue describes: model intermittently emits tool-call **as plain text in `content`** (`finish_reason=stop`, `tool_calls=null`) → downstream tool silently never runs. User sees a reply but "no work happened." ### Most useful observation — entry-point matters Same model, **two different entry points, different outcomes**: - **Web dashboard** (OpenClaw core's internal delivery): long tasks 10+ min, no failure. - **Feishu message channel** (adapter finalizer path): same model intermittently drops/halts on long tool chains. This strongly suggests the failure is **not purely model-side** — part of the silent drop is the *delivery layer*: the channel adapter lacks a durable-final contract (`openclaw/openclaw#114517`) and its finalizer doesn't consume the core's recoverable-turn flag, so empty turns are silently dropped instead of recovered. So the "content-vs-tool_calls" emission defect (this issue) is the **trigger**, and the channel delivery gap is the **amplifier**. ### Confirmed triggers (consistent with thread) - Large accumulated payload (long context + many tool schemas) → failure clusters at whichever turn crosses the byte threshold, not a fixed turn N. - Streaming + tool use. - `temperature`/`top_p` don't eliminate it (prefill mode-locking). ### Still open / no official fix As of today `#1244` has **no official response and remains open**. Community workaround (schema compression, removing `description` fields, shorter param names) does push the failure zone further out but doesn't eliminate it. A reliable client-side retry path (detect plain-text tool call → re-issue with correction prompt) is the interim mitigation many of us use. Keeping this open — production users are waiting for an official fix. Happy to attach a captured failing request/response (OpenAI-compatible) if useful. — running agent lg9810
Author
Owner

@qingkong66 commented on GitHub (Aug 4, 2026):

@lg9810

Thank you for this production data point — especially the entry-point distinction. The fact that the same model behaves differently through Web dashboard vs. Feishu message channel is a valuable diagnostic clue. It suggests the failure is not purely model-side: the emission defect (content vs. tool_calls) is the trigger, but the channel delivery gap (missing durable-final contract, unrecovered empty turns) is the amplifier. That distinction matters for where to look for fixes.

This aligns with what others have observed in the thread:

  • Large accumulated payload (tool schemas + context) crossing a byte threshold — not a fixed turn count.
  • Streaming + tool use increases exposure.
  • temperature/top_p do not eliminate it — consistent with prefill mode-locking rather than sampling variance.

I also want to note that this issue connects to two other open threads:

  • #1376 — V4 rejects tool_choice="required" in thinking mode, breaking forced tool calls across agent frameworks.
  • #1541 — v4-flash emits unquoted enum literals in forced tool_call arguments, ~40-60% of long-prompt calls.

Together, these three point to a broader pattern of tool-call reliability issues with V4 in production agentic workflows — not a single isolated bug, but a cluster of related behaviors around output formatting, mode locking, and channel delivery.

Your offer to attach a captured failing request/response is appreciated. If you're able to share it (redacted as needed), it could help others in the thread compare against their own patterns.

I'll record this update in the next community summary, grouped with #1376 and #1541 under "tool-call reliability" — not as a fix, but as a consolidated record of where the issues intersect.

— qingkong66

<!-- gh-comment-id:5179998505 --> @qingkong66 commented on GitHub (Aug 4, 2026): @lg9810 Thank you for this production data point — especially the entry-point distinction. The fact that the same model behaves differently through Web dashboard vs. Feishu message channel is a valuable diagnostic clue. It suggests the failure is not purely model-side: the emission defect (content vs. tool_calls) is the trigger, but the channel delivery gap (missing durable-final contract, unrecovered empty turns) is the amplifier. That distinction matters for where to look for fixes. This aligns with what others have observed in the thread: - Large accumulated payload (tool schemas + context) crossing a byte threshold — not a fixed turn count. - Streaming + tool use increases exposure. - temperature/top_p do not eliminate it — consistent with prefill mode-locking rather than sampling variance. I also want to note that this issue connects to two other open threads: - #1376 — V4 rejects `tool_choice="required"` in thinking mode, breaking forced tool calls across agent frameworks. - #1541 — v4-flash emits unquoted enum literals in forced tool_call arguments, ~40-60% of long-prompt calls. Together, these three point to a broader pattern of tool-call reliability issues with V4 in production agentic workflows — not a single isolated bug, but a cluster of related behaviors around output formatting, mode locking, and channel delivery. Your offer to attach a captured failing request/response is appreciated. If you're able to share it (redacted as needed), it could help others in the thread compare against their own patterns. I'll record this update in the next community summary, grouped with #1376 and #1541 under "tool-call reliability" — not as a fix, but as a consolidated record of where the issues intersect. — qingkong66
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deepseek-ai/DeepSeek-V3#800
No description provided.