opencode-thinking-fix 1.1.4 → 1.1.6

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.
package/README.md CHANGED
@@ -10,6 +10,8 @@ npm install opencode-thinking-fix
10
10
  ```
11
11
 
12
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
+ > Docs: [OpenCode Plugins](https://opencode.ai/docs/plugins)
13
15
 
14
16
  ---
15
17
 
@@ -17,13 +19,15 @@ npm install opencode-thinking-fix
17
19
 
18
20
  **This is an OpenCode plugin. Install it inside OpenCode — no terminal needed.**
19
21
 
20
- ### Method 1: TUI (press Ctrl+P while OpenCode is running)
22
+ ### Method 1: TUI (press `Ctrl+P` while OpenCode is running)
21
23
 
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.
24
+ 1. Press `Ctrl+P` to open the command palette.
25
+ 2. Type `install plugin` and press `Enter`.
26
+ 3. Press `Tab` to switch the install scope to **Global** (recommended — works across all projects).
27
+ 4. Type `opencode-thinking-fix`.
28
+ 5. Press `Enter`. Restart OpenCode.
29
+
30
+ You should see `[ThinkingFix] Plugin loaded — universal reasoning_content fix active` at startup.
27
31
 
28
32
  ### Method 2: CLI (shell command)
29
33
 
@@ -31,13 +35,15 @@ npm install opencode-thinking-fix
31
35
  opencode plugin opencode-thinking-fix
32
36
  ```
33
37
 
34
- For specific version:
38
+ For a specific version:
35
39
 
36
40
  ```bash
37
- opencode plugin opencode-thinking-fix@1.1.0
41
+ opencode plugin opencode-thinking-fix@1.1.4
38
42
  ```
39
43
 
40
- ### Method 3: Manual config (add to opencode.json)
44
+ Restart OpenCode after installing.
45
+
46
+ ### Method 3: Manual config (add to `opencode.json`)
41
47
 
42
48
  ```json
43
49
  {
@@ -51,7 +57,9 @@ Config file location:
51
57
 
52
58
  Restart OpenCode after adding. You should see `[ThinkingFix] Plugin loaded` at startup.
53
59
 
54
- ## Table of Contents
60
+ See also: [OpenCode plugin docs](https://opencode.ai/docs/plugins)
61
+
62
+ ---
55
63
 
56
64
  - [What problem this fixes](#what-problem-this-fixes)
57
65
  - [Option 1: Plugin (stops the crashes)](#option-1-plugin-stops-the-crashes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-thinking-fix",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",
@@ -20,13 +20,18 @@
20
20
  "url": "git+https://github.com/tbosancheros39/opencode-thinking-fix.git"
21
21
  },
22
22
  "oc-plugin": [
23
- "server"
23
+ "server",
24
+ "tui"
24
25
  ],
25
26
  "main": "./plugins/opencode-thinking-fix-universal.ts",
26
27
  "exports": {
27
28
  "./server": {
28
29
  "import": "./plugins/opencode-thinking-fix-universal.ts",
29
30
  "types": "./plugins/opencode-thinking-fix-universal.ts"
31
+ },
32
+ "./tui": {
33
+ "import": "./plugins/opencode-thinking-fix-tui.ts",
34
+ "types": "./plugins/opencode-thinking-fix-tui.ts"
30
35
  }
31
36
  },
32
37
  "files": [
@@ -0,0 +1,20 @@
1
+ import type { TUIPlugin } from '@opencode-ai/plugin'
2
+
3
+ /**
4
+ * No-op TUI plugin entry point.
5
+ *
6
+ * opencode-thinking-fix is a server-side plugin: it patches outgoing chat
7
+ * messages via experimental.chat.messages.transform. OpenCode's plugin
8
+ * installer, however, validates that a package exposes either a server or a
9
+ * TUI target. By exposing an empty TUI target we make installation via
10
+ * Ctrl+P / `opencode plugin opencode-thinking-fix` work without warnings.
11
+ */
12
+ const tuiPlugin: TUIPlugin = {
13
+ name: 'opencode-thinking-fix-tui',
14
+ version: '1.1.6',
15
+ activate() {
16
+ // nothing to do at TUI level
17
+ },
18
+ }
19
+
20
+ export default tuiPlugin
package/proxy/proxy.js CHANGED
@@ -155,9 +155,13 @@ function createStreamParser(sessionId, onComplete) {
155
155
  buffer += chunk.toString()
156
156
  const lines = buffer.split('\n')
157
157
  buffer = lines.pop() // keep incomplete last line
158
+ const outLines = []
158
159
 
159
160
  for (const line of lines) {
160
- if (!line.startsWith('data: ')) continue
161
+ if (!line.startsWith('data: ')) {
162
+ outLines.push(line)
163
+ continue
164
+ }
161
165
  const data = line.slice(6).trim()
162
166
  if (data === '[DONE]') {
163
167
  // Flush accumulated reasoning for this turn
@@ -165,12 +169,16 @@ function createStreamParser(sessionId, onComplete) {
165
169
  onComplete(assistantIndex, reasoningBuffer)
166
170
  reasoningBuffer = ''
167
171
  }
172
+ outLines.push(line)
168
173
  continue
169
174
  }
170
175
  try {
171
176
  const parsed = JSON.parse(data)
172
177
  const delta = parsed.choices?.[0]?.delta
173
- if (!delta) continue
178
+ if (!delta) {
179
+ outLines.push(line)
180
+ continue
181
+ }
174
182
 
175
183
  // reasoning_content delta (DeepSeek/Kimi/GLM/MiniMax)
176
184
  if (typeof delta.reasoning_content === 'string') {
@@ -194,6 +202,17 @@ function createStreamParser(sessionId, onComplete) {
194
202
  inReasoning = true
195
203
  }
196
204
 
205
+ // content may contain embedded <think> tags when reasoning_split is not
206
+ // honored (MiniMax fallback). Extract them into reasoningBuffer.
207
+ if (typeof delta.content === 'string') {
208
+ const thinkMatch = delta.content.match(/<think>([\s\S]*?)<\/think>/)
209
+ if (thinkMatch) {
210
+ reasoningBuffer += thinkMatch[1]
211
+ inReasoning = true
212
+ delta.content = delta.content.replace(/<think>[\s\S]*?<\/think>/, '').trimStart()
213
+ }
214
+ }
215
+
197
216
  // finish_reason means the assistant turn is complete
198
217
  // Only flush here — NOT on content appearance (interleaved thinking
199
218
  // emits reasoning AFTER content on GLM-5+ and MiniMax-M3)
@@ -205,10 +224,16 @@ function createStreamParser(sessionId, onComplete) {
205
224
  }
206
225
  assistantIndex++
207
226
  }
227
+
228
+ // Re-serialize the possibly-modified chunk and forward it
229
+ outLines.push('data: ' + JSON.stringify(parsed))
208
230
  } catch {
209
- // malformed SSE chunk, skip
231
+ // malformed SSE chunk, forward unchanged
232
+ outLines.push(line)
210
233
  }
211
234
  }
235
+
236
+ return outLines.join('\n') + '\n'
212
237
  },
213
238
  flush() {
214
239
  if (reasoningBuffer) {
@@ -240,16 +265,22 @@ const server = http.createServer((req, res) => {
240
265
  // Determine upstream route
241
266
  let modelName = ''
242
267
  let routeTarget = DEFAULT_ROUTE
268
+
269
+ // Always parse model name from body so provider-specific patches work
270
+ // even in fixed-upstream mode (OpenCode Go)
271
+ if (isPost && isJson && rawBody.length > 0) {
272
+ try {
273
+ const bodyJson = JSON.parse(rawBody.toString('utf8'))
274
+ modelName = bodyJson.model || ''
275
+ } catch { /* body not JSON yet */ }
276
+ }
277
+
243
278
  if (UPSTREAM_URL) {
244
279
  // Fixed upstream mode (e.g. OpenCode Go)
245
280
  routeTarget = { base: UPSTREAM_URL, reasoning: true }
246
- } else if (isPost && isJson && rawBody.length > 0) {
281
+ } else {
247
282
  // Model-based routing mode
248
- try {
249
- const bodyJson = JSON.parse(rawBody.toString('utf8'))
250
- modelName = bodyJson.model || ''
251
- routeTarget = route(modelName)
252
- } catch { /* body not JSON yet, use defaults */ }
283
+ routeTarget = route(modelName)
253
284
  }
254
285
  const upstream = url.parse(routeTarget.base)
255
286
  const shouldPatch = isPost && isJson && routeTarget.reasoning
@@ -329,8 +360,9 @@ const server = http.createServer((req, res) => {
329
360
  })
330
361
 
331
362
  proxyRes.on('data', chunk => {
332
- parser.feed(chunk)
333
- res.write(chunk) // forward chunk immediately (no buffering)
363
+ // parser.feed returns the (possibly modified) SSE text to forward
364
+ const forwardChunk = parser.feed(chunk)
365
+ res.write(forwardChunk)
334
366
  })
335
367
 
336
368
  proxyRes.on('end', () => {