@goodandready/dsh-clinebot 0.5.7 → 0.5.9
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 +23 -0
- package/README.md +29 -16
- package/README.ru.md +29 -16
- package/README.zh.md +29 -16
- package/lib/account-pool.js +7 -5
- package/lib/client.js +189 -78
- package/lib/cline-client.js +46 -6
- package/lib/index.js +43 -32
- package/lib/models.js +35 -9
- package/lib/provider-sync.js +21 -8
- package/lib/routes/models.js +4 -4
- package/lib/routes/proxy.js +51 -12
- package/lib/slash-command.js +4 -3
- package/lib/updater.js +83 -30
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -230,6 +230,7 @@ export function apply(ctx, config) {
|
|
|
230
230
|
const is429 = failure.status === 429 || failure.code === 429 || failure.code === 'rate_limit_exceeded'
|
|
231
231
|
const isExceeded = isQuotaExceededError(`${failure.message || ''} ${failure.code || ''}`)
|
|
232
232
|
|
|
233
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
233
234
|
if (isError) {
|
|
234
235
|
recordSessionRequest({
|
|
235
236
|
latencyMs,
|
|
@@ -239,15 +240,17 @@ export function apply(ctx, config) {
|
|
|
239
240
|
completionTokens,
|
|
240
241
|
model: options?.model,
|
|
241
242
|
})
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
if (!isProxy) {
|
|
244
|
+
recordUsage({
|
|
245
|
+
model: options?.model,
|
|
246
|
+
promptTokens,
|
|
247
|
+
completionTokens,
|
|
248
|
+
totalTokens: promptTokens + completionTokens,
|
|
249
|
+
isError: true,
|
|
250
|
+
is429,
|
|
251
|
+
statsPath: publicConfig(live()).statsPath,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
251
254
|
if (is429 || isExceeded) {
|
|
252
255
|
handleStream429()
|
|
253
256
|
}
|
|
@@ -268,13 +271,15 @@ export function apply(ctx, config) {
|
|
|
268
271
|
completionTokens,
|
|
269
272
|
model: options?.model,
|
|
270
273
|
})
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
if (!isProxy) {
|
|
275
|
+
recordUsage({
|
|
276
|
+
model: options?.model,
|
|
277
|
+
promptTokens,
|
|
278
|
+
completionTokens,
|
|
279
|
+
totalTokens: promptTokens + completionTokens,
|
|
280
|
+
statsPath: publicConfig(live()).statsPath,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
yield chunk
|
|
@@ -287,13 +292,16 @@ export function apply(ctx, config) {
|
|
|
287
292
|
completionTokens,
|
|
288
293
|
model: options?.model,
|
|
289
294
|
})
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
296
|
+
if (!isProxy) {
|
|
297
|
+
recordUsage({
|
|
298
|
+
model: options?.model,
|
|
299
|
+
promptTokens,
|
|
300
|
+
completionTokens,
|
|
301
|
+
totalTokens: promptTokens + completionTokens,
|
|
302
|
+
statsPath: publicConfig(live()).statsPath,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
297
305
|
}
|
|
298
306
|
} catch (err) {
|
|
299
307
|
if (isCline) {
|
|
@@ -308,15 +316,18 @@ export function apply(ctx, config) {
|
|
|
308
316
|
completionTokens,
|
|
309
317
|
model: options?.model,
|
|
310
318
|
})
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
319
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
320
|
+
if (!isProxy) {
|
|
321
|
+
recordUsage({
|
|
322
|
+
model: options?.model,
|
|
323
|
+
promptTokens,
|
|
324
|
+
completionTokens,
|
|
325
|
+
totalTokens: promptTokens + completionTokens,
|
|
326
|
+
isError: true,
|
|
327
|
+
is429: status === 429,
|
|
328
|
+
statsPath: publicConfig(live()).statsPath,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
320
331
|
if (status === 429 || isQuotaExceededError(msg)) {
|
|
321
332
|
handleStream429()
|
|
322
333
|
}
|
package/lib/models.js
CHANGED
|
@@ -422,10 +422,10 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
|
|
|
422
422
|
description: cm.description || `Custom model ${cm.name || cm.id}`,
|
|
423
423
|
contextLength: cm.contextLength || 128000,
|
|
424
424
|
maxTokens: cm.maxTokens || 8192,
|
|
425
|
-
input: ['text'],
|
|
425
|
+
input: Array.isArray(cm.input) && cm.input.length ? cm.input : ['text'],
|
|
426
426
|
category: cm.category || 'general',
|
|
427
427
|
isCustom: true,
|
|
428
|
-
reasoningEfforts: cm.isReasoning ? ['low', 'medium', 'high', 'max'] : undefined,
|
|
428
|
+
reasoningEfforts: cm.reasoningEfforts !== undefined ? cm.reasoningEfforts : (cm.isReasoning ? ['low', 'medium', 'high', 'max'] : undefined),
|
|
429
429
|
unverified: false,
|
|
430
430
|
}
|
|
431
431
|
baseModels.push(applyOverride(fullCustom))
|
|
@@ -437,19 +437,19 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
|
|
|
437
437
|
return baseModels
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
function findModel(id, dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
441
|
-
const all = getAllModels(dynamicModels, modelContextOverrides)
|
|
440
|
+
export function findModel(id, dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
441
|
+
const all = getAllModels(dynamicModels, modelContextOverrides, customModels)
|
|
442
442
|
const found = all.find((m) => m.id === id)
|
|
443
443
|
if (found) return found
|
|
444
444
|
return CLINE_MODELS.find((m) => m.id === id) || null
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
export function isSupportedModel(id, dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
448
|
-
return findModel(id, dynamicModels, modelContextOverrides) !== null
|
|
448
|
+
return findModel(id, dynamicModels, modelContextOverrides, customModels) !== null
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
function getDefaultModelIds(dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
452
|
-
return getAllModels(dynamicModels, modelContextOverrides).map((m) => m.id)
|
|
451
|
+
export function getDefaultModelIds(dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
452
|
+
return getAllModels(dynamicModels, modelContextOverrides, customModels).map((m) => m.id)
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
/**
|
|
@@ -501,8 +501,8 @@ export function formatModelDescription(model) {
|
|
|
501
501
|
/**
|
|
502
502
|
* Check if a model supports vision input.
|
|
503
503
|
*/
|
|
504
|
-
export function isVisionModel(id, dynamicModels = [], modelContextOverrides = []) {
|
|
505
|
-
const m = findModel(id, dynamicModels, modelContextOverrides)
|
|
504
|
+
export function isVisionModel(id, dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
505
|
+
const m = findModel(id, dynamicModels, modelContextOverrides, customModels)
|
|
506
506
|
if (!m) return false
|
|
507
507
|
return Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
508
508
|
}
|
|
@@ -548,3 +548,29 @@ export async function loadModelsDiskCache(cachePath) {
|
|
|
548
548
|
return null
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Detects whether the authoritative dynamic models catalog differs from the current catalog
|
|
554
|
+
* in model count, IDs, or metadata (e.g. contextLength, maxTokens, category, etc.).
|
|
555
|
+
*/
|
|
556
|
+
export function isCatalogDifferent(currentList = [], authoritativeList = []) {
|
|
557
|
+
if (!Array.isArray(currentList) || !Array.isArray(authoritativeList)) return true
|
|
558
|
+
if (currentList.length !== authoritativeList.length) return true
|
|
559
|
+
const currentMap = new Map(currentList.map((m) => [m.id, m]))
|
|
560
|
+
for (const auth of authoritativeList) {
|
|
561
|
+
const cur = currentMap.get(auth.id)
|
|
562
|
+
if (!cur) return true
|
|
563
|
+
if (
|
|
564
|
+
cur.name !== auth.name ||
|
|
565
|
+
cur.contextLength !== auth.contextLength ||
|
|
566
|
+
cur.maxTokens !== auth.maxTokens ||
|
|
567
|
+
cur.category !== auth.category ||
|
|
568
|
+
cur.description !== auth.description ||
|
|
569
|
+
JSON.stringify(cur.input || []) !== JSON.stringify(auth.input || []) ||
|
|
570
|
+
JSON.stringify(cur.reasoningEfforts || []) !== JSON.stringify(auth.reasoningEfforts || [])
|
|
571
|
+
) {
|
|
572
|
+
return true
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return false
|
|
576
|
+
}
|
package/lib/provider-sync.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getStatsSummary } from './stats-storage.js'
|
|
1
2
|
import os from 'node:os'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
loadModelsDiskCache,
|
|
9
10
|
OBSOLETE_MODEL_ID_MAP,
|
|
10
11
|
migrateModelEntry,
|
|
12
|
+
isCatalogDifferent,
|
|
11
13
|
} from './models.js'
|
|
12
14
|
import {
|
|
13
15
|
probeHealth,
|
|
@@ -121,12 +123,16 @@ export async function buildStatus(ctx, cfg) {
|
|
|
121
123
|
if (fiveHour.percentUsed >= 95) {
|
|
122
124
|
quotaWarning = {
|
|
123
125
|
level: 'exhausted',
|
|
126
|
+
code: 'quota.exhausted_5h',
|
|
127
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
124
128
|
message: `5-hour rolling limit is almost exhausted (${fiveHour.percentUsed}%). New requests may be rejected until quota reset.`,
|
|
125
129
|
resetsAt: fiveHour.resetsAt,
|
|
126
130
|
}
|
|
127
131
|
} else if (fiveHour.percentUsed >= 90) {
|
|
128
132
|
quotaWarning = {
|
|
129
133
|
level: 'warning',
|
|
134
|
+
code: 'quota.warning_5h',
|
|
135
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
130
136
|
message: `Low quota warning: ${fiveHour.percentUsed}% of 5-hour limit consumed (<10% remaining).`,
|
|
131
137
|
resetsAt: fiveHour.resetsAt,
|
|
132
138
|
}
|
|
@@ -134,17 +140,23 @@ export async function buildStatus(ctx, cfg) {
|
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
if (!quotaWarning && weekly && typeof weekly.percentUsed === 'number' && weekly.percentUsed >= 90) {
|
|
143
|
+
const remainingPercent = weekly.remainingPercent ?? (100 - weekly.percentUsed)
|
|
137
144
|
quotaWarning = {
|
|
138
145
|
level: weekly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
139
|
-
|
|
146
|
+
code: 'quota.warning_weekly',
|
|
147
|
+
params: { percentUsed: weekly.percentUsed, remainingPercent },
|
|
148
|
+
message: `Low weekly quota warning: ${weekly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
140
149
|
resetsAt: weekly.resetsAt,
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
if (!quotaWarning && monthly && typeof monthly.percentUsed === 'number' && monthly.percentUsed >= 90) {
|
|
154
|
+
const remainingPercent = monthly.remainingPercent ?? (100 - monthly.percentUsed)
|
|
145
155
|
quotaWarning = {
|
|
146
156
|
level: monthly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
147
|
-
|
|
157
|
+
code: 'quota.warning_monthly',
|
|
158
|
+
params: { percentUsed: monthly.percentUsed, remainingPercent },
|
|
159
|
+
message: `Low monthly quota warning: ${monthly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
148
160
|
resetsAt: monthly.resetsAt,
|
|
149
161
|
}
|
|
150
162
|
}
|
|
@@ -176,6 +188,7 @@ export async function buildStatus(ctx, cfg) {
|
|
|
176
188
|
usage: publicUsage(usage),
|
|
177
189
|
quotaWarning,
|
|
178
190
|
sessionStats: { ...sessionStats },
|
|
191
|
+
statsSummary: getStatsSummary(),
|
|
179
192
|
lastRotation: getLastRotation(),
|
|
180
193
|
isRegistered,
|
|
181
194
|
availableModels: allModels,
|
|
@@ -213,9 +226,10 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
|
213
226
|
const providerObj = buildPiAiProvider({
|
|
214
227
|
baseUrl: effectiveBaseUrl,
|
|
215
228
|
apiKeyEnv: isProxy ? LOCAL_PROXY_KEY_ENV : (activeAcc?.apiKeyEnv || pub.apiKeyEnv),
|
|
216
|
-
apiKey: localToken,
|
|
217
229
|
models: modelsToRegister.length ? modelsToRegister : allModels,
|
|
218
|
-
|
|
230
|
+
dynamicModels: pub.dynamicModels,
|
|
231
|
+
modelContextOverrides: pub.modelContextOverrides,
|
|
232
|
+
customModels: pub.customModels,
|
|
219
233
|
displayName: PROVIDER_DISPLAY_NAME,
|
|
220
234
|
})
|
|
221
235
|
|
|
@@ -301,12 +315,11 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
|
|
|
301
315
|
bypassCache: true,
|
|
302
316
|
})
|
|
303
317
|
|
|
304
|
-
if (usageData?.ok && Array.isArray(usageData.dynamicModels)
|
|
318
|
+
if (usageData?.ok && Array.isArray(usageData.dynamicModels)) {
|
|
305
319
|
const existingDynamic = pub.dynamicModels || []
|
|
306
|
-
const
|
|
307
|
-
const hasNew = usageData.dynamicModels.some((m) => !existingIds.has(m.id))
|
|
320
|
+
const isDifferent = isCatalogDifferent(existingDynamic, usageData.dynamicModels)
|
|
308
321
|
|
|
309
|
-
if (
|
|
322
|
+
if (isDifferent && settingsApi?.replace) {
|
|
310
323
|
const now = Date.now()
|
|
311
324
|
const next = plainConfig(Config({
|
|
312
325
|
...plainConfig(live()),
|
package/lib/routes/models.js
CHANGED
|
@@ -33,7 +33,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const dynamicModels = Array.isArray(usageData.dynamicModels) ? usageData.dynamicModels : []
|
|
36
|
-
const allModels = getAllModels(dynamicModels, pub.modelContextOverrides)
|
|
36
|
+
const allModels = getAllModels(dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
37
37
|
const settingsApi = getSettingsApi()
|
|
38
38
|
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
39
39
|
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
@@ -87,7 +87,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
87
87
|
if (Array.isArray(body.disabledModels)) {
|
|
88
88
|
patch.disabledModels = body.disabledModels
|
|
89
89
|
} else if (Array.isArray(body.enabledModels)) {
|
|
90
|
-
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
|
|
90
|
+
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
91
91
|
const enabledSet = new Set(body.enabledModels)
|
|
92
92
|
patch.disabledModels = allModels.map((m) => m.id).filter((id) => !enabledSet.has(id))
|
|
93
93
|
}
|
|
@@ -120,7 +120,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const pub = publicConfig(live())
|
|
123
|
-
const allModels = getAllModels(pub.dynamicModels, [])
|
|
123
|
+
const allModels = getAllModels(pub.dynamicModels, [], pub.customModels)
|
|
124
124
|
let currentOverrides = Array.isArray(pub.modelContextOverrides) ? [...pub.modelContextOverrides] : []
|
|
125
125
|
|
|
126
126
|
if (body.mode === 'all-original') {
|
|
@@ -171,7 +171,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
171
171
|
await settingsApi.replace(next)
|
|
172
172
|
await syncProviderState(next)
|
|
173
173
|
|
|
174
|
-
const updatedModels = getAllModels(pub.dynamicModels, currentOverrides)
|
|
174
|
+
const updatedModels = getAllModels(pub.dynamicModels, currentOverrides, pub.customModels)
|
|
175
175
|
return writeJson(res, 200, {
|
|
176
176
|
ok: true,
|
|
177
177
|
modelContextOverrides: next.modelContextOverrides,
|
package/lib/routes/proxy.js
CHANGED
|
@@ -154,6 +154,8 @@ export function registerProxyRoutes(ctx, { live, getSettingsApi, syncProviderSta
|
|
|
154
154
|
let attempt = 0
|
|
155
155
|
let lastError = null
|
|
156
156
|
let hadTimeout = false
|
|
157
|
+
let lastTargetAccount = null
|
|
158
|
+
let lastRetryAfter = null
|
|
157
159
|
|
|
158
160
|
while (attempt < maxRetries) {
|
|
159
161
|
attempt += 1
|
|
@@ -165,6 +167,7 @@ export function registerProxyRoutes(ctx, { live, getSettingsApi, syncProviderSta
|
|
|
165
167
|
|
|
166
168
|
const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount)
|
|
167
169
|
const targetAccount = routeResult.account
|
|
170
|
+
lastTargetAccount = targetAccount
|
|
168
171
|
|
|
169
172
|
if (!targetAccount || !targetAccount.value) {
|
|
170
173
|
cleanupClientListeners()
|
|
@@ -230,13 +233,23 @@ export function registerProxyRoutes(ctx, { live, getSettingsApi, syncProviderSta
|
|
|
230
233
|
const accountId = targetAccount.apiKeyEnv || targetAccount.id || targetAccount.label
|
|
231
234
|
markAccountCooldown(accountId, 60000, null, 'upstream_429')
|
|
232
235
|
if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, 'upstream_429')
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
lastError = new Error(`Upstream rejected with HTTP ${upstreamRes.status}`)
|
|
237
|
+
lastError.status = upstreamRes.status
|
|
238
|
+
const retryAfter = upstreamRes.headers?.get?.('retry-after')
|
|
239
|
+
if (retryAfter) {
|
|
240
|
+
lastRetryAfter = retryAfter
|
|
241
|
+
}
|
|
242
|
+
if (upstreamRes.body) {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof upstreamRes.body.cancel === 'function') {
|
|
245
|
+
upstreamRes.body.cancel().catch(() => {})
|
|
246
|
+
} else if (typeof upstreamRes.body.destroy === 'function') {
|
|
247
|
+
upstreamRes.body.destroy()
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
/* ignore cancel error */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
240
253
|
ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" hit 429, attempting failover (attempt ${attempt}/${maxRetries})`)
|
|
241
254
|
continue
|
|
242
255
|
}
|
|
@@ -419,21 +432,47 @@ export function registerProxyRoutes(ctx, { live, getSettingsApi, syncProviderSta
|
|
|
419
432
|
|
|
420
433
|
cleanupClientListeners()
|
|
421
434
|
// Exhausted all retries
|
|
435
|
+
const is429Exhausted = !hadTimeout && (lastError?.status === 429 || lastError?.status === 402)
|
|
436
|
+
recordUsage({
|
|
437
|
+
model: body.model,
|
|
438
|
+
accountId: lastTargetAccount?.id || lastTargetAccount?.label || 'default',
|
|
439
|
+
isError: true,
|
|
440
|
+
is429: is429Exhausted,
|
|
441
|
+
statsPath: pub.statsPath
|
|
442
|
+
})
|
|
422
443
|
if (hadTimeout) {
|
|
423
444
|
return writeJson(res, 504, {
|
|
424
445
|
error: {
|
|
425
446
|
message: 'Gateway Timeout: upstream provider timed out across all available accounts',
|
|
426
447
|
type: 'gateway_timeout',
|
|
427
|
-
code: 'upstream_timeout'
|
|
448
|
+
code: 'upstream_timeout',
|
|
449
|
+
details: lastError?.message || null
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (is429Exhausted) {
|
|
455
|
+
if (lastRetryAfter) {
|
|
456
|
+
res.setHeader('Retry-After', lastRetryAfter)
|
|
457
|
+
}
|
|
458
|
+
return writeJson(res, 429, {
|
|
459
|
+
error: {
|
|
460
|
+
message: 'All configured accounts in pool exceeded rate limits or quota',
|
|
461
|
+
type: 'rate_limit_exceeded',
|
|
462
|
+
code: 'rate_limit_exceeded',
|
|
463
|
+
details: lastError?.message || null
|
|
428
464
|
}
|
|
429
465
|
})
|
|
430
466
|
}
|
|
431
467
|
|
|
432
|
-
|
|
468
|
+
const status = typeof lastError?.status === 'number' && lastError.status >= 500 && lastError.status <= 599
|
|
469
|
+
? lastError.status
|
|
470
|
+
: 502
|
|
471
|
+
return writeJson(res, status, {
|
|
433
472
|
error: {
|
|
434
|
-
message:
|
|
435
|
-
type: '
|
|
436
|
-
code: '
|
|
473
|
+
message: `Bad Gateway: upstream network error or unavailable: ${lastError?.message || 'unknown error'}`,
|
|
474
|
+
type: 'bad_gateway',
|
|
475
|
+
code: 'upstream_unavailable',
|
|
437
476
|
details: lastError?.message || null
|
|
438
477
|
}
|
|
439
478
|
})
|
package/lib/slash-command.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
clearUsageCache,
|
|
17
17
|
clearProbeCache,
|
|
18
18
|
usageCache,
|
|
19
|
+
buildUsageCacheKey,
|
|
19
20
|
sessionStats,
|
|
20
21
|
} from './cline-client.js'
|
|
21
22
|
|
|
@@ -33,7 +34,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
33
34
|
|
|
34
35
|
// 1. Subcommand /cline models
|
|
35
36
|
if (subcmd === 'models') {
|
|
36
|
-
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
|
|
37
|
+
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
37
38
|
const disabledSet = new Set(pub.disabledModels || [])
|
|
38
39
|
const activeCount = allModels.filter((m) => !disabledSet.has(m.id)).length
|
|
39
40
|
const syncStatus = pub.planSynced
|
|
@@ -76,7 +77,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
76
77
|
for (const acc of pool) {
|
|
77
78
|
const pinStr = acc.isPinned ? ' [Pinned]' : ''
|
|
78
79
|
const statusStr = acc.present ? 'Configured' : 'Missing Key'
|
|
79
|
-
const cacheKey =
|
|
80
|
+
const cacheKey = buildUsageCacheKey(pub.baseUrl, acc.value)
|
|
80
81
|
const cached = usageCache.get(cacheKey)?.data
|
|
81
82
|
const usageInfo = cached?.windows?.fiveHour ? ` · 5h ${cached.windows.fiveHour.percentUsed}% used` : ''
|
|
82
83
|
lines.push(` ${acc.label} (${acc.apiKeyEnv})`)
|
|
@@ -156,7 +157,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
156
157
|
|
|
157
158
|
// 7. Subcommand /cline test [model] / smoke
|
|
158
159
|
if (subcmd === 'test' || subcmd === 'smoke') {
|
|
159
|
-
if (param && !isSupportedModel(param, pub.dynamicModels)) {
|
|
160
|
+
if (param && !isSupportedModel(param, pub.dynamicModels, pub.modelContextOverrides, pub.customModels)) {
|
|
160
161
|
return `ClineBot: Model "${param}" is not recognized. Use /cline models to see available models.`
|
|
161
162
|
}
|
|
162
163
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
package/lib/updater.js
CHANGED
|
@@ -219,10 +219,32 @@ export async function checkUpdateStatus(options, target = detectRuntime()) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
export function cleanStaleLock(profileDir) {
|
|
223
|
+
if (!profileDir) return false
|
|
224
|
+
const lockFile = resolve(profileDir, 'package.json.lock')
|
|
225
|
+
if (existsSync(lockFile)) {
|
|
226
|
+
try {
|
|
227
|
+
const pid = parseInt(readFileSync(lockFile, 'utf8').trim(), 10)
|
|
228
|
+
if (!isPidAlive(pid)) {
|
|
229
|
+
unlinkSync(lockFile)
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
/* best-effort stale lock unlink */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function installExact(target, packageSpec, options = {}) {
|
|
223
240
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
241
|
+
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : UPDATE_TIMEOUT_MS
|
|
242
|
+
const sigtermGraceMs = typeof options.sigtermGraceMs === 'number' ? options.sigtermGraceMs : 500
|
|
243
|
+
const sigkillGraceMs = typeof options.sigkillGraceMs === 'number' ? options.sigkillGraceMs : 500
|
|
244
|
+
const spawnFn = typeof options.spawn === 'function' ? options.spawn : spawn
|
|
245
|
+
|
|
224
246
|
await new Promise((resolvePromise, reject) => {
|
|
225
|
-
const child =
|
|
247
|
+
const child = spawnFn(process.execPath, [
|
|
226
248
|
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
227
249
|
packageSpec,
|
|
228
250
|
`--registry=${options.registry || 'https://registry.npmjs.org/'}`,
|
|
@@ -235,40 +257,71 @@ async function installExact(target, packageSpec, options) {
|
|
|
235
257
|
let detail = ''
|
|
236
258
|
child.stdout?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
|
|
237
259
|
child.stderr?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
|
|
261
|
+
let exited = false
|
|
262
|
+
let isTimedOut = false
|
|
263
|
+
let sigtermTimer = null
|
|
264
|
+
let sigkillTimer = null
|
|
265
|
+
|
|
266
|
+
function cleanupTimers() {
|
|
267
|
+
clearTimeout(mainTimer)
|
|
268
|
+
if (sigtermTimer) clearTimeout(sigtermTimer)
|
|
269
|
+
if (sigkillTimer) clearTimeout(sigkillTimer)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function onProcessExit(code, signal) {
|
|
273
|
+
if (exited) return
|
|
274
|
+
exited = true
|
|
275
|
+
cleanupTimers()
|
|
276
|
+
|
|
277
|
+
if (isTimedOut) {
|
|
278
|
+
cleanStaleLock(target.profileDir)
|
|
279
|
+
reject(new Error('Update timed out.'))
|
|
280
|
+
} else if (code === 0) {
|
|
281
|
+
resolvePromise()
|
|
282
|
+
} else {
|
|
283
|
+
reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
child.once('error', (error) => {
|
|
288
|
+
cleanupTimers()
|
|
289
|
+
reject(error)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
child.once('exit', onProcessExit)
|
|
293
|
+
child.once('close', onProcessExit)
|
|
294
|
+
|
|
295
|
+
const mainTimer = setTimeout(() => {
|
|
296
|
+
isTimedOut = true
|
|
297
|
+
// Step 1: Send SIGTERM
|
|
298
|
+
try {
|
|
299
|
+
child.kill('SIGTERM')
|
|
300
|
+
} catch (err) {
|
|
301
|
+
/* ignore if process already terminated */
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Step 2: Wait for grace period, if process has not exited escalate to SIGKILL (#172)
|
|
305
|
+
sigtermTimer = setTimeout(() => {
|
|
306
|
+
const hasExited = exited || (child.exitCode !== null && child.exitCode !== undefined) || (child.signalCode !== null && child.signalCode !== undefined)
|
|
307
|
+
if (!hasExited) {
|
|
242
308
|
try {
|
|
243
309
|
child.kill('SIGKILL')
|
|
244
310
|
} catch (err) {
|
|
245
311
|
/* ignore if process already terminated */
|
|
246
312
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!isPidAlive(pid)) {
|
|
255
|
-
unlinkSync(lockFile)
|
|
256
|
-
}
|
|
257
|
-
} catch (err) {
|
|
258
|
-
/* best-effort stale lock unlink */
|
|
259
|
-
}
|
|
313
|
+
|
|
314
|
+
// Step 3: Hard deadline for exit notification
|
|
315
|
+
sigkillTimer = setTimeout(() => {
|
|
316
|
+
if (!exited) {
|
|
317
|
+
exited = true
|
|
318
|
+
cleanStaleLock(target.profileDir)
|
|
319
|
+
reject(new Error('Update timed out.'))
|
|
260
320
|
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
}, UPDATE_TIMEOUT_MS)
|
|
266
|
-
child.once('error', (error) => { clearTimeout(timer); reject(error) })
|
|
267
|
-
child.once('exit', (code) => {
|
|
268
|
-
clearTimeout(timer)
|
|
269
|
-
if (code === 0) resolvePromise()
|
|
270
|
-
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
271
|
-
})
|
|
321
|
+
}, sigkillGraceMs)
|
|
322
|
+
}
|
|
323
|
+
}, sigtermGraceMs)
|
|
324
|
+
}, timeoutMs)
|
|
272
325
|
})
|
|
273
326
|
}
|
|
274
327
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|