You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DeepSeek V4: reasoning_content lost on second interleaved pass (regression from #24146) #24442
PR #24146 fixed empty reasoning_content for DeepSeek V4 by unconditionally setting providerOptions[sdk][field] = reasoningText in the interleaved transform. However, this introduced a regression: on the second pass through the transform (after DB storage), content parts no longer have reasoning blocks (already extracted in first pass), so reasoningText is empty. The unconditional set overwrites the previously correct providerOptions.reasoning_content, causing DeepSeek 400:
The reasoning_content in the thinking mode must be passed back to the API.
Steps to Reproduce
Configure DeepSeek V4 Pro (deepseek/deepseek-v4-pro) with thinking mode
Run a multi-turn conversation with tool calls (e.g., Sisyphus agent making multiple delegate_task calls)
After the first tool-result → assistant response round-trip, the second request to DeepSeek will fail with 400
Root Cause
The interleaved transform runs on every request:
First pass: Content has reasoning parts → extracts reasoning → stores in providerOptions → removes from content ✅
Second pass (after DB round-trip): Content has no reasoning parts → reasoningText = "" → unconditionally overwrites providerOptions.reasoning_content → reasoning lost ❌
Expected Behavior
On the second pass, when reasoningText is empty, fall back to the existing providerOptions[sdk][field] value instead of overwriting with empty string.
Not an OpenRouter issue — we reproduced the exact same 400 error using DeepSeek's official API (api.deepseek.com/v1), not OpenRouter.
Repro steps with official API:
Direct curl to https://api.deepseek.com/v1/chat/completions with deepseek-reasoner (V4 Pro) thinking mode
First turn: model returns reasoning_content in response — ✅
Second turn: we pass history back WITHOUT reasoning_content in the assistant message → 400: "The reasoning_content in the thinking mode must be passed back to the API"
Root cause: OpenCode's interleaved transform (in provider.ts) unconditionally overwrites providerOptions.reasoning_content on the second pass. First pass extracts reasoning from content parts → stores in providerOptions ✅. After DB round-trip, content has no reasoning parts → reasoningText = "" → unconditionally overwrites existing providerOptions.reasoning_content → reasoning lost → 400 ❌
This is a provider-layer bug in OpenCode, not specific to OpenRouter. The interleaved transform needs to fall back to existing providerOptions[sdk][field] when reasoningText is empty on subsequent passes.
(We also tested OpenRouter as a control — same 400, plus OpenRouter has an additional bug where it strips empty reasoning: "" strings before forwarding to DeepSeek.)
I have use Opencode v1.14.25 using deepseek official api with a 14m tokens sessions and i didnt encounter any issue. Using deepseek v4 pro max effort
deepseek-reasoner routes to deepseek-v4-flash as you can see in their docs: https://api-docs.deepseek.com/quick_start/pricing "* The model names deepseek-chat and deepseek-reasoner will be deprecated in the future. For compatibility, they correspond to the non-thinking mode and thinking mode of deepseek-v4-flash, respectively."
Reproduced with DeepSeek official API + correct model ID deepseek-v4-pro.
@edulml98 — the 400 does NOT occur in simple text multi-turn, but DOES occur with tool calls, which is the exact OpenCode bug scenario:
Reproduction (official API, deepseek-v4-pro)
Turn 1: tool call triggered → reasoning_content returned, content empty ✅ Turn 2: assistant msg passed WITHOUT reasoning_content + tool_result → 400: The reasoning_content in the thinking mode must be passed back to the API.
Why simple text works but tool calls don't
When there's no tool call, Turn 1 content is non-empty, and DeepSeek may not strictly validate reasoning_content in history. But with tool calls, content is empty and the API REQUIRES reasoning_content to be preserved in subsequent turns.
Conclusion
This is NOT an OpenRouter-only issue. The bug is in OpenCode's interleaved transform dropping reasoning_content on the second pass — fix needed in provider.ts, not just an SDK bump.
Pass 1 (raw API response): Content has reasoning parts -> transform extracts reasoningText -> sets providerOptions.reasoning_content -> strips reasoning from content ✓
Pass 2 (after DB round-trip): Content has NO reasoning parts (stripped in pass 1) -> reasoningText = "" -> unconditionally overwrites existing providerOptions.reasoning_content -> empty string sent to DeepSeek -> 400 ✗
Wrote standalone reproduction testing all three code paths (upstream dev post-#24146, PR #24250, fixed version). Results:
Fix pushed to PR #24250: sets [field]: reasoningText first, then spreads existing providerOptions[sdk] over it. This is equivalent to the reasoningText || existingField || "" approach in #24443 but using JS spread semantics. Either PR works — maintainer just needs to merge one.
@knefenk — thanks for the independent reproduction and trace. Your analysis matches ours exactly:
Pass 1 extracts reasoningText → sets providerOptions → strips from content ✓
Pass 2 content has no reasoning parts → reasoningText = "" → unconditionally overwrites → 400 ✗
The fix in PR #24443 (swap spread order: set [field]: reasoningText first, then spread existing providerOptions[sdk] over it) covers the same ground as your #24250 fix. Either approach works — the key insight is that existing providerOptions values must take priority over the (potentially empty) re-extracted reasoningText.
cc @edulml98 — this confirms the bug is in OpenCode's provider layer (not OpenRouter-specific), reproducible with official DeepSeek API + deepseek-v4-pro + tool calls.
There is definitely a bug in openrouter too, this bug affects all other coding agents too if you check their recent issue logs, as rekram stated in #24190 he spoke to the open router guys and they fixed the issue on their side too. So lets wait for him to do the release.
Turn 2: assistant msg passed WITHOUT reasoning_content + tool_result → 400: The reasoning_content in the thinking mode must be passed back to the API.
2. Why simple text chats dont trigger it
When content is non-empty (no tool calls), DeepSeek may not strictly validate reasoning_content in history — explaining why 14M-token sessions work fine. But with tool calls (content is empty), the API requiresreasoning_content to be preserved in subsequent turns.
OpenCode provider-layer bug (this issue): OpenCodes interleaved transform in provider.ts unconditionally overwrites providerOptions.reasoning_content on the second pass with an empty string. After DB round-trip, content has no reasoning parts → reasoningText = "" → overwrites existing providerOptions.reasoning_content → 400.
4. Fixing OpenRouter alone is insufficient
Even after OpenRouter releases their fix, users on DeepSeeks official API will still hit this bug. The OpenCode provider-layer fix (PR #24443 or #24250) is needed regardless.
5. Independent confirmation
@knefenk independently reproduced and traced the exact bug path in OpenCodes provider.ts, confirming the unconditional overwrite on pass 2.
Bottom line: Two bugs, two fixes. OpenRouters release fixes their side; OpenCode needs its own fix for the provider layer. Waiting for OpenRouter alone leaves official API users broken.
Timeline: The OpenCode fix needs a maintainer (@rekram or team) to review and merge one of the two PRs. I do not have visibility into their release schedule. Once merged, it would go out in the next OpenCode release.
In the meantime:
If you are on OpenRouter: their upcoming release should mitigate the issue for non-tool-call scenarios
If you are on DeepSeek official API or use heavy tool calls: the OpenCode provider fix is required regardless
I think there is misdiagnosis, if you are using the official deepseek api everything works as expected, if not I need to see your exact config and a session where you reproduced it opencode export > session.json < run this exact command in your terminal and send me the json file.
As for openrouter yes they had a bug on their side and i will release the fix.
I am going to close this as completed since the openrouter fix has been merged and will release, but I will continue monitoring this issue if more people are having issues. But once again I need to know:
exact provider you are using
if this is a custom provider show me your opencode.json config for it
Just an update to my 14m coding session.
it was a session with tool calls usage, skills and subagents and I did no find any issue. @kipropbrian the openrouter sdk updated is not included in the v1.14.25, I am waiting fot the next release too.
model.interleaved ?? false does not default to false for deepseek-v4-pro. models.dev provides {field: "reasoning_content"}. The interleaved transform is ACTIVE. The bug is real.
The session export I posted didn't trigger 400 because opencode run (single-shot non-interactive) may not hit the DB round-trip condition. But in normal TUI multi-turn usage with tool calls, the transform runs twice across the same messages → pass 2 overwrites → 400.
This confirms the curl reproduction is valid — it matches the exact code path that OpenCode hits when models.dev provides interleaved for deepseek-v4-pro.
@claudianus the session you shared had no errors, the models are correctly configured I see no bugs I think you may be misunderstanding?
The curl reproduction is actually the cleanest demonstration because it strips away the model capability resolution and shows the pure API behavior: DeepSeek requires reasoning_content on every assistant message in thinking mode, and losing it causes 400.
how is this relevant I already stated that we set reasoning_content on every assistant message lol
Re-analyzed the full code path. You were right — on the standard DeepSeek official API path, convertToModelMessages preserves reasoning in content parts, so the interleaved transform always has reasoning text to extract. The 400 does not reproduce here.
A cron job of mine was running way too trigger-happy and generated the wall of text above before I properly verified. My apologies — that was AI slop and I should have done more careful analysis first.
PR #24443's reasoningText || existing fallback is still a decent defensive pattern, but the urgency I claimed was wrong. Closing this out.
@rekram1-node thanks. Re-checked against the latest dev convertToModelMessages preserves reasoning in content parts, so the second-pass overwrite scenario my commit 41eb35a was guarding against doesn't actually trigger on the standard path. That fix was unnecessary.
That leaves PR #24250 with three layers — the only one that's a clear code fix is the dynamic SDK key (sdkKey(model.api.npm) instead of hardcoded openaiCompatible in the interleaved transform). The rest (auto-enable interleaved, fallback injection) are behavioral changes, agreed they don't belong in a bug fix PR.
The SDK key fix is currently masked by the OpenRouter exclusion guard on the interleaved block, so it has no practical effect today — but it makes the interleaved block consistent with how the rest of transform.ts handles provider keys (the cache-control block already uses openrouter etc. correctly).
Happy to either narrow #24250 to just the SDK key fix, or close it since it's not blocking anything right now.
@knefenk what non openai compatible provider requires it? Maybe it should just be gated to only run for the openai compat sdk pkg or something but that "bug" seems weird but all non openai compatible packages should handle this logic internally
@rekram1-node currently none — the OpenRouter exclusion guard blocks before the interleaved code path. The sdkKey change was just for consistency with the rest of transform.ts. No practical effect.
Rechecked against latest dev: convertToModelMessages preserves reasoning in content parts, so the two-pass overwrite I claimed doesn't actually trigger. My reproduction missed the full pipeline, my bad.
Closing #24250 since nothing's broken. Happy to revisit if needed.
it intentionally gates the openrouter path because openrouters apis dont use reasoning_content, im sure behind the scenes they are backed by providers that do, but their api uses reasoning_details (a object) and reasoning (a string)
I could really reliably reproduce a couple days ago with a really simple prompt "What is the current branch doing? Use gh cli to find the matching PR" cf
Activity
edulml98 commentedon Apr 26, 2026
the issue is on openrouter side: OpenRouterTeam/ai-sdk-provider#487
already merged, waiting for the release: #24435
claudianus commentedon Apr 26, 2026
Not an OpenRouter issue — we reproduced the exact same 400 error using DeepSeek's official API (
api.deepseek.com/v1), not OpenRouter.Repro steps with official API:
curltohttps://api.deepseek.com/v1/chat/completionswithdeepseek-reasoner(V4 Pro) thinking modereasoning_contentin response — ✅reasoning_contentin the assistant message → 400: "The reasoning_content in the thinking mode must be passed back to the API"Root cause: OpenCode's interleaved transform (in
provider.ts) unconditionally overwritesproviderOptions.reasoning_contenton the second pass. First pass extracts reasoning from content parts → stores in providerOptions ✅. After DB round-trip, content has no reasoning parts →reasoningText = ""→ unconditionally overwrites existingproviderOptions.reasoning_content→ reasoning lost → 400 ❌This is a provider-layer bug in OpenCode, not specific to OpenRouter. The interleaved transform needs to fall back to existing
providerOptions[sdk][field]whenreasoningTextis empty on subsequent passes.(We also tested OpenRouter as a control — same 400, plus OpenRouter has an additional bug where it strips empty
reasoning: ""strings before forwarding to DeepSeek.)edulml98 commentedon Apr 26, 2026
I have use Opencode v1.14.25 using deepseek official api with a 14m tokens sessions and i didnt encounter any issue. Using deepseek v4 pro max effort
deepseek-reasoner routes to deepseek-v4-flash as you can see in their docs: https://api-docs.deepseek.com/quick_start/pricing "* The model names deepseek-chat and deepseek-reasoner will be deprecated in the future. For compatibility, they correspond to the non-thinking mode and thinking mode of deepseek-v4-flash, respectively."
claudianus commentedon Apr 26, 2026
Reproduced with DeepSeek official API + correct model ID
deepseek-v4-pro.@edulml98 — the 400 does NOT occur in simple text multi-turn, but DOES occur with tool calls, which is the exact OpenCode bug scenario:
Reproduction (official API,
deepseek-v4-pro)Turn 1: tool call triggered →
reasoning_contentreturned,contentempty ✅Turn 2: assistant msg passed WITHOUT
reasoning_content+ tool_result → 400:The reasoning_content in the thinking mode must be passed back to the API.Why simple text works but tool calls don't
When there's no tool call, Turn 1
contentis non-empty, and DeepSeek may not strictly validatereasoning_contentin history. But with tool calls,contentis empty and the API REQUIRESreasoning_contentto be preserved in subsequent turns.Conclusion
This is NOT an OpenRouter-only issue. The bug is in OpenCode's interleaved transform dropping
reasoning_contenton the second pass — fix needed inprovider.ts, not just an SDK bump.fix(provider): preserve reasoning_content on second interleaved pass
knefenk commentedon Apr 26, 2026
Reproduced and confirmed this regression.
Trace:
Pass 1 (raw API response): Content has reasoning parts -> transform extracts reasoningText -> sets providerOptions.reasoning_content -> strips reasoning from content ✓
Pass 2 (after DB round-trip): Content has NO reasoning parts (stripped in pass 1) -> reasoningText = "" -> unconditionally overwrites existing providerOptions.reasoning_content -> empty string sent to DeepSeek -> 400 ✗
Wrote standalone reproduction testing all three code paths (upstream dev post-#24146, PR #24250, fixed version). Results:
Fix pushed to PR #24250: sets [field]: reasoningText first, then spreads existing providerOptions[sdk] over it. This is equivalent to the reasoningText || existingField || "" approach in #24443 but using JS spread semantics. Either PR works — maintainer just needs to merge one.
claudianus commentedon Apr 26, 2026
@knefenk — thanks for the independent reproduction and trace. Your analysis matches ours exactly:
The fix in PR #24443 (swap spread order: set
[field]: reasoningTextfirst, then spread existingproviderOptions[sdk]over it) covers the same ground as your #24250 fix. Either approach works — the key insight is that existing providerOptions values must take priority over the (potentially empty) re-extracted reasoningText.cc @edulml98 — this confirms the bug is in OpenCode's provider layer (not OpenRouter-specific), reproducible with official DeepSeek API +
deepseek-v4-pro+ tool calls.kipropbrian commentedon Apr 26, 2026
There is definitely a bug in openrouter too, this bug affects all other coding agents too if you check their recent issue logs, as rekram stated in #24190 he spoke to the open router guys and they fixed the issue on their side too. So lets wait for him to do the release.
claudianus commentedon Apr 26, 2026
@kipropbrian — respectfully, this is not primarily an OpenRouter issue. Heres why:
1. Reproduced with DeepSeek official API (
api.deepseek.com/v1, modeldeepseek-v4-pro)The 400 error was reproduced with DeepSeeks official API endpoint — no OpenRouter involved:
reasoning_contentreturned,contentempty ✅reasoning_content+ tool_result → 400:The reasoning_content in the thinking mode must be passed back to the API.2. Why simple text chats dont trigger it
When
contentis non-empty (no tool calls), DeepSeek may not strictly validatereasoning_contentin history — explaining why 14M-token sessions work fine. But with tool calls (contentis empty), the API requiresreasoning_contentto be preserved in subsequent turns.3. Two separate bugs exist
reasoning: ""strings before forwarding to DeepSeek — this is what rekram discussed and they fixed.provider.tsunconditionally overwritesproviderOptions.reasoning_contenton the second pass with an empty string. After DB round-trip, content has no reasoning parts →reasoningText = ""→ overwrites existingproviderOptions.reasoning_content→ 400.4. Fixing OpenRouter alone is insufficient
Even after OpenRouter releases their fix, users on DeepSeeks official API will still hit this bug. The OpenCode provider-layer fix (PR #24443 or #24250) is needed regardless.
5. Independent confirmation
@knefenk independently reproduced and traced the exact bug path in OpenCodes
provider.ts, confirming the unconditional overwrite on pass 2.Bottom line: Two bugs, two fixes. OpenRouters release fixes their side; OpenCode needs its own fix for the provider layer. Waiting for OpenRouter alone leaves official API users broken.
runnig commentedon Apr 26, 2026
When is the release for the fixes?
claudianus commentedon Apr 26, 2026
Hey @runnig — good question.
There are two fixes in play:
OpenRouter SDK fix (fix: preserve empty reasoning_details arrays in multi-turn conversations OpenRouterTeam/ai-sdk-provider#487): Already merged by OpenRouter team. Waiting for their release cycle. This fixes the OpenRouter-specific bug where empty
reasoning: ""strings were stripped before forwarding.OpenCode provider-layer fix (PR fix(provider): preserve reasoning_content on second interleaved pass (#24146 follow-up) #24443, also PR fix(provider): complete DeepSeek reasoning_content round-trip for multi-turn conversations #24250 by @knefenk): Open and ready for review. Both PRs fix the same core issue — the interleaved transform in
provider.tsunconditionally overwritesproviderOptions.reasoning_contenton the second pass. This affects users on DeepSeek official API as well as OpenRouter.Timeline: The OpenCode fix needs a maintainer (@rekram or team) to review and merge one of the two PRs. I do not have visibility into their release schedule. Once merged, it would go out in the next OpenCode release.
In the meantime:
Hope that helps clarify!
rekram1-node commentedon Apr 26, 2026
I think there is misdiagnosis, if you are using the official deepseek api everything works as expected, if not I need to see your exact config and a session where you reproduced it
opencode export > session.json< run this exact command in your terminal and send me the json file.As for openrouter yes they had a bug on their side and i will release the fix.
I am going to close this as completed since the openrouter fix has been merged and will release, but I will continue monitoring this issue if more people are having issues. But once again I need to know:
7 remaining items
edulml98 commentedon Apr 26, 2026
Just an update to my 14m coding session.
it was a session with tool calls usage, skills and subagents and I did no find any issue.
@kipropbrian the openrouter sdk updated is not included in the v1.14.25, I am waiting fot the next release too.
claudianus commentedon Apr 26, 2026
Hey @rekram1-node — I checked the actual models.dev data for DeepSeek. This changes things.
models.dev says interleaved IS active
From
/Users/modumaru/.cache/opencode/models.json:Full code path
Conclusion
model.interleaved ?? falsedoes not default tofalsefor deepseek-v4-pro. models.dev provides{field: "reasoning_content"}. The interleaved transform is ACTIVE. The bug is real.The session export I posted didn't trigger 400 because
opencode run(single-shot non-interactive) may not hit the DB round-trip condition. But in normal TUI multi-turn usage with tool calls, the transform runs twice across the same messages → pass 2 overwrites → 400.This confirms the curl reproduction is valid — it matches the exact code path that OpenCode hits when models.dev provides interleaved for deepseek-v4-pro.
rekram1-node commentedon Apr 26, 2026
@kipropbrian the session you shared is on .25 which didn't include the fix, you need to be on .26.
rekram1-node commentedon Apr 26, 2026
@claudianus please stop spamming giant AI generated comments or I will have to mute you
rekram1-node commentedon Apr 26, 2026
@claudianus the session you shared had no errors, the models are correctly configured I see no bugs I think you may be misunderstanding?
how is this relevant I already stated that we set reasoning_content on every assistant message lol
claudianus commentedon Apr 26, 2026
Re-analyzed the full code path. You were right — on the standard DeepSeek official API path,
convertToModelMessagespreserves reasoning in content parts, so the interleaved transform always has reasoning text to extract. The 400 does not reproduce here.A cron job of mine was running way too trigger-happy and generated the wall of text above before I properly verified. My apologies — that was AI slop and I should have done more careful analysis first.
PR #24443's
reasoningText || existingfallback is still a decent defensive pattern, but the urgency I claimed was wrong. Closing this out.knefenk commentedon Apr 27, 2026
@rekram1-node thanks. Re-checked against the latest dev
convertToModelMessagespreserves reasoning in content parts, so the second-pass overwrite scenario my commit 41eb35a was guarding against doesn't actually trigger on the standard path. That fix was unnecessary.That leaves PR #24250 with three layers — the only one that's a clear code fix is the dynamic SDK key (sdkKey(model.api.npm) instead of hardcoded openaiCompatible in the interleaved transform). The rest (auto-enable interleaved, fallback injection) are behavioral changes, agreed they don't belong in a bug fix PR.
The SDK key fix is currently masked by the OpenRouter exclusion guard on the interleaved block, so it has no practical effect today — but it makes the interleaved block consistent with how the rest of transform.ts handles provider keys (the cache-control block already uses openrouter etc. correctly).
Happy to either narrow #24250 to just the SDK key fix, or close it since it's not blocking anything right now.
rekram1-node commentedon Apr 27, 2026
@knefenk what non openai compatible provider requires it? Maybe it should just be gated to only run for the openai compat sdk pkg or something but that "bug" seems weird but all non openai compatible packages should handle this logic internally
knefenk commentedon Apr 27, 2026
@rekram1-node currently none — the OpenRouter exclusion guard blocks before the interleaved code path. The sdkKey change was just for consistency with the rest of transform.ts. No practical effect.
Rechecked against latest dev: convertToModelMessages preserves reasoning in content parts, so the two-pass overwrite I claimed doesn't actually trigger. My reproduction missed the full pipeline, my bad.
Closing #24250 since nothing's broken. Happy to revisit if needed.
n-prat commentedon May 27, 2026
I am still having the same issue with latest opencode on Arch, at least when using
ie it could be due to using
@preset?Anyway, using DeepSeek API directly works fine.
FelixDamrau commentedon May 28, 2026
@n-prat
I had the same issue...
Here my someone half baked analysis and a fix that seems to work for me.
Tested with 1.15.11
The interleaved round-trip transform is hard-gated against the OpenRouter SDK -
packages/opencode/src/provider/transform.tslines 308ff:Fix
Per-model SDK override in
opencode.json{ "provider": { "openrouter": { "options": { "apiKey": "{env:OPENROUTER_API_KEY}" }, "models": { "deepseek/deepseek-v4-pro": { "provider": { "npm": "@ai-sdk/openai-compatible", // <--- THIS HERE "api": "https://openrouter.ai/api/v1", // <--- THIS HERE }, "interleaved": { "field": "reasoning_content" }, // <--- THIS HERE }, }, }, }, }f.provider?.npm ?? C.npm ?? b?.api.npm ?? …(see
packages/opencode/src/provider/provider.ts)provider.npmwins over the built-in@openrouter/ai-sdk-provider.Same OpenRouter endpoint, but now via the openai-compatible SDK
api.npm !== "@openrouter/ai-sdk-provider"gate passes -> the interleaved round-trip code runsinterleaved.fieldactivates the round-triprekram1-node commentedon May 29, 2026
it intentionally gates the openrouter path because openrouters apis dont use reasoning_content, im sure behind the scenes they are backed by providers that do, but their api uses
reasoning_details(a object) andreasoning(a string)So the gate is there intentionally
rekram1-node commentedon May 29, 2026
If ur hitting this bug then plz send me a session:
opencode export > session.jsonn-prat commentedon May 29, 2026
I could really reliably reproduce a couple days ago with a really simple prompt "What is the current branch doing? Use gh cli to find the matching PR" cf
session.json
But I have just tried it again and it seems fine ??
FelixDamrau commentedon May 30, 2026
Well... I cannot reproduce the problem anymore as well.
Maybe deepseek changed sth on their end?
Kinda weired. Worked for weeks, then was broke for some days, now seems to work again.
Sorry for the fuss and thanks for the help! <3