opencode-thinking-fix 1.1.7 → 1.1.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to `opencode-thinking-fix`.
4
4
 
5
+ ## [1.1.9] - 2026-06-23
6
+
7
+ ### Added
8
+ - **File-based logging**: JSON-lines log at `~/.local/share/opencode/thinking-fix.log` — captures `plugin_loaded`, `inspect`, `patched`, and `error` events with timestamps.
9
+ - **`inspect` event**: logs field coverage on every request — `isReasoningModel`, `assistantTurns`, `missingContent`, `missingReasoningContent`, `missingReasoning` — proving plugin activity even when zero patches needed.
10
+ - **Console fallback**: `writeLog` catches file write failures and emits `console.error` so failures are visible somewhere.
11
+
12
+ ### Changed
13
+ - `client.app.log()` calls now use `await` for proper async handling.
14
+ - Removal of `hook_fired` debug artifact — replaced by structured `inspect` event.
15
+
16
+ ### Fixed
17
+ - **TUI install flow**: Ctrl+P "install plugin" → type `opencode-thinking-fix` no longer errors. Server-only plugin with bare `export default` properly installs via TUI and CLI without TUI target warnings.
18
+
19
+ ## [1.1.8] - 2026-06-23
20
+
21
+ ### Fixed
22
+ - **package.json aligned with project Documents**: description shortened, `homepage` added, `peerDependenciesMeta.optional` set to `false`, `repository.url` normalized, keywords added (`mimo`, `minimax`), duplicate keywords removed.
23
+ - **Stale `node_modules/` deleted** from repo (was committed accidentally).
24
+ - **Stale npm cache entry** (`npm i opencode-thinking-fix`) removed from `~/.cache/opencode/packages/`.
25
+
5
26
  ## [1.1.7] - 2026-06-23
6
27
 
7
28
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-thinking-fix",
3
- "version": "1.1.7",
4
- "description": "Fix DeepSeek/Kimi/GLM/MiMo reasoning_content 400 errors in OpenCode. Plugin + proxy + watchdog.",
3
+ "version": "1.1.9",
4
+ "description": "Fix reasoning_content 400 errors for DeepSeek/Kimi/GLM/MiMo/MiniMax in OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "tbosancheros39",
@@ -10,16 +10,17 @@
10
10
  "deepseek",
11
11
  "kimi",
12
12
  "glm",
13
+ "mimo",
14
+ "minimax",
13
15
  "reasoning",
14
16
  "reasoning_content",
15
- "thinking",
16
- "proxy",
17
17
  "plugin"
18
18
  ],
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+https://github.com/tbosancheros39/opencode-thinking-fix.git"
21
+ "url": "https://github.com/tbosancheros39/opencode-thinking-fix"
22
22
  },
23
+ "homepage": "https://github.com/tbosancheros39/opencode-thinking-fix#readme",
23
24
  "main": "./plugins/opencode-thinking-fix-universal.ts",
24
25
  "exports": {
25
26
  ".": "./plugins/opencode-thinking-fix-universal.ts",
@@ -46,7 +47,7 @@
46
47
  },
47
48
  "peerDependenciesMeta": {
48
49
  "@opencode-ai/plugin": {
49
- "optional": true
50
+ "optional": false
50
51
  }
51
52
  },
52
53
  "devDependencies": {
@@ -1,4 +1,7 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin'
2
+ import { appendFileSync, mkdirSync } from 'fs'
3
+ import { homedir } from 'os'
4
+ import { join } from 'path'
2
5
 
3
6
  // ─────────────────────────────────────────────────────────────────────────────
4
7
  // CONFIRMED FACTS (cross-validated across DeepSeek V4 Pro, Claude MAX, Kimi
@@ -27,57 +30,88 @@ import type { Plugin } from '@opencode-ai/plugin'
27
30
  // OR: "plugin": ["opencode-thinking-fix-universal"] (array, not object)
28
31
  // Build: none — OpenCode compiles .ts plugins at load time
29
32
  // ─────────────────────────────────────────────────────────────────────────────
33
+ // LOGGING
34
+ // File: ~/.local/share/opencode/thinking-fix.log
35
+ // Format: JSON lines — one entry per event
36
+ // View: tail -f ~/.local/share/opencode/thinking-fix.log | jq
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ const LOG_DIR = join(homedir(), '.local', 'share', 'opencode')
40
+ const LOG_FILE = join(LOG_DIR, 'thinking-fix.log')
41
+
42
+ function writeLog(entry: Record<string, unknown>): void {
43
+ try {
44
+ mkdirSync(LOG_DIR, { recursive: true })
45
+ appendFileSync(LOG_FILE, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n', 'utf8')
46
+ } catch (err: any) {
47
+ try { console.error('[thinking-fix] log write failed:', err?.message) } catch {}
48
+ }
49
+ }
30
50
 
31
- function patchMessages(messages: any[]): number {
32
- let count = 0
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ // Message patching — self-detection, in-place mutation
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ interface PatchResult {
55
+ patched: number
56
+ totalMessages: number
57
+ turns: Array<{ index: number; fields: string[] }>
58
+ }
59
+
60
+ function patchMessages(messages: any[]): PatchResult {
61
+ const result: PatchResult = { patched: 0, totalMessages: messages.length, turns: [] }
33
62
 
34
63
  // ── Step 1: Self-detect reasoning model ──────────────────────────────────
35
- // Scan for any assistant message that already has reasoning_content.
36
- // OpenCode sets this field on the most recent response turn.
37
- // Non-reasoning providers (Qwen/GPT/Claude/Mistral/Llama) never have it.
38
64
  const isReasoningModel = messages.some((wrapper) => {
39
65
  const msg = wrapper?.info ?? wrapper
40
66
  return msg?.role === 'assistant' && ('reasoning_content' in msg || 'reasoning' in msg)
41
67
  })
42
68
 
43
69
  // ── Step 2: Non-reasoning model — exit immediately, zero modification ─────
44
- // Qwen/GPT/Claude reject unknown fields with HTTP 400.
45
- // This guard is what prevents multi-session errors on non-reasoning models.
46
- if (!isReasoningModel) return 0
70
+ if (!isReasoningModel) return result
47
71
 
48
72
  // ── Step 3: Patch all assistant turns missing required fields ─────────────
49
- for (const wrapper of messages) {
73
+ messages.forEach((wrapper, index) => {
50
74
  const msg = wrapper?.info ?? wrapper
51
- if (!msg || msg.role !== 'assistant') continue
75
+ if (!msg || msg.role !== 'assistant') return
76
+
77
+ const fields: string[] = []
52
78
 
53
- // Fix 1: content must be non-null string on every assistant turn.
54
- // OpenAI-compatible SDK omits it when only tool_calls are present.
79
+ // Fix 1: content must be non-null string on every assistant turn
55
80
  if (!msg.content && msg.content !== '') {
56
81
  msg.content = msg.tool_calls?.length ? 'call tool' : ''
57
- count++
82
+ fields.push('content')
58
83
  }
59
84
 
60
- // Fix 2: reasoning_content must be present on every assistant turn in history.
61
- // Absence causes HTTP 400 from DeepSeek/Kimi/GLM/MiMo on turn 2+.
62
- // Use "" (empty string) — confirmed working cross-provider.
63
- // "" and null both work per DeepSeek docs; "" is safer cross-provider.
85
+ // Fix 2: reasoning_content — absent causes HTTP 400 from DeepSeek/Kimi/GLM/MiMo on turn 2+
64
86
  if (!('reasoning_content' in msg)) {
65
87
  msg.reasoning_content = ''
66
- count++
88
+ fields.push('reasoning_content')
67
89
  }
68
90
 
69
- // Fix 3: reasoning must be present for OpenCode Go provider on every turn.
91
+ // Fix 3: reasoning — required for OpenCode Go provider on every turn
70
92
  if (!('reasoning' in msg)) {
71
93
  msg.reasoning = ''
72
- count++
94
+ fields.push('reasoning')
73
95
  }
74
- }
75
96
 
76
- return count
97
+ if (fields.length > 0) {
98
+ result.patched += fields.length
99
+ result.turns.push({ index, fields })
100
+ }
101
+ })
102
+
103
+ return result
77
104
  }
78
105
 
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+ // Plugin
108
+ // ─────────────────────────────────────────────────────────────────────────────
79
109
  export const ThinkingFixPlugin: Plugin = async ({ client }) => {
80
- client.app.log({ body: { service: 'thinking-fix', level: 'info', message: 'Plugin loaded — universal reasoning_content fix active' } })
110
+ mkdirSync(LOG_DIR, { recursive: true })
111
+
112
+ writeLog({ event: 'plugin_loaded', logFile: LOG_FILE })
113
+
114
+ await client.app.log({ body: { service: 'thinking-fix', level: 'info', message: `Plugin loaded — logging to ${LOG_FILE}` } })
81
115
 
82
116
  return {
83
117
  'chat.headers': async (input: any, output: any) => {
@@ -91,13 +125,48 @@ export const ThinkingFixPlugin: Plugin = async ({ client }) => {
91
125
  const messages = output?.messages
92
126
  if (!Array.isArray(messages) || messages.length === 0) return
93
127
 
94
- const patched = patchMessages(messages)
128
+ const assistantTurns = messages.filter((w: any) => {
129
+ const m = w?.info ?? w
130
+ return m?.role === 'assistant'
131
+ })
132
+ const isReasoningModel = assistantTurns.some((w: any) => {
133
+ const m = w?.info ?? w
134
+ return 'reasoning_content' in m || 'reasoning' in m
135
+ })
136
+ writeLog({
137
+ event: 'inspect',
138
+ isReasoningModel,
139
+ totalMessages: messages.length,
140
+ assistantTurns: assistantTurns.length,
141
+ missingContent: assistantTurns.filter((w: any) => {
142
+ const m = w?.info ?? w
143
+ return !m.content && m.content !== ''
144
+ }).length,
145
+ missingReasoningContent: assistantTurns.filter((w: any) => {
146
+ const m = w?.info ?? w
147
+ return !('reasoning_content' in m)
148
+ }).length,
149
+ missingReasoning: assistantTurns.filter((w: any) => {
150
+ const m = w?.info ?? w
151
+ return !('reasoning' in m)
152
+ }).length,
153
+ })
154
+
155
+ const result = patchMessages(messages)
156
+
157
+ if (result.patched > 0) {
158
+ writeLog({
159
+ event: 'patched',
160
+ patchedFields: result.patched,
161
+ totalMessages: result.totalMessages,
162
+ turns: result.turns,
163
+ })
95
164
 
96
- if (patched > 0) {
97
- client.app.log({ body: { service: 'thinking-fix', level: 'info', message: `patched ${patched} field(s) across ${messages.length} message(s)` } })
165
+ await client.app.log({ body: { service: 'thinking-fix', level: 'info', message: `patched ${result.patched} field(s) across ${result.totalMessages} message(s)` } })
98
166
  }
99
- } catch (err) {
100
- client.app.log({ body: { service: 'thinking-fix', level: 'error', message: `Error — passing through unmodified: ${err}` } })
167
+ } catch (err: any) {
168
+ writeLog({ event: 'error', message: err?.message ?? String(err) })
169
+ await client.app.log({ body: { service: 'thinking-fix', level: 'error', message: `Error — passing through unmodified: ${err?.message ?? err}` } })
101
170
  }
102
171
  },
103
172
  }