opencode-thinking-fix 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +94 -7
  2. package/package.json +5 -3
  3. package/proxy/proxy.js +34 -11
package/README.md CHANGED
@@ -9,7 +9,47 @@
9
9
  npm install opencode-thinking-fix
10
10
  ```
11
11
 
12
- > Fix for the `reasoning_content` 400 error that kills multi-turn conversations with DeepSeek, Kimi, GLM, and MiMo in OpenCode.
12
+ > Fix for the `reasoning_content` 400 error that kills multi-turn conversations with DeepSeek, Kimi, GLM, MiMo, and MiniMax-M3 in OpenCode.
13
+
14
+ ---
15
+
16
+ ## Quick Install
17
+
18
+ **This is an OpenCode plugin. Install it inside OpenCode — no terminal needed.**
19
+
20
+ ### Method 1: TUI (press Ctrl+P while OpenCode is running)
21
+
22
+ 1. Press `Ctrl+P` to open the command palette
23
+ 2. Type `install plugin` and press Enter
24
+ 3. Press `Tab` to switch scope to **Global** (recommended — works across all projects)
25
+ 4. Type `opencode-thinking-fix`
26
+ 5. Press Enter — done. Restart OpenCode.
27
+
28
+ ### Method 2: CLI (shell command)
29
+
30
+ ```bash
31
+ opencode plugin opencode-thinking-fix
32
+ ```
33
+
34
+ For specific version:
35
+
36
+ ```bash
37
+ opencode plugin opencode-thinking-fix@1.1.0
38
+ ```
39
+
40
+ ### Method 3: Manual config (add to opencode.json)
41
+
42
+ ```json
43
+ {
44
+ "plugin": ["opencode-thinking-fix"]
45
+ }
46
+ ```
47
+
48
+ Config file location:
49
+ - **Linux/macOS:** `~/.config/opencode/opencode.json` (global) or `.opencode/opencode.json` (project)
50
+ - **Windows:** `%APPDATA%/OpenCode/opencode.json` (global) or `.opencode/opencode.json` (project)
51
+
52
+ Restart OpenCode after adding. You should see `[ThinkingFix] Plugin loaded` at startup.
13
53
 
14
54
  ## Table of Contents
15
55
 
@@ -45,7 +85,13 @@ This repo fixes it. Three layers, pick what you need.
45
85
 
46
86
  ## Option 1: plugin (stops the crashes)
47
87
 
48
- One file. Drop it in `~/.config/opencode/plugins/` and restart:
88
+ ### Install via npm (recommended)
89
+
90
+ See [Quick Install](#quick-install) above — use OpenCode TUI (`Ctrl+P`) or CLI (`opencode plugin opencode-thinking-fix`).
91
+
92
+ ### Manual install (for local development)
93
+
94
+ Drop the plugin file in your OpenCode plugins directory and restart:
49
95
 
50
96
  ```bash
51
97
  mkdir -p ~/.config/opencode/plugins
@@ -80,13 +126,15 @@ The proxy runs on **two ports**:
80
126
  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.
81
127
 
82
128
  ```bash
83
- # run port 3457 (direct providers)
129
+ # Linux / macOS / Windows (Node.js required)
84
130
  node proxy/proxy.js
85
131
 
86
- # run port 3458 (OpenCode Go)
132
+ # OpenCode Go proxy
87
133
  PORT=3458 UPSTREAM_URL=https://opencode.ai/zen/go/v1 node proxy/proxy.js
88
134
  ```
89
135
 
136
+ > **Windows PowerShell:** use `$env:PORT=3457; node proxy/proxy.js` (PowerShell) or `set PORT=3457 && node proxy/proxy.js` (CMD).
137
+
90
138
  ### Install as systemd services (auto-start at boot)
91
139
 
92
140
  ```bash
@@ -115,6 +163,8 @@ Then point OpenCode at it — in your `opencode.json`:
115
163
 
116
164
  Zero npm dependencies. It uses `http`, `https`, and `url` — nothing else.
117
165
 
166
+ **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.
167
+
118
168
  **Kimi K2.7 Code and OpenCode Go need this.** The rest of the models benefit from it but do not technically require it.
119
169
 
120
170
  ---
@@ -153,9 +203,10 @@ The plugin is the safety net. If the proxy goes down, the plugin still injects e
153
203
  | Kimi K2.5 / K2.6 | Yes | Nice to have | Accepts `""` |
154
204
  | **Kimi K2.7 Code** | **Not enough alone** | **Required** | Needs real text |
155
205
  | GLM-5.x / Zhipu | Yes | Nice to have | Accepts `""` |
156
- | MiMo V2.5 / MiniMax | Yes | Nice to have | Accepts `""` |
206
+ | MiMo V2.5 / MiniMax | Yes | Nice to have | Accepts `""` (default mode embeds `<think>` in `content`) |
207
+ | **MiniMax-M3** | Yes | **Recommended** | `reasoning_details[]` array; ~40% quality loss if stripped. Proxy injects `reasoning_split:true` to keep thinking separate from content. |
157
208
  | OpenCode Go | Yes | Required | Uses `reasoning` field |
158
- | Qwen, GPT, Claude, Gemini, Llama, Mistral | No | No | Do not use reasoning_content |
209
+ | Qwen, GPT, Claude, Gemini, Llama, Mistral | No | No | No reasoning_content |
159
210
 
160
211
  ---
161
212
 
@@ -277,4 +328,40 @@ systemd/
277
328
 
278
329
  ## Tested on
279
330
 
280
- OpenCode v1.17.9, Kubuntu 24.04, DeepSeek V4 Pro, OpenCode Go.
331
+ | Platform | Plugin | Proxy | Watchdog | Systemd |
332
+ |----------|--------|-------|----------|---------|
333
+ | **Linux** (Kubuntu 24.04) | ✅ | ✅ | ✅ (bash) | ✅ |
334
+ | **macOS** | ✅ | ✅ | ✅ (bash) | ❌ (use launchd) |
335
+ | **Windows** | ✅ | ✅ | ❌ (bash) | ❌ |
336
+
337
+ OpenCode v1.17.9+, DeepSeek V4 Pro, Kimi K2.5/K2.6/K2.7, GLM-5.x, MiMo V2.5, MiniMax-M3, OpenCode Go.
338
+
339
+ ### Windows notes
340
+
341
+ **Plugin and proxy work fully on Windows.** The proxy (`proxy.js`) uses only Node.js built-in modules (`http`, `https`, `url`) — zero platform-specific code. Start it with:
342
+
343
+ ```powershell
344
+ # PowerShell
345
+ $env:PORT=3457; node proxy\proxy.js
346
+ ```
347
+
348
+ **Watchdog 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:
349
+
350
+ ```powershell
351
+ # Using NSSM (install once: winget install nssm)
352
+ nssm install ReasoningCacheProxy node.exe proxy\proxy.js
353
+ nssm set ReasoningCacheProxy AppDirectory C:\path\to\opencode-thinking-fix
354
+ nssm set ReasoningCacheProxy AppEnvironmentExtra PORT=3457
355
+ nssm start ReasoningCacheProxy
356
+ ```
357
+
358
+ Repeat for the Go proxy on PORT=3458 with `UPSTREAM_URL=https://opencode.ai/zen/go/v1`.
359
+
360
+ **OpenCode config paths on Windows:**
361
+
362
+ | Scope | Path |
363
+ |-------|------|
364
+ | Global | `%APPDATA%\OpenCode\opencode.json` |
365
+ | Project | `<project>\.opencode\opencode.json` |
366
+ | Plugins dir | `%APPDATA%\OpenCode\plugins\` |
367
+ | npm cache | `%LOCALAPPDATA%\opencode\node_modules\` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-thinking-fix",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Fix DeepSeek/Kimi/GLM/MiMo reasoning_content 400 errors in OpenCode. Plugin + proxy + watchdog.",
5
5
  "license": "MIT",
6
6
  "author": "tbosancheros39",
@@ -17,9 +17,11 @@
17
17
  ],
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "https://github.com/tbosancheros39/opencode-thinking-fix.git"
20
+ "url": "git+https://github.com/tbosancheros39/opencode-thinking-fix.git"
21
21
  },
22
- "oc-plugin": ["server"],
22
+ "oc-plugin": [
23
+ "server"
24
+ ],
23
25
  "main": "./plugins/opencode-thinking-fix-universal.ts",
24
26
  "exports": {
25
27
  "./server": {
package/proxy/proxy.js CHANGED
@@ -115,6 +115,16 @@ function patchRequestBody(body, sessionId) {
115
115
  modified = true
116
116
  }
117
117
 
118
+ // MiniMax-M3 reasoning_details array format (reasoning_split: true)
119
+ if (!msg.reasoning_details && !msg.reasoning_content) {
120
+ const cached = sessionCache.get(assistantIndex)
121
+ if (cached) {
122
+ msg.reasoning_details = [{ text: cached, type: 'thinking' }]
123
+ console.log(`[Cache] session ${sessionId.slice(-8)}: replayed reasoning_details for turn ${assistantIndex} (${cached.length} chars)`)
124
+ modified = true
125
+ }
126
+ }
127
+
118
128
  // OpenCode Go uses reasoning instead of reasoning_content
119
129
  if (!msg.reasoning) {
120
130
  const cached = sessionCache.get(assistantIndex)
@@ -162,28 +172,31 @@ function createStreamParser(sessionId, onComplete) {
162
172
  const delta = parsed.choices?.[0]?.delta
163
173
  if (!delta) continue
164
174
 
165
- // reasoning_content delta (DeepSeek/Kimi/GLM native)
175
+ // reasoning_content delta (DeepSeek/Kimi/GLM/MiniMax)
166
176
  if (typeof delta.reasoning_content === 'string') {
167
177
  reasoningBuffer += delta.reasoning_content
168
178
  inReasoning = true
169
179
  }
170
180
 
181
+ // reasoning_details array (MiniMax-M3 with reasoning_split: true)
182
+ if (Array.isArray(delta.reasoning_details)) {
183
+ for (const detail of delta.reasoning_details) {
184
+ if (typeof detail.text === 'string') {
185
+ reasoningBuffer += detail.text
186
+ }
187
+ }
188
+ inReasoning = true
189
+ }
190
+
171
191
  // OpenCode Go uses delta.reasoning instead of reasoning_content
172
192
  if (typeof delta.reasoning === 'string') {
173
193
  reasoningBuffer += delta.reasoning
174
194
  inReasoning = true
175
195
  }
176
196
 
177
- // When content starts after reasoning, the reasoning block is done
178
- if (typeof delta.content === 'string' && delta.content && inReasoning) {
179
- if (reasoningBuffer) {
180
- onComplete(assistantIndex, reasoningBuffer)
181
- reasoningBuffer = ''
182
- inReasoning = false
183
- }
184
- }
185
-
186
197
  // finish_reason means the assistant turn is complete
198
+ // Only flush here — NOT on content appearance (interleaved thinking
199
+ // emits reasoning AFTER content on GLM-5+ and MiniMax-M3)
187
200
  if (parsed.choices?.[0]?.finish_reason) {
188
201
  if (reasoningBuffer) {
189
202
  onComplete(assistantIndex, reasoningBuffer)
@@ -244,7 +257,17 @@ const server = http.createServer((req, res) => {
244
257
  // Patch request body if this is a reasoning provider
245
258
  let bodyToSend = rawBody
246
259
  if (shouldPatch && rawBody.length > 0) {
247
- const patched = patchRequestBody(rawBody.toString('utf8'), sessionId)
260
+ // Inject reasoning_split for MiniMax models (avoids <think> tags in content)
261
+ const isMiniMax = modelName.toLowerCase().startsWith('minimax') || modelName.toLowerCase().startsWith('mimo')
262
+ let bodyStr = rawBody.toString('utf8')
263
+ if (isMiniMax) {
264
+ try {
265
+ const bj = JSON.parse(bodyStr)
266
+ bj.reasoning_split = true
267
+ bodyStr = JSON.stringify(bj)
268
+ } catch { /* pass */ }
269
+ }
270
+ const patched = patchRequestBody(bodyStr, sessionId)
248
271
  bodyToSend = Buffer.from(patched, 'utf8')
249
272
  }
250
273