@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.
@@ -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
- export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId = '', accountMode = 'least_used') {
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: pinnedAccount ? 'pinned_in_cooldown_fallback' : (accountMode === 'round_robin' ? 'round_robin_ad_hoc' : 'no_session_id')
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))
@@ -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
- lines.push(` ${mId}: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens`)
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) {
@@ -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.20",
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",