@goodandready/dsh-clinebot 0.5.32 → 0.6.0
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 +31 -0
- package/README.md +63 -36
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/lib/adapter-messages.js +237 -0
- package/lib/adapter-stream.js +292 -0
- package/lib/adapter.js +616 -0
- package/lib/client.js +2564 -924
- package/lib/cline-client.js +15 -6
- package/lib/config.js +266 -233
- package/lib/credential-refs.js +104 -84
- package/lib/index.js +569 -502
- package/lib/model-prices.js +117 -0
- package/lib/provider-sync.js +404 -387
- package/lib/proxy-token.js +187 -140
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/proxy.js +8 -27
- package/lib/routes/settings.js +63 -2
- package/lib/services.js +55 -0
- package/lib/stats-storage.js +364 -166
- package/lib/transport/cline-transport.js +314 -0
- package/lib/tui-settings.js +158 -0
- package/lib/updater.js +5 -1
- package/package.json +113 -77
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert ClinePass OpenAI-compatible stream events into DSH `StreamChunk`s.
|
|
3
|
+
*
|
|
4
|
+
* The adapter owns the block lifecycle: it opens a block on the first delta,
|
|
5
|
+
* forwards deltas, then closes every open block before the terminal finish.
|
|
6
|
+
* Tool-call arguments remain raw JSON strings, as required by the DSH protocol.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LlmError, ToolCallId } from '@deepseek-ai/dsh-llm'
|
|
10
|
+
import { extractUsage, isAbortError } from './transport/cline-transport.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep only fields the DSH `TokenUsage` contract accepts.
|
|
14
|
+
* @param {ReturnType<typeof extractUsage>} usage
|
|
15
|
+
* @returns {Record<string, number>}
|
|
16
|
+
*/
|
|
17
|
+
function wireUsage(usage) {
|
|
18
|
+
return {
|
|
19
|
+
inputTokens: usage.inputTokens,
|
|
20
|
+
outputTokens: usage.outputTokens,
|
|
21
|
+
...(usage.totalTokens !== undefined ? { totalTokens: usage.totalTokens } : {}),
|
|
22
|
+
...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
23
|
+
...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),
|
|
24
|
+
...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map a provider finish reason to a DSH finish chunk.
|
|
30
|
+
* @param {string} finishReason
|
|
31
|
+
* @param {boolean} hasToolCalls
|
|
32
|
+
* @returns {{ type: 'finish', reason: Record<string, unknown> }}
|
|
33
|
+
*/
|
|
34
|
+
function finishChunk(finishReason, hasToolCalls) {
|
|
35
|
+
if (hasToolCalls || finishReason === 'tool_calls') {
|
|
36
|
+
return { type: 'finish', reason: { kind: 'tool-calls' } }
|
|
37
|
+
}
|
|
38
|
+
if (finishReason === 'length' || finishReason === 'max_tokens') {
|
|
39
|
+
return { type: 'finish', reason: { kind: 'max-tokens' } }
|
|
40
|
+
}
|
|
41
|
+
return { type: 'finish', reason: { kind: 'stop' } }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stream one OpenAI-compatible SSE response as DSH chunks.
|
|
46
|
+
*
|
|
47
|
+
* @param {AsyncIterable<Record<string, unknown> | { done: true } | { raw: string }>} events
|
|
48
|
+
* @param {{
|
|
49
|
+
* signal?: AbortSignal,
|
|
50
|
+
* onUsage?: (usage: ReturnType<typeof extractUsage>) => void,
|
|
51
|
+
* onFirstToken?: () => void,
|
|
52
|
+
* }} [options]
|
|
53
|
+
* @returns {AsyncGenerator<Record<string, unknown>>}
|
|
54
|
+
*/
|
|
55
|
+
export async function* streamOpenAiChunks(events, { signal, onUsage, onFirstToken } = {}) {
|
|
56
|
+
let nextIndex = 0
|
|
57
|
+
let textIndex = -1
|
|
58
|
+
let reasoningIndex = -1
|
|
59
|
+
let text = ''
|
|
60
|
+
let reasoning = ''
|
|
61
|
+
const toolBlocks = new Map()
|
|
62
|
+
let finishReason = ''
|
|
63
|
+
let sawContent = false
|
|
64
|
+
let aborted = false
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
for await (const event of events) {
|
|
68
|
+
if (signal?.aborted) {
|
|
69
|
+
aborted = true
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
if (!event || typeof event !== 'object') continue
|
|
73
|
+
if (event.done === true) break
|
|
74
|
+
if (event.raw !== undefined) continue
|
|
75
|
+
if (event.error) {
|
|
76
|
+
const error = event.error
|
|
77
|
+
throw new LlmError(
|
|
78
|
+
String(error.message || 'ClinePass stream returned an error'),
|
|
79
|
+
String(error.code || 'PROVIDER_ERROR'),
|
|
80
|
+
{
|
|
81
|
+
...(Number.isFinite(Number(error.status)) ? { status: Number(error.status) } : {}),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (event.usage) {
|
|
87
|
+
const usage = extractUsage(event.usage)
|
|
88
|
+
onUsage?.(usage)
|
|
89
|
+
yield { type: 'usage', usage: wireUsage(usage) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const choice = Array.isArray(event.choices) ? event.choices[0] : null
|
|
93
|
+
if (!choice) continue
|
|
94
|
+
const delta = choice.delta && typeof choice.delta === 'object' ? choice.delta : {}
|
|
95
|
+
|
|
96
|
+
const reasoningText = typeof delta.reasoning_content === 'string'
|
|
97
|
+
? delta.reasoning_content
|
|
98
|
+
: (typeof delta.reasoning === 'string' ? delta.reasoning : '')
|
|
99
|
+
if (reasoningText) {
|
|
100
|
+
if (reasoningIndex < 0) {
|
|
101
|
+
reasoningIndex = nextIndex++
|
|
102
|
+
yield { type: 'block-start', index: reasoningIndex, blockType: 'reasoning' }
|
|
103
|
+
}
|
|
104
|
+
reasoning += reasoningText
|
|
105
|
+
if (!sawContent) onFirstToken?.()
|
|
106
|
+
sawContent = true
|
|
107
|
+
yield { type: 'reasoning-delta', index: reasoningIndex, text: reasoningText }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const contentText = typeof delta.content === 'string' ? delta.content : ''
|
|
111
|
+
if (contentText) {
|
|
112
|
+
if (textIndex < 0) {
|
|
113
|
+
textIndex = nextIndex++
|
|
114
|
+
yield { type: 'block-start', index: textIndex, blockType: 'text' }
|
|
115
|
+
}
|
|
116
|
+
text += contentText
|
|
117
|
+
if (!sawContent) onFirstToken?.()
|
|
118
|
+
sawContent = true
|
|
119
|
+
yield { type: 'text-delta', index: textIndex, text: contentText }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
123
|
+
for (const call of delta.tool_calls) {
|
|
124
|
+
if (!call || typeof call !== 'object') continue
|
|
125
|
+
const providerIndex = Number.isInteger(call.index) ? call.index : toolBlocks.size
|
|
126
|
+
let block = toolBlocks.get(providerIndex)
|
|
127
|
+
if (!block) {
|
|
128
|
+
block = {
|
|
129
|
+
index: nextIndex++,
|
|
130
|
+
id: typeof call.id === 'string' && call.id ? call.id : `clinebot_call_${providerIndex}`,
|
|
131
|
+
name: '',
|
|
132
|
+
arguments: '',
|
|
133
|
+
}
|
|
134
|
+
toolBlocks.set(providerIndex, block)
|
|
135
|
+
yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
|
|
136
|
+
}
|
|
137
|
+
if (typeof call.id === 'string' && call.id) block.id = call.id
|
|
138
|
+
const name = typeof call.function?.name === 'string' ? call.function.name : ''
|
|
139
|
+
if (name) block.name = name
|
|
140
|
+
const argumentsDelta = typeof call.function?.arguments === 'string' ? call.function.arguments : ''
|
|
141
|
+
block.arguments += argumentsDelta
|
|
142
|
+
if (!sawContent) onFirstToken?.()
|
|
143
|
+
sawContent = true
|
|
144
|
+
yield {
|
|
145
|
+
type: 'tool-call-delta',
|
|
146
|
+
index: block.index,
|
|
147
|
+
id: ToolCallId(block.id),
|
|
148
|
+
...(name ? { name } : {}),
|
|
149
|
+
argumentsDelta,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof choice.finish_reason === 'string' && choice.finish_reason) {
|
|
155
|
+
finishReason = choice.finish_reason
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isAbortError(error)) {
|
|
160
|
+
yield { type: 'finish', reason: { kind: 'aborted', failure: { message: 'Request aborted', code: 'ABORTED' } } }
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
throw error
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (aborted || signal?.aborted) {
|
|
167
|
+
yield { type: 'finish', reason: { kind: 'aborted', failure: { message: 'Request aborted', code: 'ABORTED' } } }
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Close every block that was opened. DSH's assembler consumes block-end as
|
|
172
|
+
// the authoritative final content of that block.
|
|
173
|
+
if (textIndex >= 0) {
|
|
174
|
+
yield { type: 'block-end', index: textIndex, block: { type: 'text', text } }
|
|
175
|
+
}
|
|
176
|
+
if (reasoningIndex >= 0) {
|
|
177
|
+
yield { type: 'block-end', index: reasoningIndex, block: { type: 'reasoning', text: reasoning } }
|
|
178
|
+
}
|
|
179
|
+
for (const block of toolBlocks.values()) {
|
|
180
|
+
yield {
|
|
181
|
+
type: 'block-end',
|
|
182
|
+
index: block.index,
|
|
183
|
+
block: {
|
|
184
|
+
type: 'tool-call',
|
|
185
|
+
id: ToolCallId(block.id),
|
|
186
|
+
name: block.name,
|
|
187
|
+
arguments: block.arguments,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!sawContent) {
|
|
193
|
+
yield {
|
|
194
|
+
type: 'finish',
|
|
195
|
+
reason: {
|
|
196
|
+
kind: 'error',
|
|
197
|
+
failure: { message: 'ClinePass returned an empty completion', code: 'EMPTY_RESPONSE' },
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
yield finishChunk(finishReason, toolBlocks.size > 0)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert a non-streaming OpenAI-compatible completion into DSH chunks.
|
|
208
|
+
* Used as a fallback when ClinePass ignores `stream: true` or returns JSON.
|
|
209
|
+
*
|
|
210
|
+
* @param {Record<string, unknown>} json
|
|
211
|
+
* @param {{ onUsage?: (usage: ReturnType<typeof extractUsage>) => void, onFirstToken?: () => void }} [options]
|
|
212
|
+
* @returns {AsyncGenerator<Record<string, unknown>>}
|
|
213
|
+
*/
|
|
214
|
+
export async function* streamJsonCompletion(json, { onUsage, onFirstToken } = {}) {
|
|
215
|
+
if (json?.error) {
|
|
216
|
+
throw new LlmError(
|
|
217
|
+
String(json.error.message || 'ClinePass returned an error'),
|
|
218
|
+
String(json.error.code || 'PROVIDER_ERROR'),
|
|
219
|
+
Number.isFinite(Number(json.error.status)) ? { status: Number(json.error.status) } : {},
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
const choice = Array.isArray(json?.choices) ? json.choices[0] : null
|
|
223
|
+
if (!choice) {
|
|
224
|
+
yield {
|
|
225
|
+
type: 'finish',
|
|
226
|
+
reason: {
|
|
227
|
+
kind: 'error',
|
|
228
|
+
failure: { message: 'ClinePass returned an empty completion', code: 'EMPTY_RESPONSE' },
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const message = choice.message && typeof choice.message === 'object' ? choice.message : {}
|
|
234
|
+
let nextIndex = 0
|
|
235
|
+
let sawContent = false
|
|
236
|
+
|
|
237
|
+
if (json.usage) {
|
|
238
|
+
const usage = extractUsage(json.usage)
|
|
239
|
+
onUsage?.(usage)
|
|
240
|
+
yield { type: 'usage', usage: wireUsage(usage) }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const reasoning = typeof message.reasoning_content === 'string'
|
|
244
|
+
? message.reasoning_content
|
|
245
|
+
: (typeof message.reasoning === 'string' ? message.reasoning : '')
|
|
246
|
+
if (reasoning) {
|
|
247
|
+
const index = nextIndex++
|
|
248
|
+
yield { type: 'block-start', index, blockType: 'reasoning' }
|
|
249
|
+
yield { type: 'reasoning-delta', index, text: reasoning }
|
|
250
|
+
yield { type: 'block-end', index, block: { type: 'reasoning', text: reasoning } }
|
|
251
|
+
sawContent = true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const content = typeof message.content === 'string' ? message.content : ''
|
|
255
|
+
if (content) {
|
|
256
|
+
const index = nextIndex++
|
|
257
|
+
yield { type: 'block-start', index, blockType: 'text' }
|
|
258
|
+
yield { type: 'text-delta', index, text: content }
|
|
259
|
+
yield { type: 'block-end', index, block: { type: 'text', text: content } }
|
|
260
|
+
sawContent = true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let hasToolCalls = false
|
|
264
|
+
if (Array.isArray(message.tool_calls)) {
|
|
265
|
+
for (const call of message.tool_calls) {
|
|
266
|
+
if (!call || typeof call !== 'object') continue
|
|
267
|
+
hasToolCalls = true
|
|
268
|
+
const id = typeof call.id === 'string' && call.id ? call.id : `clinebot_call_${nextIndex}`
|
|
269
|
+
const name = typeof call.function?.name === 'string' ? call.function.name : ''
|
|
270
|
+
const args = typeof call.function?.arguments === 'string' ? call.function.arguments : ''
|
|
271
|
+
const index = nextIndex++
|
|
272
|
+
yield { type: 'block-start', index, blockType: 'tool-call' }
|
|
273
|
+
yield { type: 'tool-call-delta', index, id: ToolCallId(id), ...(name ? { name } : {}), argumentsDelta: args }
|
|
274
|
+
yield { type: 'block-end', index, block: { type: 'tool-call', id: ToolCallId(id), name, arguments: args } }
|
|
275
|
+
sawContent = true
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!sawContent) {
|
|
280
|
+
yield {
|
|
281
|
+
type: 'finish',
|
|
282
|
+
reason: {
|
|
283
|
+
kind: 'error',
|
|
284
|
+
failure: { message: 'ClinePass returned an empty completion', code: 'EMPTY_RESPONSE' },
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
onFirstToken?.()
|
|
291
|
+
yield finishChunk(String(choice.finish_reason || ''), hasToolCalls)
|
|
292
|
+
}
|