npm install opencode-thinking-fixFix for the
reasoning_content400 error that kills multi-turn conversations with DeepSeek, Kimi, GLM, MiMo, and MiniMax-M3 in OpenCode.Zero config. Install via
Ctrl+P, restart OpenCode, done. The plugin auto-detects reasoning models and only patches when needed.Docs: OpenCode Plugins
Your AI has a secret notebook.
When DeepSeek or Kimi answers you, it scribbles notes first. "Let me think... the user wants a login page... I should use React Hook Form... check the API docs..." These notes are reasoning_content. You never see them. But the AI needs them.
OpenCode throws the notebook away. Next turn, the AI reaches for it — but OpenCode already handed the request to the API without it. The API returns HTTP 400. Or worse, OpenCode hands back a blank notebook. The AI doesn't crash, but it forgot everything it was thinking. That is why your AI seems dumber on turn 2. It's not dumber. It just lost its notes.
Without the plugin: "Build me a login page." → AI builds it. "Now add password reset." → 400 error, conversation dead.
With the plugin: "Build me a login page." → AI builds it, notes saved. "Now add password reset." → AI reads its notes: "I used React Hook Form for login, I'll extend that for password reset." → Works.
This is an OpenCode plugin. Install it inside OpenCode, no terminal needed.
| Method | Command | Best for |
|---|---|---|
| TUI | Ctrl+P → type install plugin → opencode-thinking-fix |
First-time users |
| CLI | opencode plugin opencode-thinking-fix |
Scripting |
| Manual | Add "plugin": ["opencode-thinking-fix"] to opencode.json |
Version pinning |
- Press
Ctrl+Pto open the command palette. - Type
install pluginand pressEnter. - Press
Tabto switch the install scope to Global (recommended, works across all projects). - Type
opencode-thinking-fix. - Press
Enter. Restart OpenCode.
Check ~/.local/share/opencode/thinking-fix.log for plugin_loaded. See Is it working?.
opencode plugin opencode-thinking-fixFor a specific version:
opencode plugin opencode-thinking-fix@2.0.0Restart OpenCode after installing.
{
"plugin": ["opencode-thinking-fix"]
}Config file location:
- Linux/macOS:
~/.config/opencode/opencode.json(global) or.opencode/opencode.json(project) - Windows:
%APPDATA%/OpenCode/opencode.json(global) or.opencode/opencode.json(project)
Restart OpenCode after adding. Check ~/.local/share/opencode/thinking-fix.log for plugin_loaded. See Is it working?.
See also: OpenCode plugin docs
On Windows? See Windows notes for PowerShell commands, NSSM service setup, and config paths.
- What problem this fixes
- Option 1: Plugin (stops the crashes)
- Option 2: Proxy (replays real reasoning)
- Option 3: Watchdog (auto-recovery)
- How they work together
- Affected models
- Model routing
- Is it working?
- Running tests
- This bug is everywhere
- Files in this repo
- Changelog
You ask DeepSeek a question. It picks a tool, calls it, works fine. Then you ask a follow-up and you get this:
HTTP 400: The reasoning_content in the thinking mode must be passed back to the API
This is OpenCode dropping the field before it reaches the API — the providers are doing exactly what their docs say.
DeepSeek V4 (and Kimi K2.7, GLM 5.x, MiMo V2.5) require that reasoning_content from every prior assistant turn gets included in subsequent API requests. The docs say it clearly: if you do not pass back reasoning_content correctly, the API returns a 400 error. All five providers confirm this in their official documentation:
- DeepSeek: "The reasoning_content will be ignored by the API", but the conversation history must contain the field.
- Z.AI / GLM: "Key: return reasoning_content to keep the reasoning coherent."
- Kimi / Moonshot: "You must keep the reasoning_content in the multi-round conversation... otherwise an error will be thrown."
- MiniMax: "The complete model response must be append to the conversation history."
- Xiaomi MiMo: "Any assistant message with tool calls... must preserve its full reasoning_content field, otherwise the API will return a 400 error. Affected frameworks include TRAE, Cursor, Roo Code, Codex, GitHub Copilot CLI, Zed, AutoGen."
OpenCode's provider layer drops this field. Three upstream PRs (#24250, #24428, #24895) tried to fix it. None merged. The field is non-standard per OpenAI, so both OpenCode and the AI SDK ignore it.
This repo fixes it. Three layers, pick what you need.
See Quick Install above, use OpenCode TUI (Ctrl+P) or CLI (opencode plugin opencode-thinking-fix).
Drop the plugin file in your OpenCode plugins directory and restart:
mkdir -p ~/.config/opencode/plugins
cp plugins/opencode-thinking-fix-universal.ts ~/.config/opencode/plugins/It scans outgoing messages for any assistant turn that already has reasoning_content. If it finds one (meaning you are using a reasoning model), it adds reasoning_content: "" to every assistant turn missing it. If it finds nothing (Qwen, GPT, Claude, they never produce this field), it does nothing.
It also handles reasoning for the OpenCode Go provider, and patches empty content fields that OpenAI-compatible SDKs sometimes omit.
No config file changes. No build step. OpenCode compiles .ts plugins when it starts.
The catch: the plugin fills in empty strings, not your model's actual prior thinking. DeepSeek, Kimi K2.5/K2.6, GLM, and MiMo accept empty strings fine, your conversation works but the model does not see its earlier reasoning. Kimi K2.7 Code rejects empty strings entirely, it needs the real text.
A Node.js proxy that catches API responses as they come back, pulls out the actual reasoning_content text, and caches it in memory. On the next request, it injects that real text back into the conversation history instead of empty strings.
Your model sees its full chain-of-thought from turn 1 on every subsequent turn. The difference is noticeable on complex multi-turn coding sessions.
The proxy runs on two ports:
| Port | Purpose | Environment |
|---|---|---|
| 3457 | Direct providers (DeepSeek, Kimi, GLM, MiMo, GPT, Claude, Qwen, Gemini, etc.) | PORT=3457 |
| 3458 | OpenCode Go provider | PORT=3458 UPSTREAM_URL=https://opencode.ai/zen/go/v1 |
Port 3457 auto-routes based on model name using the built-in route table. Port 3458 is a fixed-upstream proxy specifically for the OpenCode Go provider, which uses delta.reasoning (not reasoning_content) in its SSE streams. Both are handled by the same proxy.js binary, just different environment variables.
# Linux / macOS / Windows (Node.js required)
node proxy/proxy.js
# OpenCode Go proxy
PORT=3458 UPSTREAM_URL=https://opencode.ai/zen/go/v1 node proxy/proxy.jsWindows PowerShell: use
$env:PORT=3457; node proxy/proxy.js(PowerShell) orset PORT=3457 && node proxy/proxy.js(CMD).
mkdir -p ~/.config/systemd/user
cp systemd/reasoning-cache.service ~/.config/systemd/user/
cp systemd/reasoning-cache-go.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now reasoning-cache.service
systemctl --user enable --now reasoning-cache-go.serviceThen point OpenCode at it, in your opencode.json:
{
"provider": {
"deepseek-v4-pro": {
"baseURL": "http://127.0.0.1:3457/v1"
},
"opencode-go": {
"baseURL": "http://127.0.0.1:3458/v1"
}
}
}One runtime dependency (eventsource-parser). The proxy uses Node.js built-in http, https, and url for everything else.
Interleaved thinking support: GLM-5+ and MiniMax-M3 emit reasoning AFTER content in the same turn (interleaved thinking between tool calls). The proxy accumulates ALL reasoning across an entire assistant turn and flushes only on finish_reason, never on delta.content arrival. This prevents split/lost reasoning blocks.
Kimi K2.7 Code and OpenCode Go need this. The rest of the models benefit from it but do not technically require it.
The watchdog script checks both proxy instances every 4 minutes and restarts any that are down:
cp watchdog/watchdog.sh ~/reasoning-cache-proxy/
cp systemd/reasoning-proxy-watchdog.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now reasoning-proxy-watchdog.serviceOpenCode → [plugin patches missing reasoning_content/reasoning]
→ [proxy injects cached real text]
→ [watchdog keeps both proxies alive]
→ API
The plugin is the safety net. If the proxy goes down, the plugin still injects empty strings so you do not get 400s. If the proxy is up, its cached text takes priority because the plugin sees the field is already filled in. Either way, your conversation does not break.
| Model | Plugin helps | Proxy helps | What it needs |
|---|---|---|---|
| DeepSeek V4 Pro / Flash | Yes | Nice to have | Accepts "" |
| Kimi K2.5 / K2.6 | Yes | Nice to have | Accepts "" |
| Kimi K2.7 Code | Not enough alone | Required | Needs real text |
| GLM-5.x / Zhipu | Yes | Nice to have | Accepts "" |
| MiMo V2.5 / MiniMax | Yes | Nice to have | Accepts "" (default mode embeds <think> in content) |
| MiniMax-M3 | Yes | Recommended | reasoning_details[] array; ~40% quality loss if stripped. Proxy injects reasoning_split:true to keep thinking separate from content. |
| OpenCode Go | Yes | Required | Uses reasoning field |
| Qwen, GPT, Claude, Gemini, Llama, Mistral | No | No | No reasoning_content |
The proxy auto-routes by model name prefix. All 15 supported prefixes:
| Prefix | Upstream | Reasoning |
|---|---|---|
deepseek-v4-pro |
https://api.deepseek.com |
Yes |
deepseek |
https://api.deepseek.com |
Yes |
kimi, moonshot |
https://api.moonshot.ai/v1 |
Yes |
glm, zhipu |
https://open.bigmodel.cn/api/paas/v4 |
Yes |
minimax, mimo |
https://api.minimax.io/v1 |
Yes |
gpt, o1 |
https://api.openai.com |
No |
claude, anthropic |
https://api.anthropic.com |
No |
qwen |
https://dashscope-intl.aliyuncs.com/compatible-mode/v1 |
No |
gemini |
https://generativelanguage.googleapis.com/v1beta/openai |
No |
llama |
https://api.together.xyz |
No |
mistral |
https://api.mistral.ai |
No |
Unknown models fall back to https://api.deepseek.com with reasoning disabled.
The plugin writes a structured JSON log: ~/.local/share/opencode/thinking-fix.log.
628 unique sessions. 12,551 inspect events (the hook fires twice per message by design — first pass patches, second confirms clean). 297 reasoning model sessions patched, 331 non-reasoning correctly skipped. Zero false patches.
Before and after, from a real session:
Before: HTTP 400: The reasoning_content in the thinking mode must be passed back to the API
After: 34 fields patched across a 104-message session → conversation continues
Here is a live excerpt from that session:
{"ts":"2026-06-25T01:41:50.572Z","event":"inspect","isReasoningModel":true,
"totalMessages":104,"patchedFields":34,"turns":[
{"index":1,"fields":["text","reasoning"]},
{"index":5,"fields":["text"]},
{"index":9,"fields":["text"]},
{"index":42,"fields":["reasoning"]}
]}No output? Either you are on a non-reasoning model (correct, no patching needed) or the plugin did not load. Check:
grep plugin_loaded ~/.local/share/opencode/thinking-fix.logProxy health:
curl http://127.0.0.1:3457/health # → {"ok":true,"uptime":1225}
curl http://127.0.0.1:3458/health # → {"ok":true,"uptime":1225}
journalctl --user -u reasoning-cache.service -f
journalctl --user -u reasoning-cache-go.service -fnpm test
# or directly:
node tests/test-plugin.js
node tests/test-proxy.jsThe plugin tests cover 12 cases: native reasoning model detection, OpenCode Go reasoning field detection, non-reasoning model passthrough, mixed messages, multiple assistant turns, already-complete messages, wrapper format ({ info: Message }), empty reasoning_content, empty arrays, tool_calls with reasoning and without, and null/undefined wrappers.
The proxy tests cover 15 cases: route resolution for all model prefixes, patchRequestBody injection from cache for both reasoning_content and reasoning, no-cache fallback to empty strings, user message isolation, multi-turn caching, and SSE stream parsing for delta.reasoning_content, delta.reasoning, content-triggered flush, and finish_reason flush.
OpenCode is not the only tool that drops reasoning_content. Here is a partial list of places this same bug shows up:
OpenCode (anomalyco/opencode): #24190, #24104, #24722, #25311, #25134, #25000, #24124, #24130, #24261, #24442, #24569
Kilo Code: #9501
VS Code: #318920
OpenAI Codex: #24500
GitHub Copilot: discussion #193953
OmniRoute: #1628
Reddit: r/opencodeCLI, r/DeepSeek, r/RooCode
Blogs covering it: AkitaOnRails, ClawHub
plugins/
opencode-thinking-fix-universal.ts # self-detection plugin (92 lines)
proxy/
proxy.js # reasoning cache proxy (422 lines, 1 dep)
tests/
test-plugin.js # plugin unit tests (228 lines, 12 cases)
test-proxy.js # proxy unit tests (359 lines, 15 cases)
watchdog/
watchdog.sh # auto-recovery watchdog (64 lines)
systemd/
reasoning-cache.service # proxy systemd unit (port 3457)
reasoning-cache-go.service # OpenCode Go proxy unit (port 3458)
reasoning-proxy-watchdog.service # watchdog systemd unit
See CHANGELOG.md for release history. Current: v2.0.0 — eventsource-parser, LRU cache, SIGTERM handling, 12+15 passing tests.
| Platform | Plugin | Proxy | Watchdog | Systemd |
|---|---|---|---|---|
| Linux (Kubuntu 24.04) | ✅ | ✅ | ✅ (bash) | ✅ |
| macOS | ✅ | ✅ | ✅ (bash) | ❌ (use launchd) |
| Windows | ✅ | ✅ | ❌ (bash) | ❌ |
OpenCode v1.17.9+, DeepSeek V4 Pro, Kimi K2.5/K2.6/K2.7, GLM-5.x, MiMo V2.5, MiniMax-M3, OpenCode Go.
Plugin and proxy work fully on Windows. The proxy (proxy.js) uses one runtime dependency (eventsource-parser) plus Node.js built-in modules (http, https, url). No platform-specific code. Start it with:
# PowerShell
$env:PORT=3457; node proxy\proxy.jsWatchdog and systemd are Linux-only. For Windows auto-restart, use Task Scheduler or NSSM (Non-Sucking Service Manager) to run the proxy as a Windows service:
# Using NSSM (install once: winget install nssm)
nssm install ReasoningCacheProxy node.exe proxy\proxy.js
nssm set ReasoningCacheProxy AppDirectory C:\path\to\opencode-thinking-fix
nssm set ReasoningCacheProxy AppEnvironmentExtra PORT=3457
nssm start ReasoningCacheProxyRepeat for the Go proxy on PORT=3458 with UPSTREAM_URL=https://opencode.ai/zen/go/v1.
OpenCode config paths on Windows:
| Scope | Path |
|---|---|
| Global | %APPDATA%\OpenCode\opencode.json |
| Project | <project>\.opencode\opencode.json |
| Plugins dir | %APPDATA%\OpenCode\plugins\ |
| npm cache | %LOCALAPPDATA%\opencode\node_modules\ |