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.
- package/README.md +148 -57
- package/package.json +3 -7
- package/proxy/core.js +255 -0
- package/proxy/proxy.js +216 -330
- package/CHANGELOG.md +0 -106
- package/tests/test-plugin.js +0 -228
- package/tests/test-proxy.js +0 -359
package/tests/test-proxy.js
DELETED
|
@@ -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)
|