Repository navigation
Description
Bug Report
Description
Using DeepSeek models with thinking mode enabled (thinking: {type: "enabled"}), every API call after the first assistant response fails with:
The
reasoning_contentin the thinking mode must be passed back to the API.
The session becomes stuck — you can't continue the conversation beyond the first assistant reply.
System Info
- OpenCode version: 1.14.19 (running from source, branch anomalyco/opencode)
- OS: Windows 11
- Provider:
@ai-sdk/openai-compatible(bundled) - DeepSeek API:
api.deepseek.com, thinking mode enabled
Model Configuration
"deepseek-v4-flash": {
"limit": { "context": 200000, "output": 393216 },
"options": { "thinking": { "type": "enabled" } }
}Also reproduced with deepseek-reasoner (R1) when conversation history contains assistant messages that were stored before thinking mode was enabled.
Steps to Reproduce
- Configure a DeepSeek V4 model (e.g.
deepseek-v4-flash) inopencode.jsonwiththinking: {type: "enabled"}in options - Start a new session with this model
- Send a message — the first assistant reply works fine
- Send a follow-up message — API returns 400 with the reasoning_content error
- The session is now stuck; every retry produces the same error
Expected Behavior
Conversation should continue normally across multiple turns when using thinking mode models.
Actual Behavior
Second API call fails with reasoning_content must be passed back. The @ai-sdk/openai-compatible provider's convertToOpenAICompatibleChatMessages() function strips reasoning_content from assistant messages that don't have explicit reasoning parts in their content. On history replay (second turn), these messages lack the reasoning_content field that DeepSeek's API requires for all assistant messages in thinking mode.
Workaround
Add interleaved: { field: "reasoning_content" } to the model config in opencode.json, which causes the existing normalizeMessages() code to inject reasoning_content into providerOptions. But this only partially helps — messages replayed from DB history still get skipped by the narrow condition in normalizeMessages().
Related Issues
- Missing reasoning_content field in the assistant message for DeepSeek API implementation #17523 — same root cause (reasoning_content stripped on replay)
- reasoning_content stripped from assistant messages on replay, causing KV cache invalidation on local inference #19081 — reasoning_content lost during message replay (overlapping fix area)
- The problem with deepseek-reasoner in OpenCode #8934 — general DeepSeek-reasoner compatibility issues
Activity
[-]fix: DeepSeek thinking mode reasoning_content must be passed back to API[/-][+]DeepSeek thinking mode: reasoning_content must be passed back to API on conversation continuation[/+]github-actions commentedon Apr 24, 2026
Thanks for updating the issue! It now follows the contributing guidelines and looks great. We appreciate the clear description, system info, and reproduction steps.
bilbillm commentedon Apr 24, 2026
Same issue here with deepseek-v4-flash + thinking mode. The error "The reasoning_content in the thinking mode must be passed back to the API" occurs consistently on the second turn — the first assistant reply always succeeds, then every follow-up message fails with a 400 error.
From my investigation, the root cause is:
easoning_content field (even if empty string ""), especially when tool calls are involved.
easoning_content from assistant messages on history replay, so when the second user message is sent, the replayed assistant messages from turn 1 lack the required field.
A temporary workaround I found: the error doesn't occur if no tool calls are made in the first turn (simple chat-only messages work). But as soon as tool calls happen (file read/write, etc.), the conversation deadlocks.
Hope this gets prioritized — thinking mode is unusable with agent workflows right now.
fkyah3 commentedon Apr 24, 2026
Thanks for confirming — same findings on my end. The tool-call trigger matches what I traced: tool-call messages replayed from history lack
easoning_content\ because the inner condition in
ormalizeMessages()\ (transform.ts) skipped them.
I have a working fix that addresses both the entry condition and the narrow inner skip — committed at \�5b6ad05d\ on branch \ix/permission-reasoning-truncation\ of my fork (\kyah3/opencode-fkyah3). The key changes:
easoning_content\ injected (empty string if no reasoning text), including string-content messages from old DB format
So far stable across long conversations with heavy tool use. Hope this helps while a proper upstream fix lands.
hypernewbie commentedon Apr 24, 2026
same here
fkyah3 commentedon Apr 24, 2026
If you want to try the fix now, it's on my fork branch fix/permission-reasoning-truncation at fkyah3/opencode-fkyah3. Commit b5b6ad05d in packages/opencode/src/provider/transform.ts — removes the narrow condition that skipped history-replayed assistant messages.
quake commentedon Apr 24, 2026
I tried your fix, it works smoothly, thanks!
bilbillm commentedon Apr 24, 2026
老哥我在b站刷到您的视频了,这pr #17529就有人发了愣是没人管,您方便的话能提交一个修复的pr嘛,我看到一堆类似的issue了现在交个pr上去大概率人家会看
fkyah3 commentedon Apr 24, 2026
PR #24150 has been submitted. It covers both the interleaved and non-interleaved paths (unlike #24146 which only handles interleaved models).
In short: the if (reasoningText) guard was the immediate cause, but the interleaved-only outer condition and hardcoded provider key were also blocking DeepSeek from getting the fix.
The fix is also available on our fork branch ix/permission-reasoning-truncation if you need it before upstream merges.
PR #24150 已提交。相比 #24146(只修 interleaved 路径),我们的修复合盖了非 interleaved 场景(如 DeepSeek 通过 @ai-sdk/openai-compatible)。如果 upstream 合并前急用,可以直接切到 fork 的 ix/permission-reasoning-truncation 分支。
hammerhoundai commentedon Apr 24, 2026
In my experience using the below config, it works fine in multiple turns unlike the issue raised by others here. However if I use a different provider and then switch mid-session to deepseek then I get the same error as mentioned here.
fkyah3 commentedon Apr 24, 2026
This should be covered by PR #24150. The root cause is the same — the API requires
easoning_content on every assistant message, and old messages from a different provider won't have it.
What the fix does:
easoning_content (empty string for legacy messages) on ALL assistant messages
One thing I'm less sure about for your setup: you're using @ai-sdk/anthropic as the transport. The providerOptionsKey() logic returns "anthropic" as the key, and defaults the field to "reasoning_content". So the empty value goes into providerOptions.anthropic.reasoning_content. I haven't tested whether the DeepSeek anthropic-compatible endpoint expects it under the "anthropic" namespace or the "openaiCompatible" namespace. If it only reads from openaiCompatible, you might still hit the issue. Would be good to know if your config has interleaved configured for the model — that would use the correct field name.
(In the standard @ai-sdk/openai-compatible path this is well-tested and works.)
luisifa commentedon Apr 24, 2026
社区已经有现成的修复插件:opencode-deepseek-thinking-fix
cd ~/.config/opencode
bun add opencode-deepseek-thinking-fix
(或者用 npm: npm i opencode-deepseek-thinking-fix)
然后在 ~/.config/opencode/opencode.json 中添加插件配置:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-deepseek-thinking-fix"]
}
fkyah3 commentedon Apr 24, 2026
Thanks @luisifa for the pointer! I took a look at the plugin source.
The plugin wraps the provider fetch layer, caching and re-injecting actual thinking blocks (with signatures) across turns — that's a fundamentally different approach from our fix in
transform.ts(which injects emptyreasoning_content: ""unconditionally at the message serialization layer).They're complementary:
reasoning_content), works at the message normalization layerGoing to test installing it today. If it works well, having both layers active means the plugin handles the rich thinking preservation and our fix acts as the fallback safeguard.
One question: does this plugin handle the case where history messages are replayed from DB (e.g. after Magic Context compression)? That's the specific path where we found
reasoning_contentgets dropped in DeepSeek's thinking mode — the fingerprint-based cache approach might miss on cold replay.fkyah3 commentedon Apr 24, 2026
@LynnGuo666 @luisifa I tracked down the root cause. The plugin's design is correct for programmatic use, but there's an interaction with OpenCode's legacy plugin loader that causes the crash.
Root cause chain (
src/index.tsline 317):The plugin exports
ThinkingCache,fingerprint,assistantTurnFingerprintas named exports — this is legitimate for the programmatic API documented in the README. However, OpenCode'sgetLegacyPlugins()inplugin/index.tsiterates ALL named exports and treats every function/class as a server plugin:This causes two failures:
ThinkingCache(class) →Cannot call a class constructor without |new|assistantTurnFingerprint→messages.slice is not a function(called with PluginInput instead of messages array)Two layers of the problem:
Layer 1 —
readV1Plugindoesn't handle function default exports:The plugin uses the legacy V1 pattern where
export defaultis an async function returning{ config, "chat.params" }. This IS a valid pattern, butreadV1Plugin(shared.ts:272) checksisRecord(mod.default)which returnsfalsefor functions (typeof function !== "object"). In "detect" mode, it silently returns undefined, falling through togetLegacyPlugins.Layer 2 —
getLegacyPluginscan't distinguish classes from functions:isServerPluginjust checkstypeof === "function", which is true for both regular functions and ES6 classes. A class thrown when called withoutnew.Suggested fix for the plugin (easiest):
Restructure to V1 plugin pattern with explicit server export, avoiding the legacy loader entirely:
This way
readV1Pluginpicks it up directly (mod.defaultis an object withidandserver) andgetLegacyPluginsis never reached.Our temporary workaround (for immediate use):
We removed the utility re-exports from
dist/index.js:After this change the plugin loads and works correctly on our fork.
simonlearnscoding commentedon Apr 24, 2026
This is not fixed for me. Using
@ai-sdk/openai-compatible+thinking: {type: "enabled"}:✅ First message works
❌ Second message fails: "reasoning_content must be passed back"
PR #24146 only fixes
interleavedmodels. PR #24150 fixes both — please merge that instead./cc @rekram1-node
rekram1-node commentedon Apr 24, 2026
so you have a custom model definition? Is that the issue ur having?
rekram1-node commentedon Apr 24, 2026
deepseek is an interleaved reasoning model so yeah the config should prolly not be explicit but you can just configure the model to forward the reasoning correctly in your opencode.json
fkyah3 commentedon Apr 24, 2026
That's the nuance — when using the built-in DeepSeek provider (which sets
capabilities.interleaved = true), PR #24146's fix works fine. But when using@ai-sdk/openai-compatibleas a generic adapter, the model definition lacks bothinterleavedandreasoningcapability flags, sonormalizeMessages()never enters thereasoning_contentinjection branch.The user's opencode.json model config may be correct, but the AI SDK's
convertToOpenAICompatibleChatMessages()doesn't addreasoning_contentto history messages (it only emitsreasoning_contentwhenreasoning.length > 0at the SDK layer). So even with a proper model definition, the replay path still drops it.Our fix (PR #24150 / commit b5b6ad05d on our fork) approaches this at
normalizeMessages()level — unconditional injection ofreasoning_content: ""for any model that has eitherinterleaved,reasoningcapability, or any existing message with reasoning parts in the conversation. This covers the generic adapter case without requiring model-config-level changes.rekram1-node commentedon Apr 24, 2026
Just put something like this in your opencode.json and it will behave as expected:
rekram1-node commentedon Apr 24, 2026
If someone wants to open up a simple clean pr to make it happen for deepseek by default for openai compatible provider thats fine too
fix(provider): complete DeepSeek reasoning_content round-trip for mul…
dante4rt commentedon May 13, 2026
I’m seeing the same issue, but I’m not using DeepSeek. I’m using
mimo-v.2.5-pro.md-rifatkhan commentedon Aug 5, 2026
same issue
fix(llm): a refusal for missing reasoning_content is answered with an…
134579 commentedon Oct 22, 2026
same issue when using vscode copilot + deepseek flash v4.1