@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
package/lib/provider-sync.js
CHANGED
|
@@ -1,387 +1,404 @@
|
|
|
1
|
-
import { getStatsSummary } from './stats-storage.js'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
PROVIDER_ID,
|
|
6
|
-
PROVIDER_DISPLAY_NAME,
|
|
7
|
-
getAllModels,
|
|
8
|
-
saveModelsDiskCache,
|
|
9
|
-
loadModelsDiskCache,
|
|
10
|
-
OBSOLETE_MODEL_ID_MAP,
|
|
11
|
-
migrateModelEntry,
|
|
12
|
-
isCatalogDifferent,
|
|
13
|
-
} from './models.js'
|
|
14
|
-
import {
|
|
15
|
-
probeHealth,
|
|
16
|
-
fetchUsageLimits,
|
|
17
|
-
sessionStats,
|
|
18
|
-
usageCache,
|
|
19
|
-
buildPiAiProvider,
|
|
20
|
-
DEFAULT_API_KEY_ENV,
|
|
21
|
-
} from './cline-client.js'
|
|
22
|
-
import { publicConfig, plainConfig, Config, LLM_PI_AI_NS } from './config.js'
|
|
23
|
-
import { publicUsage } from './http.js'
|
|
24
|
-
import { resolveKeyValue } from './credential-refs.js'
|
|
25
|
-
import { resolveAccountPool, getLastRotation } from './account-pool.js'
|
|
26
|
-
import { getLocalProxyToken, LOCAL_PROXY_KEY_ENV } from './proxy-token.js'
|
|
27
|
-
import { saveCredentialKey } from './credential-refs.js'
|
|
28
|
-
import { getProxyPort, detectHostWebServerFence } from './proxy-server.js'
|
|
29
|
-
|
|
30
|
-
export function resolvePathWithHome(p) {
|
|
31
|
-
let guard = 0
|
|
32
|
-
while (guard < 5 && p) {
|
|
33
|
-
if (typeof p === 'function') {
|
|
34
|
-
try { p = p() } catch { p = ''; break }
|
|
35
|
-
guard += 1
|
|
36
|
-
} else if (typeof p === 'object' && typeof p.get === 'function') {
|
|
37
|
-
try { p = p.get() } catch { p = ''; break }
|
|
38
|
-
guard += 1
|
|
39
|
-
} else {
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (typeof p !== 'string') return ''
|
|
44
|
-
if (p.startsWith('~/') || p === '~') {
|
|
45
|
-
const home = process.env.HOME || os.homedir()
|
|
46
|
-
return path.join(home, p.slice(1))
|
|
47
|
-
}
|
|
48
|
-
return p
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function checkRegisteredInPiAi(ctx) {
|
|
52
|
-
const settings = ctx?.get?.('settings')
|
|
53
|
-
try {
|
|
54
|
-
if (typeof settings?.describe === 'function') {
|
|
55
|
-
const descriptors = settings.describe()
|
|
56
|
-
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
57
|
-
if (piAiDesc?.value?.providers?.[PROVIDER_ID]) return true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const llm = ctx?.get?.('llm')
|
|
61
|
-
if (typeof llm?.listProviders === 'function') {
|
|
62
|
-
const list = await llm.listProviders()
|
|
63
|
-
if (Array.isArray(list) && list.some((p) => (p?.id || p) === PROVIDER_ID)) {
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export async function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const pub = publicConfig(cfg)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
await
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1
|
+
import { getStatsSummary } from './stats-storage.js'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
PROVIDER_ID,
|
|
6
|
+
PROVIDER_DISPLAY_NAME,
|
|
7
|
+
getAllModels,
|
|
8
|
+
saveModelsDiskCache,
|
|
9
|
+
loadModelsDiskCache,
|
|
10
|
+
OBSOLETE_MODEL_ID_MAP,
|
|
11
|
+
migrateModelEntry,
|
|
12
|
+
isCatalogDifferent,
|
|
13
|
+
} from './models.js'
|
|
14
|
+
import {
|
|
15
|
+
probeHealth,
|
|
16
|
+
fetchUsageLimits,
|
|
17
|
+
sessionStats,
|
|
18
|
+
usageCache,
|
|
19
|
+
buildPiAiProvider,
|
|
20
|
+
DEFAULT_API_KEY_ENV,
|
|
21
|
+
} from './cline-client.js'
|
|
22
|
+
import { publicConfig, plainConfig, Config, LLM_PI_AI_NS } from './config.js'
|
|
23
|
+
import { publicUsage } from './http.js'
|
|
24
|
+
import { resolveKeyValue } from './credential-refs.js'
|
|
25
|
+
import { resolveAccountPool, getLastRotation } from './account-pool.js'
|
|
26
|
+
import { getLocalProxyToken, LOCAL_PROXY_KEY_ENV } from './proxy-token.js'
|
|
27
|
+
import { saveCredentialKey } from './credential-refs.js'
|
|
28
|
+
import { getProxyPort, detectHostWebServerFence } from './proxy-server.js'
|
|
29
|
+
|
|
30
|
+
export function resolvePathWithHome(p) {
|
|
31
|
+
let guard = 0
|
|
32
|
+
while (guard < 5 && p) {
|
|
33
|
+
if (typeof p === 'function') {
|
|
34
|
+
try { p = p() } catch { p = ''; break }
|
|
35
|
+
guard += 1
|
|
36
|
+
} else if (typeof p === 'object' && typeof p.get === 'function') {
|
|
37
|
+
try { p = p.get() } catch { p = ''; break }
|
|
38
|
+
guard += 1
|
|
39
|
+
} else {
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (typeof p !== 'string') return ''
|
|
44
|
+
if (p.startsWith('~/') || p === '~') {
|
|
45
|
+
const home = process.env.HOME || os.homedir()
|
|
46
|
+
return path.join(home, p.slice(1))
|
|
47
|
+
}
|
|
48
|
+
return p
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function checkRegisteredInPiAi(ctx) {
|
|
52
|
+
const settings = ctx?.get?.('settings')
|
|
53
|
+
try {
|
|
54
|
+
if (typeof settings?.describe === 'function') {
|
|
55
|
+
const descriptors = settings.describe()
|
|
56
|
+
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
57
|
+
if (piAiDesc?.value?.providers?.[PROVIDER_ID]) return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const llm = ctx?.get?.('llm')
|
|
61
|
+
if (typeof llm?.listProviders === 'function') {
|
|
62
|
+
const list = await llm.listProviders()
|
|
63
|
+
if (Array.isArray(list) && list.some((p) => (p?.id || p) === PROVIDER_ID)) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function checkRegisteredInLlm(ctx) {
|
|
74
|
+
try {
|
|
75
|
+
const llm = ctx?.get?.('llm')
|
|
76
|
+
if (llm && typeof llm.listProviders === 'function') {
|
|
77
|
+
const providers = await llm.listProviders()
|
|
78
|
+
if (Array.isArray(providers) && providers.some((entry) => (entry?.id || entry) === PROVIDER_ID)) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
/* llm service may not be mounted */
|
|
84
|
+
}
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
|
|
89
|
+
const accountPool = pool || await resolveAccountPool(ctx, cfg)
|
|
90
|
+
const configured = accountPool.filter((acc) => acc.present && acc.value)
|
|
91
|
+
if (!configured.length) {
|
|
92
|
+
const fallbackEnv = publicConfig(cfg).apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
93
|
+
return { envName: fallbackEnv, apiKeyEnv: fallbackEnv, value: '', source: 'none', id: 'default' }
|
|
94
|
+
}
|
|
95
|
+
const pub = publicConfig(cfg)
|
|
96
|
+
let chosen = configured[0]
|
|
97
|
+
if (pub.activeAccount) {
|
|
98
|
+
const pinned = configured.find((acc) => acc.apiKeyEnv === pub.activeAccount || acc.id === pub.activeAccount)
|
|
99
|
+
if (pinned) chosen = pinned
|
|
100
|
+
}
|
|
101
|
+
const keyEnv = chosen.apiKeyEnv || chosen.envName || DEFAULT_API_KEY_ENV
|
|
102
|
+
return {
|
|
103
|
+
...chosen,
|
|
104
|
+
envName: keyEnv,
|
|
105
|
+
apiKeyEnv: keyEnv,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function buildStatus(ctx, cfg) {
|
|
110
|
+
const pub = publicConfig(cfg)
|
|
111
|
+
const probeTimeout = Math.min(2500, pub.timeoutMs || 2500)
|
|
112
|
+
|
|
113
|
+
const [key, pool, isRegisteredInPiAi, isRegisteredInLlm, health] = await Promise.all([
|
|
114
|
+
resolveKeyValue(ctx, pub.apiKeyEnv),
|
|
115
|
+
resolveAccountPool(ctx, cfg),
|
|
116
|
+
checkRegisteredInPiAi(ctx),
|
|
117
|
+
checkRegisteredInLlm(ctx),
|
|
118
|
+
probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
|
|
119
|
+
])
|
|
120
|
+
const isRegistered = isRegisteredInLlm || isRegisteredInPiAi
|
|
121
|
+
|
|
122
|
+
const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
|
|
123
|
+
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
124
|
+
|
|
125
|
+
let usage = null
|
|
126
|
+
const keyToUse = activeAcc.value || key.value
|
|
127
|
+
if (keyToUse) {
|
|
128
|
+
usage = await fetchUsageLimits(pub.baseUrl, keyToUse, { timeoutMs: probeTimeout }).catch(() => null)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let quotaWarning = null
|
|
132
|
+
if (usage?.windows) {
|
|
133
|
+
const fiveHour = usage.windows.fiveHour
|
|
134
|
+
const weekly = usage.windows.weekly
|
|
135
|
+
const monthly = usage.windows.monthly
|
|
136
|
+
|
|
137
|
+
if (fiveHour && typeof fiveHour.percentUsed === 'number') {
|
|
138
|
+
if (fiveHour.percentUsed >= 95) {
|
|
139
|
+
quotaWarning = {
|
|
140
|
+
level: 'exhausted',
|
|
141
|
+
code: 'quota.exhausted_5h',
|
|
142
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
143
|
+
message: `5-hour rolling limit is almost exhausted (${fiveHour.percentUsed}%). New requests may be rejected until quota reset.`,
|
|
144
|
+
resetsAt: fiveHour.resetsAt,
|
|
145
|
+
}
|
|
146
|
+
} else if (fiveHour.percentUsed >= 90) {
|
|
147
|
+
quotaWarning = {
|
|
148
|
+
level: 'warning',
|
|
149
|
+
code: 'quota.warning_5h',
|
|
150
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
151
|
+
message: `Low quota warning: ${fiveHour.percentUsed}% of 5-hour limit consumed (<10% remaining).`,
|
|
152
|
+
resetsAt: fiveHour.resetsAt,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!quotaWarning && weekly && typeof weekly.percentUsed === 'number' && weekly.percentUsed >= 90) {
|
|
158
|
+
const remainingPercent = weekly.remainingPercent ?? (100 - weekly.percentUsed)
|
|
159
|
+
quotaWarning = {
|
|
160
|
+
level: weekly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
161
|
+
code: 'quota.warning_weekly',
|
|
162
|
+
params: { percentUsed: weekly.percentUsed, remainingPercent },
|
|
163
|
+
message: `Low weekly quota warning: ${weekly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
164
|
+
resetsAt: weekly.resetsAt,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!quotaWarning && monthly && typeof monthly.percentUsed === 'number' && monthly.percentUsed >= 90) {
|
|
169
|
+
const remainingPercent = monthly.remainingPercent ?? (100 - monthly.percentUsed)
|
|
170
|
+
quotaWarning = {
|
|
171
|
+
level: monthly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
172
|
+
code: 'quota.warning_monthly',
|
|
173
|
+
params: { percentUsed: monthly.percentUsed, remainingPercent },
|
|
174
|
+
message: `Low monthly quota warning: ${monthly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
175
|
+
resetsAt: monthly.resetsAt,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
providerId: PROVIDER_ID,
|
|
183
|
+
displayName: PROVIDER_DISPLAY_NAME,
|
|
184
|
+
config: pub,
|
|
185
|
+
key: {
|
|
186
|
+
envName: activeAcc.apiKeyEnv || key.envName,
|
|
187
|
+
present: !!(activeAcc.value || key.value),
|
|
188
|
+
source: activeAcc.source || key.source,
|
|
189
|
+
},
|
|
190
|
+
accounts: pool.map((acc) => {
|
|
191
|
+
return {
|
|
192
|
+
id: acc.id,
|
|
193
|
+
label: acc.label,
|
|
194
|
+
apiKeyEnv: acc.apiKeyEnv,
|
|
195
|
+
present: acc.present,
|
|
196
|
+
source: acc.source,
|
|
197
|
+
isPinned: acc.isPinned,
|
|
198
|
+
percentUsed: typeof acc.percentUsed === 'number' ? acc.percentUsed : null,
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
activeAccount: activeAcc.apiKeyEnv,
|
|
202
|
+
health,
|
|
203
|
+
usage: publicUsage(usage),
|
|
204
|
+
quotaWarning,
|
|
205
|
+
sessionStats: { ...sessionStats },
|
|
206
|
+
statsSummary: getStatsSummary(),
|
|
207
|
+
lastRotation: getLastRotation(),
|
|
208
|
+
isRegistered,
|
|
209
|
+
availableModels: allModels,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
214
|
+
const settings = ctx?.get?.('settings')
|
|
215
|
+
if (!settings?.mutate) {
|
|
216
|
+
throw new Error('DSH settings service unavailable')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const pub = publicConfig(cfg)
|
|
220
|
+
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
221
|
+
const allowedSet = new Set(activeModelIds || pub.enabledModels)
|
|
222
|
+
const modelsToRegister = allModels.filter((m) => allowedSet.has(m.id))
|
|
223
|
+
|
|
224
|
+
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
225
|
+
|
|
226
|
+
let isProxy = pub.proxyMode !== false
|
|
227
|
+
let effectiveBaseUrl = pub.baseUrl
|
|
228
|
+
let proxyPort = getProxyPort()
|
|
229
|
+
|
|
230
|
+
if (isProxy) {
|
|
231
|
+
if (!proxyPort) {
|
|
232
|
+
proxyPort = ctx?.webServer?.port || ctx?.get?.('webServer')?.port || process.env.PORT || 3080
|
|
233
|
+
const isFenced = await detectHostWebServerFence(proxyPort)
|
|
234
|
+
if (isFenced) {
|
|
235
|
+
ctx?.logger?.warn?.(
|
|
236
|
+
`[dsh-clinebot] DesktopWebServer security fence detected on port ${proxyPort} and dedicated loopback proxy is not active. Falling back to direct upstream mode.`
|
|
237
|
+
)
|
|
238
|
+
isProxy = false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isProxy && proxyPort) {
|
|
244
|
+
effectiveBaseUrl = `http://127.0.0.1:${proxyPort}/dsh-clinebot/v1`
|
|
245
|
+
} else {
|
|
246
|
+
isProxy = false
|
|
247
|
+
effectiveBaseUrl = pub.baseUrl
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const localToken = isProxy ? getLocalProxyToken() : undefined
|
|
251
|
+
if (isProxy && localToken) {
|
|
252
|
+
try {
|
|
253
|
+
await saveCredentialKey(ctx, LOCAL_PROXY_KEY_ENV, localToken)
|
|
254
|
+
} catch {
|
|
255
|
+
/* credentials service might be absent/read-only in tests; process.env is set */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let existingModels = []
|
|
260
|
+
try {
|
|
261
|
+
if (typeof settings.describe === 'function') {
|
|
262
|
+
const descriptors = settings.describe()
|
|
263
|
+
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
264
|
+
const existingProv = piAiDesc?.value?.providers?.[PROVIDER_ID]
|
|
265
|
+
if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
|
|
266
|
+
} else if (typeof settings.get === 'function') {
|
|
267
|
+
const piAiVal = settings.get(LLM_PI_AI_NS)
|
|
268
|
+
const existingProv = piAiVal?.providers?.[PROVIDER_ID]
|
|
269
|
+
if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
/* ignore read error */
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const providerObj = buildPiAiProvider({
|
|
276
|
+
baseUrl: effectiveBaseUrl,
|
|
277
|
+
apiKeyEnv: isProxy ? LOCAL_PROXY_KEY_ENV : (activeAcc?.apiKeyEnv || pub.apiKeyEnv),
|
|
278
|
+
models: modelsToRegister.length ? modelsToRegister : allModels,
|
|
279
|
+
dynamicModels: pub.dynamicModels,
|
|
280
|
+
modelContextOverrides: pub.modelContextOverrides,
|
|
281
|
+
customModels: pub.customModels,
|
|
282
|
+
existingModels,
|
|
283
|
+
displayName: PROVIDER_DISPLAY_NAME,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await settings.mutate(LLM_PI_AI_NS, [
|
|
287
|
+
{
|
|
288
|
+
op: 'set',
|
|
289
|
+
path: ['providers', PROVIDER_ID],
|
|
290
|
+
value: providerObj,
|
|
291
|
+
},
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
ctx?.logger?.info?.(
|
|
295
|
+
`[dsh-clinebot] Synchronized llm-pi-ai provider "${PROVIDER_ID}" with ${providerObj.models.length} models.`
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return providerObj
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function removePiAiProvider(ctx) {
|
|
302
|
+
const settings = ctx?.get?.('settings')
|
|
303
|
+
if (!settings?.mutate) {
|
|
304
|
+
throw new Error('DSH settings service unavailable')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await settings.mutate(LLM_PI_AI_NS, [
|
|
309
|
+
{
|
|
310
|
+
op: 'remove',
|
|
311
|
+
path: ['providers', PROVIDER_ID],
|
|
312
|
+
},
|
|
313
|
+
])
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const msg = String(err?.message || err).toLowerCase()
|
|
316
|
+
if (!msg.includes('not found') && !msg.includes('absent') && !msg.includes('enoent') && !msg.includes('does not exist')) {
|
|
317
|
+
throw err
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { ok: true }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function formatProgressBar(pct, totalWidth = 10) {
|
|
324
|
+
const clamped = Math.max(0, Math.min(100, pct || 0))
|
|
325
|
+
const filled = Math.round((clamped / 100) * totalWidth)
|
|
326
|
+
const empty = Math.max(0, totalWidth - filled)
|
|
327
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${clamped}%`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
331
|
+
try {
|
|
332
|
+
const cfg = live()
|
|
333
|
+
const pub = publicConfig(cfg)
|
|
334
|
+
const cacheFile = resolvePathWithHome(pub.modelsCachePath)
|
|
335
|
+
const settingsApi = getSettingsApi()
|
|
336
|
+
|
|
337
|
+
// Auto-migrate obsolete IDs (e.g. deepseek-v41-flash -> deepseek-v4.1-flash)
|
|
338
|
+
const existingDynamic = pub.dynamicModels || []
|
|
339
|
+
const hasObsolete = existingDynamic.some((m) => Boolean(OBSOLETE_MODEL_ID_MAP[m?.id]))
|
|
340
|
+
if (hasObsolete && settingsApi?.replace) {
|
|
341
|
+
const migrated = existingDynamic.map(migrateModelEntry)
|
|
342
|
+
const next = plainConfig(Config({
|
|
343
|
+
...plainConfig(live()),
|
|
344
|
+
dynamicModels: migrated,
|
|
345
|
+
}))
|
|
346
|
+
await settingsApi.replace(next)
|
|
347
|
+
await syncProviderState(next)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
351
|
+
if (!activeAcc.value) return
|
|
352
|
+
|
|
353
|
+
const cacheIdentity = {
|
|
354
|
+
endpoint: pub.baseUrl,
|
|
355
|
+
accountRef: activeAcc.envName || activeAcc.id || pub.apiKeyEnv,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let usageData = null
|
|
359
|
+
try {
|
|
360
|
+
usageData = await fetchUsageLimits(pub.baseUrl, activeAcc.value, {
|
|
361
|
+
timeoutMs: Math.min(pub.timeoutMs, 5000),
|
|
362
|
+
bypassCache: true,
|
|
363
|
+
})
|
|
364
|
+
} catch {
|
|
365
|
+
usageData = null
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (usageData?.ok && Array.isArray(usageData.dynamicModels)) {
|
|
369
|
+
const existingDynamic = pub.dynamicModels || []
|
|
370
|
+
const isDifferent = isCatalogDifferent(existingDynamic, usageData.dynamicModels)
|
|
371
|
+
const now = Date.now()
|
|
372
|
+
|
|
373
|
+
if (isDifferent && settingsApi?.replace) {
|
|
374
|
+
const next = plainConfig(Config({
|
|
375
|
+
...plainConfig(live()),
|
|
376
|
+
dynamicModels: usageData.dynamicModels,
|
|
377
|
+
planSyncedAt: now,
|
|
378
|
+
}))
|
|
379
|
+
await settingsApi.replace(next)
|
|
380
|
+
await syncProviderState(next)
|
|
381
|
+
}
|
|
382
|
+
if (cacheFile) {
|
|
383
|
+
await saveModelsDiskCache(cacheFile, usageData.dynamicModels, now, cacheIdentity)
|
|
384
|
+
}
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
|
|
389
|
+
const fromDisk = await loadModelsDiskCache(cacheFile, cacheIdentity)
|
|
390
|
+
if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
|
|
391
|
+
const migratedDisk = fromDisk.map(migrateModelEntry)
|
|
392
|
+
const next = plainConfig(Config({
|
|
393
|
+
...plainConfig(live()),
|
|
394
|
+
dynamicModels: migratedDisk,
|
|
395
|
+
planSyncedAt: 0,
|
|
396
|
+
}))
|
|
397
|
+
await settingsApi.replace(next)
|
|
398
|
+
await syncProviderState(next)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
/* best-effort discovery */
|
|
403
|
+
}
|
|
404
|
+
}
|