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 +21 -0
- package/package.json +7 -6
- package/plugins/opencode-thinking-fix-universal.ts +97 -28
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.
|
|
4
|
-
"description": "Fix DeepSeek/Kimi/GLM/MiMo
|
|
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": "
|
|
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":
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
messages.forEach((wrapper, index) => {
|
|
50
74
|
const msg = wrapper?.info ?? wrapper
|
|
51
|
-
if (!msg || msg.role !== 'assistant')
|
|
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
|
-
|
|
82
|
+
fields.push('content')
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
// Fix 2: reasoning_content
|
|
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
|
-
|
|
88
|
+
fields.push('reasoning_content')
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
// Fix 3: reasoning
|
|
91
|
+
// Fix 3: reasoning — required for OpenCode Go provider on every turn
|
|
70
92
|
if (!('reasoning' in msg)) {
|
|
71
93
|
msg.reasoning = ''
|
|
72
|
-
|
|
94
|
+
fields.push('reasoning')
|
|
73
95
|
}
|
|
74
|
-
}
|
|
75
96
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|