opencode-thinking-fix 2.0.1 → 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.
@@ -1,359 +0,0 @@
1
- 'use strict'
2
-
3
- // ── Relevant functions extracted from proxy.js ──
4
-
5
- const ROUTES = {
6
- deepseek: { base: 'https://api.deepseek.com', reasoning: true },
7
- kimi: { base: 'https://api.moonshot.ai/v1', reasoning: true },
8
- moonshot: { base: 'https://api.moonshot.ai/v1', reasoning: true },
9
- glm: { base: 'https://open.bigmodel.cn/api/paas/v4', reasoning: true },
10
- zhipu: { base: 'https://open.bigmodel.cn/api/paas/v4', reasoning: true },
11
- minimax: { base: 'https://api.minimax.io/v1', reasoning: true },
12
- mimo: { base: 'https://api.minimax.io/v1', reasoning: true },
13
- gpt: { base: 'https://api.openai.com', reasoning: false },
14
- o1: { base: 'https://api.openai.com', reasoning: false },
15
- claude: { base: 'https://api.anthropic.com', reasoning: false },
16
- anthropic: { base: 'https://api.anthropic.com', reasoning: false },
17
- qwen: { base: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', reasoning: false },
18
- gemini: { base: 'https://generativelanguage.googleapis.com/v1beta/openai', reasoning: false },
19
- llama: { base: 'https://api.together.xyz', reasoning: false },
20
- mistral: { base: 'https://api.mistral.ai', reasoning: false },
21
- }
22
-
23
- const DEFAULT_ROUTE = { base: 'https://api.deepseek.com', reasoning: false }
24
-
25
- function route(modelName) {
26
- if (!modelName) return DEFAULT_ROUTE
27
- const lower = modelName.toLowerCase()
28
- for (const [prefix, entry] of Object.entries(ROUTES)) {
29
- if (lower.startsWith(prefix)) return entry
30
- }
31
- return DEFAULT_ROUTE
32
- }
33
-
34
- const cache = new Map()
35
-
36
- function getSessionCache(sessionId) {
37
- if (!cache.has(sessionId)) cache.set(sessionId, new Map())
38
- return cache.get(sessionId)
39
- }
40
-
41
- function patchRequestBody(body, sessionId) {
42
- let parsed
43
- try {
44
- parsed = JSON.parse(body)
45
- } catch {
46
- return body
47
- }
48
-
49
- if (!Array.isArray(parsed.messages)) return body
50
-
51
- const sessionCache = getSessionCache(sessionId)
52
- let assistantIndex = 0
53
- let modified = false
54
-
55
- for (const msg of parsed.messages) {
56
- if (msg.role !== 'assistant') continue
57
-
58
- if (!msg.content && msg.content !== '') {
59
- msg.content = msg.tool_calls?.length ? 'call tool' : ''
60
- modified = true
61
- }
62
-
63
- if (!msg.reasoning_content) {
64
- const cached = sessionCache.get(assistantIndex)
65
- msg.reasoning_content = cached ?? ''
66
- modified = true
67
- }
68
-
69
- if (!msg.reasoning) {
70
- const cached = sessionCache.get(assistantIndex)
71
- msg.reasoning = cached ?? ''
72
- modified = true
73
- }
74
-
75
- assistantIndex++
76
- }
77
-
78
- return modified ? JSON.stringify(parsed) : body
79
- }
80
-
81
- function createStreamParser(sessionId, onComplete) {
82
- let buffer = ''
83
- let reasoningBuffer = ''
84
- let assistantIndex = 0
85
- let inReasoning = false
86
-
87
- return {
88
- feed(chunk) {
89
- buffer += chunk.toString()
90
- const lines = buffer.split('\n')
91
- buffer = lines.pop()
92
-
93
- for (const line of lines) {
94
- if (!line.startsWith('data: ')) continue
95
- const data = line.slice(6).trim()
96
- if (data === '[DONE]') {
97
- if (reasoningBuffer) {
98
- onComplete(assistantIndex, reasoningBuffer)
99
- reasoningBuffer = ''
100
- }
101
- continue
102
- }
103
- try {
104
- const parsed = JSON.parse(data)
105
- const delta = parsed.choices?.[0]?.delta
106
- if (!delta) continue
107
-
108
- if (typeof delta.reasoning_content === 'string') {
109
- reasoningBuffer += delta.reasoning_content
110
- inReasoning = true
111
- }
112
-
113
- if (typeof delta.reasoning === 'string') {
114
- reasoningBuffer += delta.reasoning
115
- inReasoning = true
116
- }
117
-
118
- if (typeof delta.content === 'string' && delta.content && inReasoning) {
119
- if (reasoningBuffer) {
120
- onComplete(assistantIndex, reasoningBuffer)
121
- reasoningBuffer = ''
122
- inReasoning = false
123
- }
124
- }
125
-
126
- if (parsed.choices?.[0]?.finish_reason) {
127
- if (reasoningBuffer) {
128
- onComplete(assistantIndex, reasoningBuffer)
129
- reasoningBuffer = ''
130
- inReasoning = false
131
- }
132
- assistantIndex++
133
- }
134
- } catch {
135
- // skip malformed
136
- }
137
- }
138
- },
139
- flush() {
140
- if (reasoningBuffer) {
141
- onComplete(assistantIndex, reasoningBuffer)
142
- reasoningBuffer = ''
143
- }
144
- }
145
- }
146
- }
147
-
148
- // ── Test runner ──
149
-
150
- let testsPassed = 0
151
- let testsFailed = 0
152
- const failures = []
153
-
154
- function assert(condition, description) {
155
- if (condition) {
156
- testsPassed++
157
- console.log(' PASS: ' + description)
158
- } else {
159
- testsFailed++
160
- failures.push(description)
161
- console.log(' FAIL: ' + description)
162
- }
163
- }
164
-
165
- function assertEqual(actual, expected, description) {
166
- const a = JSON.stringify(actual)
167
- const e = JSON.stringify(expected)
168
- if (a === e) {
169
- testsPassed++
170
- console.log(' PASS: ' + description)
171
- } else {
172
- testsFailed++
173
- failures.push(description + ' -- expected ' + e + ', got ' + a)
174
- console.log(' FAIL: ' + description + ' -- expected ' + e + ', got ' + a)
175
- }
176
- }
177
-
178
- function clearCache() {
179
- cache.clear()
180
- }
181
-
182
- // ═════════════════════════════════════════════════════════════════════════
183
- // route() tests
184
- // ═════════════════════════════════════════════════════════════════════════
185
-
186
- console.log('\n=== route() tests ===\n')
187
-
188
- // 11: deepseek-reasoner
189
- let r = route('deepseek-reasoner')
190
- assertEqual(r.base, 'https://api.deepseek.com', '11. deepseek-reasoner routes to deepseek')
191
- assert(r.reasoning === true, '11b. deepseek-reasoner reasoning=true')
192
-
193
- // 12: kimi-k2.6
194
- r = route('kimi-k2.6')
195
- assertEqual(r.base, 'https://api.moonshot.ai/v1', '12. kimi-k2.6 routes to kimi')
196
- assert(r.reasoning === true, '12b. kimi-k2.6 reasoning=true')
197
-
198
- // 13: glm-5.2
199
- r = route('glm-5.2')
200
- assertEqual(r.base, 'https://open.bigmodel.cn/api/paas/v4', '13. glm-5.2 routes to glm')
201
- assert(r.reasoning === true, '13b. glm-5.2 reasoning=true')
202
-
203
- // 14: gpt-4
204
- r = route('gpt-4')
205
- assertEqual(r.base, 'https://api.openai.com', '14. gpt-4 routes to gpt')
206
- assert(r.reasoning === false, '14b. gpt-4 reasoning=false')
207
-
208
- // 15: unknown-model
209
- r = route('unknown-model')
210
- assertEqual(r.base, 'https://api.deepseek.com', '15. unknown-model -> DEFAULT_ROUTE')
211
- assert(r.reasoning === false, '15b. unknown-model reasoning=false')
212
-
213
- // ═════════════════════════════════════════════════════════════════════════
214
- // patchRequestBody() tests
215
- // ═════════════════════════════════════════════════════════════════════════
216
-
217
- console.log('\n=== patchRequestBody() tests ===\n')
218
-
219
- // 1: reasoning_content injection
220
- clearCache()
221
- getSessionCache('test1').set(0, 'step 1...')
222
- const b1 = JSON.stringify({
223
- messages: [{ role: 'assistant', content: 'hi', reasoning_content: '' }]
224
- })
225
- const r1 = JSON.parse(patchRequestBody(b1, 'test1'))
226
- assertEqual(r1.messages[0].reasoning_content, 'step 1...', '1. reasoning_content injected from cache')
227
-
228
- // 2: reasoning injection (OpenCode Go)
229
- clearCache()
230
- getSessionCache('test2').set(0, 'step 1...')
231
- const b2 = JSON.stringify({
232
- messages: [{ role: 'assistant', content: 'hi', reasoning: '' }]
233
- })
234
- const r2 = JSON.parse(patchRequestBody(b2, 'test2'))
235
- assertEqual(r2.messages[0].reasoning, 'step 1...', '2. reasoning injected from cache (OpenCode Go)')
236
-
237
- // 3: BOTH fields injection
238
- clearCache()
239
- getSessionCache('test3').set(0, 'step 1...')
240
- const b3 = JSON.stringify({
241
- messages: [{ role: 'assistant', content: 'hi', reasoning_content: '', reasoning: '' }]
242
- })
243
- const r3 = JSON.parse(patchRequestBody(b3, 'test3'))
244
- assertEqual(r3.messages[0].reasoning_content, 'step 1...', '3a. both fields: reasoning_content injected')
245
- assertEqual(r3.messages[0].reasoning, 'step 1...', '3b. both fields: reasoning injected')
246
-
247
- // 4: No cache available
248
- clearCache()
249
- const b4 = JSON.stringify({
250
- messages: [{ role: 'assistant', content: 'hi' }]
251
- })
252
- const r4 = JSON.parse(patchRequestBody(b4, 'test4'))
253
- assertEqual(r4.messages[0].reasoning_content, '', '4a. no cache: reasoning_content becomes ""')
254
- assertEqual(r4.messages[0].reasoning, '', '4b. no cache: reasoning becomes ""')
255
-
256
- // 5: User messages untouched
257
- clearCache()
258
- getSessionCache('test5').set(0, 'step 1...')
259
- const b5 = JSON.stringify({
260
- messages: [
261
- { role: 'user', content: 'hello' },
262
- { role: 'assistant', content: 'hi', reasoning_content: '' }
263
- ]
264
- })
265
- const r5 = JSON.parse(patchRequestBody(b5, 'test5'))
266
- assertEqual(r5.messages[0].role, 'user', '5a. user role unchanged')
267
- assertEqual(r5.messages[0].content, 'hello', '5b. user content unchanged')
268
- assert(!('reasoning_content' in r5.messages[0]) && !('reasoning' in r5.messages[0]), '5c. user has no reasoning fields')
269
- assertEqual(r5.messages[1].reasoning_content, 'step 1...', '5d. assistant reasoning_content injected')
270
-
271
- // 6: Multiple assistant turns
272
- clearCache()
273
- getSessionCache('test6').set(0, 'first reasoning')
274
- getSessionCache('test6').set(1, 'second reasoning')
275
- const b6 = JSON.stringify({
276
- messages: [
277
- { role: 'user', content: 'q1' },
278
- { role: 'assistant', content: 'a1', reasoning_content: '' },
279
- { role: 'user', content: 'q2' },
280
- { role: 'assistant', content: 'a2', reasoning_content: '' }
281
- ]
282
- })
283
- const r6 = JSON.parse(patchRequestBody(b6, 'test6'))
284
- assertEqual(r6.messages[1].reasoning_content, 'first reasoning', '6a. first assistant gets its cached reasoning')
285
- assertEqual(r6.messages[3].reasoning_content, 'second reasoning', '6b. second assistant gets its cached reasoning')
286
-
287
- // ═════════════════════════════════════════════════════════════════════════
288
- // createStreamParser() tests
289
- // ═════════════════════════════════════════════════════════════════════════
290
-
291
- console.log('\n=== createStreamParser() tests ===\n')
292
-
293
- // 7: delta.reasoning_content accumulation
294
- const res7 = []
295
- const p7 = createStreamParser('test7', function(idx, reasoning) {
296
- res7.push({ idx: idx, reasoning: reasoning })
297
- })
298
- p7.feed('data: {"choices":[{"delta":{"reasoning_content":"step "}}]}\n')
299
- p7.feed('data: {"choices":[{"delta":{"reasoning_content":"1"}}]}\n')
300
- p7.feed('data: [DONE]\n')
301
- assertEqual(res7.length, 1, '7a. onComplete called once')
302
- if (res7.length > 0) {
303
- assertEqual(res7[0].reasoning, 'step 1', '7b. reasoning_content accumulated as "step 1"')
304
- assertEqual(res7[0].idx, 0, '7c. index is 0')
305
- }
306
-
307
- // 8: delta.reasoning accumulation (OpenCode Go)
308
- const res8 = []
309
- const p8 = createStreamParser('test8', function(idx, reasoning) {
310
- res8.push({ idx: idx, reasoning: reasoning })
311
- })
312
- p8.feed('data: {"choices":[{"delta":{"reasoning":"think "}}]}\n')
313
- p8.feed('data: {"choices":[{"delta":{"reasoning":"about it"}}]}\n')
314
- p8.feed('data: [DONE]\n')
315
- assertEqual(res8.length, 1, '8a. onComplete called once')
316
- if (res8.length > 0) {
317
- assertEqual(res8[0].reasoning, 'think about it', '8b. reasoning accumulated as "think about it"')
318
- assertEqual(res8[0].idx, 0, '8c. index is 0')
319
- }
320
-
321
- // 9: Content after reasoning triggers flush
322
- const res9 = []
323
- const p9 = createStreamParser('test9', function(idx, reasoning) {
324
- res9.push({ idx: idx, reasoning: reasoning })
325
- })
326
- p9.feed('data: {"choices":[{"delta":{"reasoning_content":"step 1"}}]}\n')
327
- p9.feed('data: {"choices":[{"delta":{"content":"Hello"}}]}\n')
328
- assertEqual(res9.length, 1, '9a. onComplete called when content starts')
329
- if (res9.length > 0) {
330
- assertEqual(res9[0].reasoning, 'step 1', '9b. reasoning is "step 1"')
331
- assertEqual(res9[0].idx, 0, '9c. index is 0')
332
- }
333
-
334
- // 10: finish_reason triggers flush
335
- const res10 = []
336
- const p10 = createStreamParser('test10', function(idx, reasoning) {
337
- res10.push({ idx: idx, reasoning: reasoning })
338
- })
339
- p10.feed('data: {"choices":[{"delta":{"reasoning_content":"step 1"}}]}\n')
340
- p10.feed('data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n')
341
- assertEqual(res10.length, 1, '10a. onComplete called on finish_reason')
342
- if (res10.length > 0) {
343
- assertEqual(res10[0].reasoning, 'step 1', '10b. reasoning is "step 1"')
344
- assertEqual(res10[0].idx, 0, '10c. index is 0 (before increment)')
345
- }
346
-
347
- // ═════════════════════════════════════════════════════════════════════════
348
- // Summary
349
- // ═════════════════════════════════════════════════════════════════════════
350
-
351
- console.log('\n' + '='.repeat(50))
352
- console.log('Tests: ' + (testsPassed + testsFailed) + ' | Passed: ' + testsPassed + ' | Failed: ' + testsFailed)
353
- if (failures.length > 0) {
354
- console.log('\nFailures:')
355
- failures.forEach(function(f) { console.log(' - ' + f) })
356
- }
357
- console.log()
358
-
359
- process.exit(testsFailed > 0 ? 1 : 0)