opencode-thinking-fix 2.0.0 → 3.1.4

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 DELETED
@@ -1,106 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `opencode-thinking-fix`.
4
-
5
- ## [2.0.0] - 2026-06-24
6
-
7
- ### Changed (breaking)
8
- - **Proxy memory leak fixed**: replaced unbounded `Map` with `LRUCache(500)`, O(1) eviction, zero timer overhead.
9
- - **SIGTERM/SIGINT cleanup**: `process.on` handlers drain active sessions and close the HTTP server cleanly. 5-second force-exit safety net.
10
- - **SSE parser replaced**: hand-rolled state machine → `eventsource-parser` (368k dependents). Built-in `maxBufferSize: 1MB` guard.
11
- - **Plugin `export default` removed**: named export `{ ThinkingFixPlugin }` only. Bare default caused TUI double-load at 71ms+73ms.
12
- - **`engines` field added**: `node >=18` required (node:http, structuredClone).
13
- - **`oc-plugin: ["server"]` field added**: explicit server-only target for OpenCode 1.3.x loader.
14
-
15
- ### Added
16
- - `eventsource-parser` runtime dependency.
17
- - Provider docs evidence: all 5 providers (DeepSeek, Z.AI, Kimi, MiniMax, Xiaomi MiMo) officially confirm the `reasoning_content` bug in their published documentation.
18
-
19
- ### Removed
20
- - `export default` bare export from plugin.
21
- - Hand-rolled SSE parser (~100 lines replaced by 18-line `eventsource-parser` integration).
22
-
23
- ### Fixed
24
- - Plugin 175→92 lines (-47%). Proxy 409→422 lines (net: +13, but SSE parser is now battle-tested and memory-safe).
25
-
26
- ## [1.1.9] - 2026-06-23
27
-
28
- ### Added
29
- - **File-based logging**: JSON-lines log at `~/.local/share/opencode/thinking-fix.log`, captures `plugin_loaded`, `inspect`, `patched`, and `error` events with timestamps.
30
- - **`inspect` event**: logs field coverage on every request, `isReasoningModel`, `assistantTurns`, `missingContent`, `missingReasoningContent`, `missingReasoning`, proving plugin activity even when zero patches needed.
31
- - **Console fallback**: `writeLog` catches file write failures and emits `console.error` so failures are visible somewhere.
32
-
33
- ### Changed
34
- - `client.app.log()` calls now use `await` for proper async handling.
35
- - Removal of `hook_fired` debug artifact, replaced by structured `inspect` event.
36
-
37
- ### Fixed
38
- - **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.
39
-
40
- ## [1.1.8] - 2026-06-23
41
-
42
- ### Fixed
43
- - **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.
44
- - **Stale `node_modules/` deleted** from repo (was committed accidentally).
45
- - **Stale npm cache entry** (`npm i opencode-thinking-fix`) removed from `~/.cache/opencode/packages/`.
46
-
47
- ## [1.1.7] - 2026-06-23
48
-
49
- ### Fixed
50
- - **TUI plugin loading error**: removed broken `opencode-thinking-fix-tui.ts`. OpenCode tried to load it as a TUI plugin at startup and failed because it didn't export `{ tui() }`. Package is now server-only.
51
- - **Plugin default export**: switched from v1 format (`{ server: Plugin }`) to v0 legacy format (bare `export default Plugin`), matching the proven opencode-wakatime pattern.
52
- - **package.json**: removed `oc-plugin` field, removed `dependencies`, moved `@opencode-ai/plugin` to `peerDependencies` (type-only import, no runtime dep needed), whitelisted individual files instead of entire directories.
53
-
54
- ## [1.1.6] - 2026-06-23
55
-
56
- ### Fixed
57
- - Exposed a no-op TUI plugin target so installation via `Ctrl+P` / `opencode plugin opencode-thinking-fix` completes without the "Package has no TUI target" warning.
58
-
59
- ## [1.1.5] - 2026-06-23
60
-
61
- ### Fixed
62
- - **MiniMax-M3 JSON parse errors in OpenCode Go mode**: model name was not parsed in fixed-upstream mode, so `reasoning_split: true` was never injected for MiniMax models routed through port 3458. Now model name is parsed regardless of `UPSTREAM_URL`.
63
- - **`<think>` tags leaked into `delta.content`**: added a fallback extractor that strips `<think>...</think>` blocks from content, moves them into the reasoning buffer, and forwards a sanitized SSE chunk.
64
- - **Kimi K2.6/K2.7 parameter rejection**: strip all hardcoded sampling parameters (`temperature`, `top_p`, `top_k`, `presence_penalty`, `frequency_penalty`, `n`, and `thinking`/`reasoning_effort` for K2.7) before sending to Moonshot.
65
-
66
- ### Changed
67
- - SSE stream parser now re-serializes and forwards modified chunks instead of forwarding the raw upstream bytes unchanged.
68
-
69
- ## [1.1.4] - 2026-06-23
70
-
71
- ### Fixed
72
- - Strip all Moonshot-hardcoded sampling parameters for Kimi K2.6/K2.7 thinking mode, not just `top_p`/`top_k`.
73
-
74
- ## [1.1.3] - 2026-06-23
75
-
76
- ### Fixed
77
- - Strip `top_p` and `top_k` from request body for Kimi/Moonshot models.
78
-
79
- ## [1.1.2] - 2026-06-23
80
-
81
- ### Fixed
82
- - Interleaved thinking support: reasoning can now appear after content in the same streaming turn (GLM-5+, MiniMax-M3). Removed premature flush on content appearance.
83
- - Inject `reasoning_split: true` for MiniMax models to keep reasoning out of `content`.
84
-
85
- ## [1.1.1] - 2026-06-23
86
-
87
- ### Fixed
88
- - Parse MiniMax-M3 `reasoning_details[]` array format and replay it on subsequent turns.
89
-
90
- ## [1.1.0] - 2026-06-23
91
-
92
- ### Fixed
93
- - Restored OpenCode 1.3.x plugin loader compatibility by exporting `{ server: ThinkingFixPlugin }` and adding `oc-plugin: ["server"]`.
94
-
95
- ## [1.0.1] - 2026-06-22
96
-
97
- ### Added
98
- - MIT LICENSE.
99
- - `files` whitelist in `package.json`.
100
-
101
- ## [1.0.0] - 2026-06-22
102
-
103
- ### Added
104
- - Initial release: plugin + proxy + watchdog.
105
- - Support for DeepSeek, Kimi, GLM, and MiMo `reasoning_content` replay.
106
- - Two-proxy architecture (direct providers on port 3457, OpenCode Go on port 3458).
@@ -1,228 +0,0 @@
1
- 'use strict'
2
-
3
- // ─────────────────────────────────────────────────────────────────────────────
4
- // patchMessages, ported from opencode-thinking-fix-universal.ts (lines 31-77)
5
- // ─────────────────────────────────────────────────────────────────────────────
6
- function patchMessages(messages) {
7
- let count = 0
8
-
9
- // Step 1: Self-detect reasoning model
10
- const isReasoningModel = messages.some((wrapper) => {
11
- const msg = wrapper?.info ?? wrapper
12
- return msg?.role === 'assistant' && ('reasoning_content' in msg || 'reasoning' in msg)
13
- })
14
-
15
- // Step 2: Non-reasoning model, exit immediately
16
- if (!isReasoningModel) return 0
17
-
18
- // Step 3: Patch all assistant turns missing required fields
19
- for (const wrapper of messages) {
20
- const msg = wrapper?.info ?? wrapper
21
- if (!msg || msg.role !== 'assistant') continue
22
-
23
- if (!msg.content && msg.content !== '') {
24
- msg.content = msg.tool_calls?.length ? 'call tool' : ''
25
- count++
26
- }
27
-
28
- if (!('reasoning_content' in msg)) {
29
- msg.reasoning_content = ''
30
- count++
31
- }
32
-
33
- if (!('reasoning' in msg)) {
34
- msg.reasoning = ''
35
- count++
36
- }
37
- }
38
-
39
- return count
40
- }
41
-
42
- // ─────────────────────────────────────────────────────────────────────────────
43
- // Helpers
44
- // ─────────────────────────────────────────────────────────────────────────────
45
- function deepClone(o) { return JSON.parse(JSON.stringify(o)) }
46
-
47
- let passed = 0
48
- let failed = 0
49
- const failures = []
50
-
51
- function test(name, fn) {
52
- try {
53
- fn()
54
- passed++
55
- console.log(` PASS: ${name}`)
56
- } catch (e) {
57
- failed++
58
- const msg = `${name}, ${e.message}`
59
- failures.push(msg)
60
- console.log(` FAIL: ${msg}`)
61
- }
62
- }
63
-
64
- function assert(cond, msg) {
65
- if (!cond) throw new Error(msg || 'assertion failed')
66
- }
67
-
68
- // ─────────────────────────────────────────────────────────────────────────────
69
- // Test Cases
70
- // ─────────────────────────────────────────────────────────────────────────────
71
- console.log('\n=== patchMessages Regression Tests ===\n')
72
-
73
- // ── TC1: Native reasoning model (reasoning_content) ─────────────────────────
74
- test('TC1: Native reasoning model detects via reasoning_content', () => {
75
- const msgs = deepClone([
76
- { role: 'user', content: 'hello' },
77
- { role: 'assistant', content: 'hi there', reasoning_content: 'step 1...' },
78
- ])
79
- const result = patchMessages(msgs)
80
- assert(result === 1, `expected 1 patch, got ${result}`)
81
- assert(msgs[1].reasoning === '', 'reasoning should be added as empty string')
82
- assert(msgs[1].reasoning_content === 'step 1...', 'should not overwrite existing reasoning_content')
83
- assert(msgs[0].role === 'user' && msgs[0].content === 'hello', 'user message must not be touched')
84
- })
85
-
86
- // ── TC2: OpenCode Go model (reasoning, NO reasoning_content) ─────────────────
87
- test('TC2: OpenCode Go model detected via reasoning field', () => {
88
- const msgs = deepClone([
89
- { role: 'assistant', content: 'ok', reasoning: 'step 1...' },
90
- ])
91
- const result = patchMessages(msgs)
92
- assert(result === 1, `expected 1 patch, got ${result}`)
93
- assert(msgs[0].reasoning_content === '', 'reasoning_content should be added as empty string')
94
- assert(msgs[0].reasoning === 'step 1...', 'should not overwrite existing reasoning')
95
- })
96
-
97
- // ── TC3: Non-reasoning model (Qwen/GPT/Claude) ──────────────────────────────
98
- test('TC3: Non-reasoning model returns 0 with zero modifications', () => {
99
- const msgs = deepClone([
100
- { role: 'user', content: 'hello' },
101
- { role: 'assistant', content: 'hi' },
102
- ])
103
- const result = patchMessages(msgs)
104
- assert(result === 0, `expected 0 patches, got ${result}`)
105
- assert(!('reasoning_content' in msgs[1]), 'reasoning_content must not be injected')
106
- assert(!('reasoning' in msgs[1]), 'reasoning must not be injected')
107
- })
108
-
109
- // ── TC4: Mixed messages (user + assistant + user) ───────────────────────────
110
- test('TC4: Only assistant messages are patched, user messages untouched', () => {
111
- const msgs = deepClone([
112
- { role: 'user', content: 'q1' },
113
- { role: 'assistant', content: 'a1', reasoning_content: 'thinking' },
114
- { role: 'user', content: 'q2' },
115
- ])
116
- const result = patchMessages(msgs)
117
- assert(result === 1, `expected 1 patch, got ${result}`)
118
- assert(msgs[1].reasoning === '', 'assistant reasoning should be added')
119
- assert(!('reasoning_content' in msgs[0]), 'user message must not get reasoning_content')
120
- assert(!('reasoning' in msgs[0]), 'user message must not get reasoning')
121
- assert(!('reasoning_content' in msgs[2]), 'second user message must not get reasoning_content')
122
- assert(msgs[2].content === 'q2', 'second user message content unchanged')
123
- })
124
-
125
- // ── TC5: Multiple assistant turns ───────────────────────────────────────────
126
- test('TC5: Both assistant messages patched when at least one has reasoning', () => {
127
- const msgs = deepClone([
128
- { role: 'assistant', content: 'first', tool_calls: [{ function: { name: 'f' } }] },
129
- { role: 'assistant', content: 'second', reasoning: 'thinking' },
130
- ])
131
- const result = patchMessages(msgs)
132
- // msgs[0]: +reasoning_content, +reasoning (2). msgs[1]: +reasoning_content (1). Total = 3.
133
- // msgs[0].content is 'first' (truthy) so no content patch.
134
- assert(result === 3, `expected 3 patches, got ${result}`)
135
- assert(msgs[0].reasoning_content === '', 'first assistant should get reasoning_content')
136
- assert(msgs[0].reasoning === '', 'first assistant should get reasoning')
137
- // first assistant content is 'first' (truthy) so NOT patched
138
- assert(msgs[0].content === 'first', 'first assistant content unchanged')
139
- assert(!('content' in Object.getOwnPropertyDescriptor(msgs[1], 'content') || true) || msgs[1].content === 'second', 'second assistant content unchanged')
140
- assert(msgs[1].reasoning_content === '', 'second assistant should get reasoning_content')
141
- assert(msgs[1].reasoning === 'thinking', 'second assistant reasoning unchanged')
142
- })
143
-
144
- // ── TC6: Already complete message ───────────────────────────────────────────
145
- test('TC6: Already complete message, no changes', () => {
146
- const msgs = deepClone([
147
- { role: 'assistant', content: 'done', reasoning_content: 'done', reasoning: 'done' },
148
- ])
149
- const result = patchMessages(msgs)
150
- assert(result === 0, `expected 0 patches, got ${result}`)
151
- assert(msgs[0].reasoning_content === 'done', 'reasoning_content unchanged')
152
- assert(msgs[0].reasoning === 'done', 'reasoning unchanged')
153
- })
154
-
155
- // ── TC7: Wrapper format ({ info: Message }) ─────────────────────────────────
156
- test('TC7: Wrapper format correctly handled', () => {
157
- const msgs = deepClone([
158
- { info: { role: 'user', content: 'hello' } },
159
- { info: { role: 'assistant', content: 'reply', reasoning: 'thinking' } },
160
- ])
161
- const result = patchMessages(msgs)
162
- assert(result === 1, `expected 1 patch, got ${result}`)
163
- assert(msgs[1].info.reasoning_content === '', 'wrapped assistant should get reasoning_content')
164
- assert(msgs[1].info.reasoning === 'thinking', 'wrapped assistant reasoning unchanged')
165
- assert(!('reasoning_content' in msgs[0].info), 'wrapped user must not get reasoning_content')
166
- assert(msgs[0].info.content === 'hello', 'wrapped user content unchanged')
167
- })
168
-
169
- // ── TC8: Empty reasoning_content string ─────────────────────────────────────
170
- test('TC8: Empty reasoning_content still triggers detection', () => {
171
- const msgs = deepClone([
172
- { role: 'assistant', content: 'ok', reasoning_content: '' },
173
- ])
174
- const result = patchMessages(msgs)
175
- assert(result === 1, `expected 1 patch (reasoning), got ${result}`)
176
- assert(msgs[0].reasoning === '', 'reasoning should be added')
177
- assert(msgs[0].reasoning_content === '', 'reasoning_content stays empty')
178
- })
179
-
180
- // ── TC9: Empty messages array ───────────────────────────────────────────────
181
- test('TC9: Empty messages array returns 0', () => {
182
- const result = patchMessages([])
183
- assert(result === 0, `expected 0, got ${result}`)
184
- })
185
-
186
- // ── TC10: Non-reasoning model with tool_calls ────────────────────────────────
187
- test('TC10: Non-reasoning model with tool_calls, no injection', () => {
188
- const msgs = deepClone([
189
- { role: 'assistant', content: '', tool_calls: [{ function: { name: 'f' } }] },
190
- ])
191
- const result = patchMessages(msgs)
192
- assert(result === 0, `expected 0, got ${result}`)
193
- assert(!('reasoning_content' in msgs[0]), 'must not inject reasoning_content')
194
- })
195
-
196
- // ── TC11: Reasoning model with empty content + tool_calls ───────────────────
197
- test('TC11: Reasoning model patches content to "call tool"', () => {
198
- const msgs = deepClone([
199
- { role: 'assistant', content: '', tool_calls: [{ function: { name: 'f' } }], reasoning_content: 'thinking' },
200
- ])
201
- const result = patchMessages(msgs)
202
- // content is '' (empty string), guard `!msg.content && msg.content !== ''` skips it
203
- // because '' !== '' is false. The original code treats '' as valid content.
204
- // Only reasoning is patched (reasoning_content and content are already present/invalid).
205
- assert(result === 1, `expected 1 patch (reasoning), got ${result}`)
206
- assert(msgs[0].content === '', 'content stays empty string')
207
- assert(msgs[0].reasoning === '', 'reasoning added')
208
- assert(msgs[0].reasoning_content === 'thinking', 'reasoning_content preserved')
209
- })
210
-
211
- // ── TC12: Null wrapper ──────────────────────────────────────────────────────
212
- test('TC12: Null/undefined wrapper in array is skipped gracefully', () => {
213
- const msgs = deepClone([
214
- null,
215
- { role: 'assistant', content: 'hi', reasoning_content: 'think' },
216
- undefined,
217
- ])
218
- const result = patchMessages(msgs)
219
- assert(result >= 1, `expected at least 1 patch, got ${result}`)
220
- })
221
-
222
- // ─────────────────────────────────────────────────────────────────────────────
223
- // Summary
224
- // ─────────────────────────────────────────────────────────────────────────────
225
- const total = passed + failed
226
- console.log(`\n=== Results: ${passed}/${total} passed, ${failed} failed ===\n`)
227
-
228
- process.exit(failed > 0 ? 1 : 0)