Repository navigation
DeepSeek silently stops executing (interleaved reasoning_content dropped in tool call messages) #35689
Description
Description
OpenCode often silently stops executing mid-task when using DeepSeek models (e.g., deepseek-ai/DeepSeek-V4-Flash) in thinking mode via an OpenAI-compatible provider. The agent abruptly exits the session loop instead of continuing to make tool calls.
Root Cause
DeepSeek in thinking mode sends responses with both reasoning_content (thinking) and tool_calls in the same message. When this response is converted back to API format for the next request, the reasoning_content field is silently dropped from the assistant message. DeepSeek's API requires reasoning_content to be preserved across turns when thinking mode is enabled.
The message conversion in the interleaved handler (normalizeMessages / toModelMessagesEffect) fails to propagate reasoning_content when the assistant message also contains tool_calls. This causes two failure modes:
- API rejection (with OpenRouter): Returns 400 with "The reasoning_content in the thinking mode must be passed back to the API."
- Silent loop exit (with openai compatible provider): The provider likely swallows the error or the model gets confused by the malformed request and returns
finish_reason: "stop", causing the session loop to exit prematurely.
Logs (silent failure)
timestamp=2026-07-07T05:48:34.603Z level=INFO message=loop step=5
timestamp=2026-07-07T05:48:34.679Z level=INFO message=stream model=deepseek-ai/DeepSeek-V4-Flash
timestamp=2026-07-07T05:48:36.279Z level=INFO message=loop step=6
timestamp=2026-07-07T05:48:36.302Z level=INFO message="exiting loop"
Only about 2 seconds between stream start and loop exit — the model likely received a malformed request and returned stop instead of continuing with tool calls.
Related Issues
- thinking is enabled but reasoning_content is missing in assistant tool call message at index ... #25758: "thinking is enabled but reasoning_content is missing in assistant tool call message" — same root cause, affects both DeepSeek and Kimi
- reasoning_content is missing when using DeepSeek V4 Flash in thinking #29618: "reasoning_content is missing when using DeepSeek V4 Flash in thinking" — specifically for DeepSeek V4 Flash/Pro
- fix(provider): skip empty reasoning_content to preserve KV cache hits #28352 (PR, unmerged): "fix(provider): skip empty reasoning_content to preserve KV cache hits" — partial fix for empty reasoning_content
Technical Analysis
The bug is in the OpenAI-compatible provider's doStream transform and the message conversion in lowerAssistantMessage. When the model returns a response with both reasoning_content and tool_calls:
- The response is correctly parsed and stored internally as
{type: "reasoning"}and{type: "tool-call"}content parts - When converting back to API format for the next request, the
reasoning_contentis extracted from the reasoning parts - But if there are also
tool_calls, the message ends up withcontent: null+tool_calls+reasoning_content— the interleaved normalization pipeline drops thereasoning_contentfield
The fix needs to ensure reasoning_content is always preserved on assistant messages that contain tool calls when interleaved mode is enabled. This is similar to the fix attempted in #28352 but needs to cover the case where reasoning_content is non-empty.
Workaround
Until fixed, users can disable thinking mode on the model or switch to a non-thinking variant.
Environment
- OpenCode: latest (1.17.14)
- Model: deepseek-ai/DeepSeek-V4-Flash (also affects DeepSeek V4 Pro, Kimi K2)
- Provider: OpenAI-compatible, interleaved: {field: "reasoning_content"}
- Mode: thinking enabled
Activity
github-actions commentedon Jul 7, 2026
This issue might be a duplicate of existing issues. Please check:
reasoning_contentin the thinking mode must be passed back to the API. #28974: Bad Request: Thereasoning_contentin the thinking mode must be passed back to the API (SiliconFlow/DeepSeek V4 Pro)Several of these cover the identical root cause across different providers. It may be worth consolidating into one of these existing threads.
4nur44g commentedon Aug 1, 2026
Adding V2 (opencode2) binary-level confirmation + two concrete code findings that pinpoint why this persists on the rewritten codebase.
Repro: opencode2
v0.0.0-next-16664, a Bifrost OpenAI-compatible proxy (@ai-sdk/openai-compatible, baseURLhttp://100.77.14.17:8080/v1), modeldeepseek/deepseek-v4-flashwith thinking. First turn OK; any subsequent turn after a tool-call assistant message →400 The reasoning_content in the thinking mode must be passed back to the API.Reproduces reliably in subagents (multi-turn tool path).Finding 1 —
message.native.openaiCompatibleappears to never be populated in V2.V2's
openai-chatadapterlowerAssistantMessagecomputesreasoning_contentfor tool-call-only assistant turns as:So when an assistant turn has tool calls and no stored reasoning part (e.g. the proxy didn't return
reasoning_contenton the tool-call response, or it wasn't round-tripped), it falls back tomessage.native?.openaiCompatible?.reasoning_content. From source-map inspection I could find no path in the V2 bundle that ever setsmessage.native.openaiCompatible(grepping all source maps foropenaiCompatible:as an object key returns nothing), so the fallback is alwaysundefined→ noreasoning_contentsent → DeepSeek 400. This matches the "zen/go proxy sometimes does not return reasoning_content on tool_calls responses" observation from #24722.Finding 2 —
compatibility.reasoningFieldis inert in V2.A commonly suggested workaround is
compatibility: { reasoningField: "reasoning_content" }on the model. In V2 this config is not read:reasoningFieldappears only in the adapter, where it readspart.providerMetadata?.openai?.reasoningField(metadata stamped on reasoning parts, not from modelcompatibility). The V1-compat config layer has zeroreasoningFieldreferences, so the model-levelcompatibility.reasoningFieldnever reaches the parts. Turns that do carry a reasoning part still sendreasoning_content(default field isreasoning_content), but tool-call-only turns have no reasoning part to carry it.Fix direction: V2's
openai-chatadapter needs to injectreasoning_content: ""on assistant tool-call messages for DeepSeek-V4 models when no reasoning part is present — mirroring@ai-sdk/deepseek'sreasoning_content: isDeepSeekV4 ? "" : void 0. The bundled@ai-sdk/deepseekpackage has this logic, but V2 routes@ai-sdk/openai-compatibleto its own adapter, not that package. Equivalently: populatemessage.native.openaiCompatible.reasoning_contentfrom the stored response so the existing fallback works. The stepcodex writeup (extendingisDeepSeekV4ModelRefto match across providers + a fallback wrapper for unowned providers) is the same shape of fix.Workaround being tested: pointing the Bifrost DeepSeek models at
npm: "@ai-sdk/deepseek"withbaseURLoverridden to the proxy, to see whether V2 routes that to the package's thinking wrapper instead of its own adapter. Will report back with the result.Happy to put together a PR for the adapter-side
""injection if that's the preferred direction.metrofun commentedon Aug 4, 2026
There’s a working alternate path for this while the OpenCode replay bug is unresolved.
I maintain Swobu, a local compatibility gateway. Swobu terminates the client-side protocol loop and sends the request to DeepSeek through its native Messages endpoint, so OpenCode does not need to reconstruct DeepSeek’s Chat
reasoning_contenthistory.I tested the full path with thinking enabled: OpenCode read files, edited the source, ran
go test, and continued successfully across multiple tool results.Install the current release:
curl -fsSL https://swobu.com/install.sh | sh swobuAdd a DeepSeek target using
DEEPSEEK_API_KEY, then configure OpenCode:{ "$schema": "https://opencode.ai/config.json", "model": "swobu/default", "provider": { "swobu": { "npm": "@ai-sdk/openai-compatible", "name": "Swobu", "options": { "baseURL": "http://127.0.0.1:7926/c/dev/v1" }, "models": { "default": { "name": "Default" } } } } }pragmaxim commentedon Aug 5, 2026
@rekram1-node it does not seem to be happening on 1.18.13, tried for a couple of times now and all sessions finished without interruption.