@goodandready/dsh-clinebot 0.5.20 → 0.5.22
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 +19 -0
- package/lib/client.js +269 -29
- package/lib/cline-client.js +21 -2
- package/lib/config.js +3 -0
- package/lib/models.js +83 -136
- package/lib/provider-sync.js +21 -0
- package/lib/proxy-token.js +22 -2
- package/lib/routes/models.js +70 -7
- package/lib/routes/proxy.js +15 -8
- package/lib/routes/settings.js +20 -1
- package/lib/session-router.js +49 -2
- package/lib/slash-command.js +19 -4
- package/lib/stats-storage.js +28 -0
- package/lib/telemetry.js +6 -1
- package/package.json +1 -1
package/lib/session-router.js
CHANGED
|
@@ -188,7 +188,31 @@ export function selectLeastUsedAccount(accounts = []) {
|
|
|
188
188
|
* @param {string} [defaultActiveId] Explicit active account ID, envName or 'auto'
|
|
189
189
|
* @returns {{ account: Object|null, isReassigned: boolean, reason: string }}
|
|
190
190
|
*/
|
|
191
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Match a model ID against modelRoutes map (supports exact match or prefix without cline-pass/).
|
|
193
|
+
*
|
|
194
|
+
* @param {string} [modelId] Target model ID
|
|
195
|
+
* @param {Object} [modelRoutes] Model routing table
|
|
196
|
+
* @param {Array<Object>} [safeAccounts] Available accounts
|
|
197
|
+
* @returns {Object|null}
|
|
198
|
+
*/
|
|
199
|
+
export function resolveModelRouteAccount(modelId, modelRoutes = {}, safeAccounts = []) {
|
|
200
|
+
if (!modelId || !modelRoutes || typeof modelRoutes !== 'object' || !Array.isArray(safeAccounts)) {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
const cleanId = String(modelId).trim()
|
|
204
|
+
const shortId = cleanId.replace(/^cline-pass\//, '')
|
|
205
|
+
const targetKey = modelRoutes[cleanId] || modelRoutes[shortId] || null
|
|
206
|
+
if (!targetKey) return null
|
|
207
|
+
|
|
208
|
+
const targetAcc = safeAccounts.find((a) => matchesAccount(a, targetKey))
|
|
209
|
+
if (targetAcc && !isAccountBlocked(targetAcc)) {
|
|
210
|
+
return targetAcc
|
|
211
|
+
}
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId = '', accountMode = 'least_used', model = '', modelRoutes = {}) {
|
|
192
216
|
const safeAccounts = Array.isArray(accounts) ? accounts.filter(a => a && a.present !== false) : []
|
|
193
217
|
if (safeAccounts.length === 0) {
|
|
194
218
|
const fallback = Array.isArray(accounts) && accounts.length > 0 ? accounts[0] : null
|
|
@@ -209,7 +233,12 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
209
233
|
|
|
210
234
|
const pinnedAvailable = pinnedAccount && !isAccountBlocked(pinnedAccount) ? pinnedAccount : null
|
|
211
235
|
|
|
236
|
+
const routedAccount = resolveModelRouteAccount(model, modelRoutes, safeAccounts)
|
|
237
|
+
|
|
212
238
|
const pickCandidate = () => {
|
|
239
|
+
if (routedAccount) {
|
|
240
|
+
return routedAccount
|
|
241
|
+
}
|
|
213
242
|
if (accountMode === 'round_robin') {
|
|
214
243
|
return getNextRoundRobinAccount(safeAccounts) || safeAccounts[0]
|
|
215
244
|
}
|
|
@@ -222,10 +251,13 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
222
251
|
return { account: pinnedAvailable, isReassigned: false, reason: 'pinned_active' }
|
|
223
252
|
}
|
|
224
253
|
const chosen = pickCandidate()
|
|
254
|
+
const noSessReason = routedAccount
|
|
255
|
+
? 'model_route'
|
|
256
|
+
: (pinnedAccount ? 'pinned_in_cooldown_fallback' : (accountMode === 'round_robin' ? 'round_robin_ad_hoc' : 'no_session_id'))
|
|
225
257
|
return {
|
|
226
258
|
account: chosen,
|
|
227
259
|
isReassigned: false,
|
|
228
|
-
reason:
|
|
260
|
+
reason: noSessReason
|
|
229
261
|
}
|
|
230
262
|
}
|
|
231
263
|
|
|
@@ -260,6 +292,21 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
260
292
|
}
|
|
261
293
|
}
|
|
262
294
|
|
|
295
|
+
// If model route is specified and available, enforce routed account:
|
|
296
|
+
if (routedAccount) {
|
|
297
|
+
const reassigned = Boolean(existing && !matchesAccount(routedAccount, existing.accountId))
|
|
298
|
+
sessions.set(sessionId, {
|
|
299
|
+
accountId: routedAccount.apiKeyEnv || routedAccount.id,
|
|
300
|
+
assignedAt: existing ? existing.assignedAt : now,
|
|
301
|
+
lastUsedAt: now
|
|
302
|
+
})
|
|
303
|
+
return {
|
|
304
|
+
account: routedAccount,
|
|
305
|
+
isReassigned: reassigned,
|
|
306
|
+
reason: 'model_route'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
263
310
|
// If in auto mode or pinned account is unavailable:
|
|
264
311
|
if (existing) {
|
|
265
312
|
const assigned = safeAccounts.find(a => matchesAccount(a, existing.accountId))
|
package/lib/slash-command.js
CHANGED
|
@@ -92,7 +92,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// 3. Subcommand /cline switch <account>
|
|
95
|
-
if (subcmd === 'switch') {
|
|
95
|
+
if (subcmd === 'switch' || subcmd === 'account' || subcmd === 'use') {
|
|
96
96
|
if (!param) {
|
|
97
97
|
return 'Please specify account: /cline switch <KEY_NAME>'
|
|
98
98
|
}
|
|
@@ -125,8 +125,22 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
125
125
|
return `Rotation skipped: ${res.message || 'no alternative configured accounts in pool'}.`
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// 5. Subcommand /cline ping (fresh host reachability probe)
|
|
128
|
+
// 5. Subcommand /cline ping (fresh host reachability or model probe)
|
|
129
129
|
if (subcmd === 'ping') {
|
|
130
|
+
if (param) {
|
|
131
|
+
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
132
|
+
if (!activeKey.value) {
|
|
133
|
+
return `Cline API Ping Failed: Missing API key for ${activeKey.envName}.`
|
|
134
|
+
}
|
|
135
|
+
const res = await smokeChat(pub.baseUrl, activeKey.value, {
|
|
136
|
+
model: param,
|
|
137
|
+
timeoutMs: Math.min(pub.smokeTimeoutMs || 25000, 15000),
|
|
138
|
+
})
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
return `Cline Model Pong: ${param} responded in ${res.latencyMs} ms (HTTP ${res.status || 200})`
|
|
141
|
+
}
|
|
142
|
+
return `Cline Model Ping Failed for ${param}: ${res.error || `HTTP ${res.status}`}`
|
|
143
|
+
}
|
|
130
144
|
const health = await probeHealth(pub.baseUrl, { bypassCache: true, timeoutMs: 5000 })
|
|
131
145
|
if (health.ok) {
|
|
132
146
|
return `Cline API Pong: ${pub.baseUrl} is reachable (latency: ${health.latencyMs} ms, HTTP ${health.status})`
|
|
@@ -141,7 +155,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
141
155
|
summary,
|
|
142
156
|
'',
|
|
143
157
|
`Requests: ${sessionStats.successfulRequests} successful / ${sessionStats.totalRequests} total${sessionStats.abortedRequests ? ` (${sessionStats.abortedRequests} aborted)` : ''}${sessionStats.failedRequests ? ` (${sessionStats.failedRequests} failed)` : ''}`,
|
|
144
|
-
`Tokens: ~${sessionStats.totalTokensEst} total (~${sessionStats.promptTokensEst} prompt, ~${sessionStats.completionTokensEst} completion)`,
|
|
158
|
+
`Tokens: ~${sessionStats.totalTokensEst} total (~${sessionStats.promptTokensEst} prompt, ~${sessionStats.completionTokensEst} completion${sessionStats.reasoningTokensEst ? `, ~${sessionStats.reasoningTokensEst} reasoning` : ''})`,
|
|
145
159
|
`Last Latency: ${sessionStats.lastLatencyMs !== null ? `${sessionStats.lastLatencyMs} ms` : '—'}`,
|
|
146
160
|
`Last Request: ${sessionStats.lastRequestAt ? new Date(sessionStats.lastRequestAt).toLocaleTimeString() : '—'}`,
|
|
147
161
|
]
|
|
@@ -151,7 +165,8 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
151
165
|
if (sessionStats.byModel && Object.keys(sessionStats.byModel).length) {
|
|
152
166
|
lines.push('', 'By Model:')
|
|
153
167
|
for (const [mId, mStats] of Object.entries(sessionStats.byModel)) {
|
|
154
|
-
|
|
168
|
+
const rStr = mStats.reasoningTokens ? ` (${mStats.reasoningTokens} reasoning)` : ''
|
|
169
|
+
lines.push(` ${mId}: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens${rStr}`)
|
|
155
170
|
}
|
|
156
171
|
}
|
|
157
172
|
if (sessionStats.lastSmoke) {
|
package/lib/stats-storage.js
CHANGED
|
@@ -28,6 +28,7 @@ export function createEmptyStats() {
|
|
|
28
28
|
rateLimited429: 0,
|
|
29
29
|
promptTokens: 0,
|
|
30
30
|
completionTokens: 0,
|
|
31
|
+
reasoningTokens: 0,
|
|
31
32
|
totalTokens: 0
|
|
32
33
|
},
|
|
33
34
|
byDay: {},
|
|
@@ -230,6 +231,7 @@ export function recordUsage({
|
|
|
230
231
|
accountId = 'default',
|
|
231
232
|
promptTokens = 0,
|
|
232
233
|
completionTokens = 0,
|
|
234
|
+
reasoningTokens = 0,
|
|
233
235
|
totalTokens = 0,
|
|
234
236
|
isError = false,
|
|
235
237
|
is429 = false,
|
|
@@ -244,6 +246,7 @@ export function recordUsage({
|
|
|
244
246
|
|
|
245
247
|
const safePrompt = Math.max(0, Number(promptTokens) || 0)
|
|
246
248
|
const safeComp = Math.max(0, Number(completionTokens) || 0)
|
|
249
|
+
const safeReasoning = Math.max(0, Number(reasoningTokens) || 0)
|
|
247
250
|
const safeTotal = Math.max(safePrompt + safeComp, Number(totalTokens) || 0)
|
|
248
251
|
|
|
249
252
|
// Totals
|
|
@@ -258,6 +261,7 @@ export function recordUsage({
|
|
|
258
261
|
}
|
|
259
262
|
inMemoryStats.totals.promptTokens += safePrompt
|
|
260
263
|
inMemoryStats.totals.completionTokens += safeComp
|
|
264
|
+
inMemoryStats.totals.reasoningTokens = (inMemoryStats.totals.reasoningTokens || 0) + safeReasoning
|
|
261
265
|
inMemoryStats.totals.totalTokens += safeTotal
|
|
262
266
|
|
|
263
267
|
// Helper to increment bucket
|
|
@@ -267,12 +271,14 @@ export function recordUsage({
|
|
|
267
271
|
requests: 0,
|
|
268
272
|
promptTokens: 0,
|
|
269
273
|
completionTokens: 0,
|
|
274
|
+
reasoningTokens: 0,
|
|
270
275
|
totalTokens: 0
|
|
271
276
|
}
|
|
272
277
|
}
|
|
273
278
|
container[key].requests += 1
|
|
274
279
|
container[key].promptTokens += safePrompt
|
|
275
280
|
container[key].completionTokens += safeComp
|
|
281
|
+
container[key].reasoningTokens = (container[key].reasoningTokens || 0) + safeReasoning
|
|
276
282
|
container[key].totalTokens += safeTotal
|
|
277
283
|
}
|
|
278
284
|
|
|
@@ -303,3 +309,25 @@ export function resetStats(customPath) {
|
|
|
303
309
|
saveStatsSync(customPath)
|
|
304
310
|
return inMemoryStats
|
|
305
311
|
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Export usage analytics as a formatted CSV string.
|
|
316
|
+
*
|
|
317
|
+
* @returns {string} CSV format
|
|
318
|
+
*/
|
|
319
|
+
export function exportStatsAsCsv() {
|
|
320
|
+
const lines = ['Dimension,Key,Requests,PromptTokens,CompletionTokens,ReasoningTokens,TotalTokens']
|
|
321
|
+
const addRows = (dimension, obj) => {
|
|
322
|
+
for (const [key, val] of Object.entries(obj || {})) {
|
|
323
|
+
lines.push(`${dimension},"${key}",${val.requests || 0},${val.promptTokens || 0},${val.completionTokens || 0},${val.reasoningTokens || 0},${val.totalTokens || 0}`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const totals = inMemoryStats.totals || {}
|
|
327
|
+
lines.push(`Total,"all",${totals.requests || 0},${totals.promptTokens || 0},${totals.completionTokens || 0},${totals.reasoningTokens || 0},${totals.totalTokens || 0}`)
|
|
328
|
+
addRows('Day', inMemoryStats.byDay)
|
|
329
|
+
addRows('Month', inMemoryStats.byMonth)
|
|
330
|
+
addRows('Model', inMemoryStats.byModel)
|
|
331
|
+
addRows('Account', inMemoryStats.byAccount)
|
|
332
|
+
return lines.join('\n')
|
|
333
|
+
}
|
package/lib/telemetry.js
CHANGED
|
@@ -9,6 +9,7 @@ export const sessionStats = {
|
|
|
9
9
|
abortedRequests: 0,
|
|
10
10
|
promptTokensEst: 0,
|
|
11
11
|
completionTokensEst: 0,
|
|
12
|
+
reasoningTokensEst: 0,
|
|
12
13
|
totalTokensEst: 0,
|
|
13
14
|
lastLatencyMs: null,
|
|
14
15
|
lastRequestAt: null,
|
|
@@ -24,6 +25,7 @@ export function recordSessionRequest({
|
|
|
24
25
|
aborted = false,
|
|
25
26
|
promptTokens = 0,
|
|
26
27
|
completionTokens = 0,
|
|
28
|
+
reasoningTokens = 0,
|
|
27
29
|
model,
|
|
28
30
|
}) {
|
|
29
31
|
sessionStats.totalRequests += 1
|
|
@@ -40,14 +42,16 @@ export function recordSessionRequest({
|
|
|
40
42
|
sessionStats.lastRequestAt = Date.now()
|
|
41
43
|
sessionStats.promptTokensEst += Number(promptTokens) || 0
|
|
42
44
|
sessionStats.completionTokensEst += Number(completionTokens) || 0
|
|
45
|
+
sessionStats.reasoningTokensEst = (sessionStats.reasoningTokensEst || 0) + (Number(reasoningTokens) || 0)
|
|
43
46
|
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
44
47
|
|
|
45
48
|
if (model) {
|
|
46
49
|
if (!sessionStats.byModel) sessionStats.byModel = {}
|
|
47
|
-
const m = sessionStats.byModel[model] || { requests: 0, promptTokens: 0, completionTokens: 0 }
|
|
50
|
+
const m = sessionStats.byModel[model] || { requests: 0, promptTokens: 0, completionTokens: 0, reasoningTokens: 0 }
|
|
48
51
|
m.requests += 1
|
|
49
52
|
m.promptTokens += Number(promptTokens) || 0
|
|
50
53
|
m.completionTokens += Number(completionTokens) || 0
|
|
54
|
+
m.reasoningTokens = (m.reasoningTokens || 0) + (Number(reasoningTokens) || 0)
|
|
51
55
|
sessionStats.byModel[model] = m
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -69,6 +73,7 @@ export function resetSessionStats() {
|
|
|
69
73
|
sessionStats.abortedRequests = 0
|
|
70
74
|
sessionStats.promptTokensEst = 0
|
|
71
75
|
sessionStats.completionTokensEst = 0
|
|
76
|
+
sessionStats.reasoningTokensEst = 0
|
|
72
77
|
sessionStats.totalTokensEst = 0
|
|
73
78
|
sessionStats.lastLatencyMs = null
|
|
74
79
|
sessionStats.lastRequestAt = null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.22",
|
|
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",
|