@goodandready/dsh-clinebot 0.5.21 → 0.5.23
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 +13 -0
- package/lib/client.js +166 -6
- package/lib/config.js +3 -0
- package/lib/proxy-server.js +0 -7
- package/lib/proxy-token.js +22 -2
- package/lib/routes/models.js +35 -1
- package/lib/routes/proxy.js +15 -8
- package/lib/routes/settings.js +20 -1
- package/lib/session-router.js +49 -10
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.23] - 2026-10-13
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
- **Dead Exports & Helper Hygiene (Issue #220)**: Removed unused `getOrStartProxyPort` export from `lib/proxy-server.js` and unused cursor helper exports `getRoundRobinCursor` and `resetRoundRobinCursor` from `lib/session-router.js`. Internal round-robin cursor rotation and lifecycle resets remain fully operational.
|
|
12
|
+
|
|
13
|
+
## [0.5.22] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Per-Model Account Routing (`modelRoutes`) (Issue #218)**: Map specific models or patterns to dedicated accounts in the pool (e.g. `deepseek-v4-pro -> KEY_PRO`) with automatic graceful fallback when the assigned account is in cooldown or unavailable. Added dedicated management UI in Accounts tab with route badges and model selector.
|
|
17
|
+
- **Reasoning Tokens & Detailed Telemetry (`reasoningTokens`, CSV Export) (Issue #218)**: Full tracking of reasoning tokens (`reasoning_tokens`, `completion_tokens_details.reasoning_tokens`) across streaming SSE and non-streaming proxy responses in totals, byModel, and byDay dimensions. Added `GET /dsh-clinebot/stats/export` for complete CSV export and added reasoning token breakdown to Settings stats overview and `/cline stats`.
|
|
18
|
+
- **Model Speed Benchmark / TTFT Latency Probe (Issue #218)**: Added `POST /dsh-clinebot/models/probe` endpoint to benchmark real TTFT latency for any specific model on demand with visual latency badges and trigger buttons in the Models table.
|
|
19
|
+
- **Chat UX & `/cline` Enhancements (Issue #218)**: Added `/cline account <KEY_NAME>` and `/cline use <KEY_NAME>` aliases to switch active account directly from chat, enabled `/cline ping [model]` to probe specific model latency, and added reasoning tokens breakdown to `/cline stats`.
|
|
20
|
+
|
|
8
21
|
## [0.5.21] - 2026-10-12
|
|
9
22
|
|
|
10
23
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -86,6 +86,11 @@ const en = {
|
|
|
86
86
|
'accounts.mode_label': 'Pool Strategy',
|
|
87
87
|
'accounts.mode_least_used': 'Least Used (Quota)',
|
|
88
88
|
'accounts.mode_round_robin': 'Round Robin (Sequential)',
|
|
89
|
+
'accounts.routes_title': 'Per-Model Account Routing',
|
|
90
|
+
'accounts.routes_desc': 'Optionally route specific models to designated accounts in the pool.',
|
|
91
|
+
'accounts.route_model_placeholder': 'Model ID (e.g. cline-pass/deepseek-v4-pro)',
|
|
92
|
+
'accounts.route_account_placeholder': 'Target Account',
|
|
93
|
+
'accounts.route_btn_add': 'Add Route',
|
|
89
94
|
'quota.title': '📊 ClinePass Quota & Rate Limits',
|
|
90
95
|
'quota.desc': 'Official rolling window request limits from ClinePass',
|
|
91
96
|
'quota.account': 'Account: {email} · Plan: {plan}',
|
|
@@ -142,6 +147,9 @@ const en = {
|
|
|
142
147
|
'models.th_id': 'Model ID',
|
|
143
148
|
'models.th_ctx': 'Context',
|
|
144
149
|
'models.th_caps': 'Capabilities',
|
|
150
|
+
'models.th_route': 'Account Route',
|
|
151
|
+
'models.route_auto': 'Auto / Pool',
|
|
152
|
+
'models.btn_probe': 'Ping',
|
|
145
153
|
'models.star': 'Star',
|
|
146
154
|
'models.auto_new': 'New',
|
|
147
155
|
'models.apply_original_all': '⚡ Original Specs',
|
|
@@ -165,6 +173,8 @@ const en = {
|
|
|
165
173
|
'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
|
|
166
174
|
'stats.requests': 'Successful / Total Requests',
|
|
167
175
|
'stats.tokens': 'Estimated Session Tokens',
|
|
176
|
+
'stats.reasoning_tokens': 'Reasoning Tokens (Thinking)',
|
|
177
|
+
'stats.export_csv': 'Export CSV',
|
|
168
178
|
'stats.latency': 'Last Request Latency',
|
|
169
179
|
'stats.last_req': 'Last Request Time',
|
|
170
180
|
'models.custom_add_title': 'Custom Models',
|
|
@@ -249,6 +259,11 @@ const en = {
|
|
|
249
259
|
'accounts.mode_label': '调度策略',
|
|
250
260
|
'accounts.mode_least_used': '低占用优先 (Least Used)',
|
|
251
261
|
'accounts.mode_round_robin': '顺序轮询 (Round Robin)',
|
|
262
|
+
'accounts.routes_title': '特定模型专属路由 (Per-Model)',
|
|
263
|
+
'accounts.routes_desc': '可针对特定模型指定绑定的账号凭据(如将昂贵模型指向特定账号)。',
|
|
264
|
+
'accounts.route_model_placeholder': '模型 ID (例如 cline-pass/deepseek-v4-pro)',
|
|
265
|
+
'accounts.route_account_placeholder': '目标账号',
|
|
266
|
+
'accounts.route_btn_add': '添加路由',
|
|
252
267
|
'quota.title': '📊 ClinePass 用量配额与限额监控',
|
|
253
268
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
254
269
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
@@ -305,6 +320,9 @@ const en = {
|
|
|
305
320
|
'models.th_id': '模型标识符 (ID)',
|
|
306
321
|
'models.th_ctx': '上下文窗口',
|
|
307
322
|
'models.th_caps': '特性能力',
|
|
323
|
+
'models.th_route': '指定路由账号',
|
|
324
|
+
'models.route_auto': '自动 / 默认池',
|
|
325
|
+
'models.btn_probe': '测试延迟',
|
|
308
326
|
'models.star': '推荐',
|
|
309
327
|
'models.auto_new': '新上线',
|
|
310
328
|
'models.apply_original_all': '⚡ 原厂规格',
|
|
@@ -328,6 +346,8 @@ const en = {
|
|
|
328
346
|
'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
|
|
329
347
|
'stats.requests': '成功 / 总请求次数',
|
|
330
348
|
'stats.tokens': '会话累计 Token 估算',
|
|
349
|
+
'stats.reasoning_tokens': '深度思考 Token 消耗',
|
|
350
|
+
'stats.export_csv': '导出 CSV',
|
|
331
351
|
'stats.latency': '最近一次请求延迟',
|
|
332
352
|
'stats.last_req': '最近请求时间',
|
|
333
353
|
'models.custom_add_title': '自定义模型',
|
|
@@ -816,7 +836,18 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
816
836
|
}
|
|
817
837
|
|
|
818
838
|
// --- src/client/components/accounts-section.js ---
|
|
819
|
-
function AccountsSection({
|
|
839
|
+
function AccountsSection({
|
|
840
|
+
status,
|
|
841
|
+
busy,
|
|
842
|
+
handlePinAccount,
|
|
843
|
+
handleAddAccount,
|
|
844
|
+
handleDeleteAccount,
|
|
845
|
+
accountMode,
|
|
846
|
+
handleUpdateAccountMode,
|
|
847
|
+
modelRoutes = {},
|
|
848
|
+
onUpdateModelRoutes,
|
|
849
|
+
t,
|
|
850
|
+
}) {
|
|
820
851
|
if (!status.accounts || !status.accounts.length) return null
|
|
821
852
|
|
|
822
853
|
const [showAdd, setShowAdd] = React.useState(false)
|
|
@@ -825,6 +856,8 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
825
856
|
const [newKey, setNewKey] = React.useState('')
|
|
826
857
|
const [showKey, setShowKey] = React.useState(false)
|
|
827
858
|
const [deleteSecret, setDeleteSecret] = React.useState(true)
|
|
859
|
+
const [routeModel, setRouteModel] = React.useState('')
|
|
860
|
+
const [routeAccount, setRouteAccount] = React.useState('')
|
|
828
861
|
|
|
829
862
|
function handleOpenAdd() {
|
|
830
863
|
let max = 1
|
|
@@ -1126,7 +1159,112 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
1126
1159
|
reason: status.lastRotation.reason,
|
|
1127
1160
|
})
|
|
1128
1161
|
)
|
|
1129
|
-
: null
|
|
1162
|
+
: null,
|
|
1163
|
+
React.createElement(
|
|
1164
|
+
'div',
|
|
1165
|
+
{ style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1166
|
+
React.createElement(
|
|
1167
|
+
'div',
|
|
1168
|
+
{ style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)', marginBottom: '4px' } },
|
|
1169
|
+
t('accounts.routes_title')
|
|
1170
|
+
),
|
|
1171
|
+
React.createElement(
|
|
1172
|
+
'div',
|
|
1173
|
+
{ style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', marginBottom: '8px' } },
|
|
1174
|
+
t('accounts.routes_desc')
|
|
1175
|
+
),
|
|
1176
|
+
React.createElement(
|
|
1177
|
+
'div',
|
|
1178
|
+
{ style: { display: 'flex', gap: '6px', flexWrap: 'wrap', alignItems: 'center' } },
|
|
1179
|
+
React.createElement('input', {
|
|
1180
|
+
type: 'text',
|
|
1181
|
+
className: 'cb-input',
|
|
1182
|
+
style: { flex: '2 1 140px', height: '26px', fontSize: '11px', padding: '0 6px' },
|
|
1183
|
+
placeholder: t('accounts.route_model_placeholder'),
|
|
1184
|
+
value: routeModel,
|
|
1185
|
+
onChange: (e) => setRouteModel(e.target.value),
|
|
1186
|
+
}),
|
|
1187
|
+
React.createElement(
|
|
1188
|
+
'select',
|
|
1189
|
+
{
|
|
1190
|
+
className: 'cb-select',
|
|
1191
|
+
style: { flex: '1 1 120px', height: '26px', fontSize: '11px', padding: '0 4px' },
|
|
1192
|
+
value: routeAccount,
|
|
1193
|
+
onChange: (e) => setRouteAccount(e.target.value),
|
|
1194
|
+
},
|
|
1195
|
+
React.createElement('option', { value: '' }, t('accounts.route_account_placeholder')),
|
|
1196
|
+
(status.accounts || []).map((acc) => {
|
|
1197
|
+
const keyName = acc.apiKeyEnv || acc.id
|
|
1198
|
+
return React.createElement('option', { key: keyName, value: keyName }, `${acc.label || keyName} (${keyName})`)
|
|
1199
|
+
})
|
|
1200
|
+
),
|
|
1201
|
+
React.createElement(
|
|
1202
|
+
'button',
|
|
1203
|
+
{
|
|
1204
|
+
type: 'button',
|
|
1205
|
+
className: 'cb-btn cb-btn-primary',
|
|
1206
|
+
style: { padding: '3px 8px', fontSize: '11px' },
|
|
1207
|
+
disabled: !routeModel.trim() || !routeAccount.trim() || !!busy,
|
|
1208
|
+
onClick: () => {
|
|
1209
|
+
if (typeof onUpdateModelRoutes === 'function') {
|
|
1210
|
+
const next = { ...(modelRoutes || {}), [routeModel.trim()]: routeAccount.trim() }
|
|
1211
|
+
onUpdateModelRoutes(next)
|
|
1212
|
+
setRouteModel('')
|
|
1213
|
+
setRouteAccount('')
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
t('accounts.route_btn_add')
|
|
1218
|
+
)
|
|
1219
|
+
),
|
|
1220
|
+
Object.keys(modelRoutes || {}).length > 0
|
|
1221
|
+
? React.createElement(
|
|
1222
|
+
'div',
|
|
1223
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '4px', marginTop: '8px' } },
|
|
1224
|
+
Object.entries(modelRoutes || {}).map(([mId, accKey]) =>
|
|
1225
|
+
React.createElement(
|
|
1226
|
+
'div',
|
|
1227
|
+
{
|
|
1228
|
+
key: mId,
|
|
1229
|
+
style: {
|
|
1230
|
+
display: 'flex',
|
|
1231
|
+
justifyContent: 'space-between',
|
|
1232
|
+
alignItems: 'center',
|
|
1233
|
+
padding: '3px 8px',
|
|
1234
|
+
fontSize: '11px',
|
|
1235
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1236
|
+
borderRadius: '4px',
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
React.createElement(
|
|
1240
|
+
'span',
|
|
1241
|
+
null,
|
|
1242
|
+
React.createElement('code', null, mId),
|
|
1243
|
+
' ➔ ',
|
|
1244
|
+
React.createElement('strong', null, accKey)
|
|
1245
|
+
),
|
|
1246
|
+
React.createElement(
|
|
1247
|
+
'button',
|
|
1248
|
+
{
|
|
1249
|
+
type: 'button',
|
|
1250
|
+
className: 'cb-btn cb-btn-danger',
|
|
1251
|
+
style: { padding: '1px 6px', fontSize: '10px' },
|
|
1252
|
+
disabled: !!busy,
|
|
1253
|
+
onClick: () => {
|
|
1254
|
+
if (typeof onUpdateModelRoutes === 'function') {
|
|
1255
|
+
const next = { ...(modelRoutes || {}) }
|
|
1256
|
+
delete next[mId]
|
|
1257
|
+
onUpdateModelRoutes(next)
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
'✕'
|
|
1262
|
+
)
|
|
1263
|
+
)
|
|
1264
|
+
)
|
|
1265
|
+
)
|
|
1266
|
+
: null
|
|
1267
|
+
)
|
|
1130
1268
|
)
|
|
1131
1269
|
}
|
|
1132
1270
|
|
|
@@ -1915,7 +2053,17 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1915
2053
|
React.createElement('div', { className: 'cb-section-title' }, t('stats.title')),
|
|
1916
2054
|
React.createElement(
|
|
1917
2055
|
'div',
|
|
1918
|
-
|
|
2056
|
+
{ style: { display: 'flex', gap: '6px', alignItems: 'center' } },
|
|
2057
|
+
React.createElement(
|
|
2058
|
+
'a',
|
|
2059
|
+
{
|
|
2060
|
+
href: '/dsh-clinebot/stats/export',
|
|
2061
|
+
download: 'clinebot-stats.csv',
|
|
2062
|
+
className: 'cb-btn',
|
|
2063
|
+
style: { padding: '2px 8px', fontSize: '11px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
2064
|
+
},
|
|
2065
|
+
`📥 ${t('stats.export_csv')}`
|
|
2066
|
+
),
|
|
1919
2067
|
confirmReset
|
|
1920
2068
|
? React.createElement(
|
|
1921
2069
|
React.Fragment,
|
|
@@ -1971,8 +2119,18 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1971
2119
|
React.createElement(
|
|
1972
2120
|
'div',
|
|
1973
2121
|
{ className: 'cb-stat-box' },
|
|
1974
|
-
React.createElement(
|
|
1975
|
-
|
|
2122
|
+
React.createElement(
|
|
2123
|
+
'div',
|
|
2124
|
+
{ className: 'cb-stat-val' },
|
|
2125
|
+
`${Number(totalTokens).toLocaleString()}`
|
|
2126
|
+
),
|
|
2127
|
+
React.createElement(
|
|
2128
|
+
'div',
|
|
2129
|
+
{ className: 'cb-stat-lbl' },
|
|
2130
|
+
totals.reasoningTokens || sess.reasoningTokensEst
|
|
2131
|
+
? `${t('stats.tokens')} (${Number(totals.reasoningTokens || sess.reasoningTokensEst).toLocaleString()} reasoning)`
|
|
2132
|
+
: t('stats.tokens')
|
|
2133
|
+
)
|
|
1976
2134
|
),
|
|
1977
2135
|
React.createElement(
|
|
1978
2136
|
'div',
|
|
@@ -2010,7 +2168,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2010
2168
|
},
|
|
2011
2169
|
},
|
|
2012
2170
|
React.createElement('span', { style: { fontFamily: 'monospace' } }, modId),
|
|
2013
|
-
React.createElement('span', null, `${data.requests || 0} reqs · ${Number(data.totalTokens ?? data.tokens ?? 0).toLocaleString()} tok`)
|
|
2171
|
+
React.createElement('span', null, `${data.requests || 0} reqs · ${Number(data.totalTokens ?? data.tokens ?? 0).toLocaleString()} tok${data.reasoningTokens ? ` (${Number(data.reasoningTokens).toLocaleString()} thk)` : ''}`)
|
|
2014
2172
|
)
|
|
2015
2173
|
)
|
|
2016
2174
|
)
|
|
@@ -2578,6 +2736,8 @@ function SettingsPage(props) {
|
|
|
2578
2736
|
status, busy, handlePinAccount, handleAddAccount, handleDeleteAccount,
|
|
2579
2737
|
accountMode: draft?.accountMode || status?.config?.accountMode || 'least_used',
|
|
2580
2738
|
handleUpdateAccountMode: (mode) => handleConfigPatch({ accountMode: mode }),
|
|
2739
|
+
modelRoutes: draft?.modelRoutes || status?.config?.modelRoutes || {},
|
|
2740
|
+
onUpdateModelRoutes: (routes) => handleConfigPatch({ modelRoutes: routes }),
|
|
2581
2741
|
t,
|
|
2582
2742
|
}),
|
|
2583
2743
|
|
package/lib/config.js
CHANGED
|
@@ -60,6 +60,8 @@ export const Config = z.object({
|
|
|
60
60
|
.description('Custom user-defined model context length and max token overrides.').volatile(),
|
|
61
61
|
modelReasoningDefaults: z.dict(z.string()).default({})
|
|
62
62
|
.description('Configured default reasoning effort per model (low, medium, high, max).').volatile(),
|
|
63
|
+
modelRoutes: z.dict(z.string()).default({})
|
|
64
|
+
.description('Per-model target account routing map (e.g. { "deepseek-v4-pro": "KEY_2" }).').volatile(),
|
|
63
65
|
customModels: z.array(z.object({
|
|
64
66
|
id: z.string(),
|
|
65
67
|
name: z.string(),
|
|
@@ -168,6 +170,7 @@ export function publicConfig(cfg) {
|
|
|
168
170
|
planSyncedAt,
|
|
169
171
|
modelContextOverrides,
|
|
170
172
|
modelReasoningDefaults: cfg?.modelReasoningDefaults && typeof cfg.modelReasoningDefaults === 'object' ? cfg.modelReasoningDefaults : {},
|
|
173
|
+
modelRoutes: cfg?.modelRoutes && typeof cfg.modelRoutes === 'object' ? cfg.modelRoutes : {},
|
|
171
174
|
customModels: custom,
|
|
172
175
|
proxyMode: cfg?.proxyMode !== false,
|
|
173
176
|
statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
|
package/lib/proxy-server.js
CHANGED
|
@@ -136,10 +136,3 @@ export async function stopLoopbackProxyServer() {
|
|
|
136
136
|
})
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
export async function getOrStartProxyPort(ctx, routeEnv) {
|
|
140
|
-
if (activeProxyPort) return activeProxyPort
|
|
141
|
-
if (routeEnv) {
|
|
142
|
-
return await startLoopbackProxyServer(ctx, routeEnv)
|
|
143
|
-
}
|
|
144
|
-
return null
|
|
145
|
-
}
|
package/lib/proxy-token.js
CHANGED
|
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
|
|
|
14
14
|
let cachedToken = null
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
|
-
let packageVersion = '0.5.
|
|
17
|
+
let packageVersion = '0.5.23'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.23'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
|
@@ -118,3 +118,23 @@ export function sendProxyTerminalError(res, { hadTimeout, is429Exhausted, lastEr
|
|
|
118
118
|
},
|
|
119
119
|
})
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Safely extract standard and reasoning token counts from usage object.
|
|
125
|
+
*/
|
|
126
|
+
export function extractUsageMetrics(usage) {
|
|
127
|
+
if (!usage || typeof usage !== 'object') {
|
|
128
|
+
return { promptTokens: 0, completionTokens: 0, reasoningTokens: 0, totalTokens: 0 }
|
|
129
|
+
}
|
|
130
|
+
const promptTokens = Number(usage.prompt_tokens) || 0
|
|
131
|
+
const completionTokens = Number(usage.completion_tokens) || 0
|
|
132
|
+
const reasoningTokens = Number(
|
|
133
|
+
usage.completion_tokens_details?.reasoning_tokens ||
|
|
134
|
+
usage.reasoning_tokens ||
|
|
135
|
+
usage.prompt_tokens_details?.reasoning_tokens ||
|
|
136
|
+
0
|
|
137
|
+
)
|
|
138
|
+
const totalTokens = Number(usage.total_tokens) || (promptTokens + completionTokens)
|
|
139
|
+
return { promptTokens, completionTokens, reasoningTokens, totalTokens }
|
|
140
|
+
}
|
package/lib/routes/models.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertTrustedSettingsRequest } from '../access.js'
|
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
|
|
5
5
|
import { getAllModels, saveModelsDiskCache, getOriginalModelContext, getOriginalModelMaxTokens } from '../models.js'
|
|
6
|
-
import { fetchUsageLimits } from '../cline-client.js'
|
|
6
|
+
import { fetchUsageLimits, smokeChat } from '../cline-client.js'
|
|
7
7
|
import { publicUsage } from '../http.js'
|
|
8
8
|
|
|
9
9
|
export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
@@ -240,4 +240,38 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
240
240
|
}
|
|
241
241
|
},
|
|
242
242
|
}), 'dsh-clinebot: /usage')
|
|
243
|
+
|
|
244
|
+
// POST /dsh-clinebot/models/probe — benchmark TTFT latency for specific model
|
|
245
|
+
ctx.effect(() => ctx.webServer.register({
|
|
246
|
+
kind: 'exact',
|
|
247
|
+
path: '/dsh-clinebot/models/probe',
|
|
248
|
+
handler: async (req, res) => {
|
|
249
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
250
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
251
|
+
try {
|
|
252
|
+
const bodyBuf = await readBody(req)
|
|
253
|
+
let body = {}
|
|
254
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
|
|
255
|
+
const pub = publicConfig(live())
|
|
256
|
+
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
257
|
+
if (!activeKey.value) {
|
|
258
|
+
return writeJson(res, 400, { ok: false, error: 'API key not found' })
|
|
259
|
+
}
|
|
260
|
+
const modelId = body.modelId || body.model || pub.defaultModel
|
|
261
|
+
const result = await smokeChat(pub.baseUrl, activeKey.value, {
|
|
262
|
+
model: modelId,
|
|
263
|
+
timeoutMs: Math.min(pub.smokeTimeoutMs || 25000, 15000),
|
|
264
|
+
})
|
|
265
|
+
return writeJson(res, 200, {
|
|
266
|
+
ok: result.ok,
|
|
267
|
+
modelId,
|
|
268
|
+
latencyMs: result.latencyMs,
|
|
269
|
+
status: result.status,
|
|
270
|
+
error: result.error || null,
|
|
271
|
+
})
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
}), 'dsh-clinebot: /models/probe')
|
|
243
277
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isAuthorizedProxyRequest,
|
|
28
28
|
USER_AGENT,
|
|
29
29
|
sendProxyTerminalError,
|
|
30
|
+
extractUsageMetrics,
|
|
30
31
|
} from '../proxy-token.js'
|
|
31
32
|
|
|
32
33
|
export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, USER_AGENT }
|
|
@@ -120,7 +121,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
120
121
|
return
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount, pub.accountMode)
|
|
124
|
+
const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount, pub.accountMode, body?.model, pub.modelRoutes)
|
|
124
125
|
const targetAccount = routeResult.account
|
|
125
126
|
lastTargetAccount = targetAccount
|
|
126
127
|
|
|
@@ -297,6 +298,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
297
298
|
|
|
298
299
|
let promptTokens = 0
|
|
299
300
|
let completionTokens = 0
|
|
301
|
+
let reasoningTokens = 0
|
|
300
302
|
let totalTokens = 0
|
|
301
303
|
let buffer = ''
|
|
302
304
|
let streamError = null
|
|
@@ -350,9 +352,11 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
350
352
|
try {
|
|
351
353
|
const parsed = JSON.parse(trimmed.slice(5).trim())
|
|
352
354
|
if (parsed.usage) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
355
|
+
const u = extractUsageMetrics(parsed.usage)
|
|
356
|
+
promptTokens = u.promptTokens || promptTokens
|
|
357
|
+
completionTokens = u.completionTokens || completionTokens
|
|
358
|
+
reasoningTokens = u.reasoningTokens || reasoningTokens
|
|
359
|
+
totalTokens = u.totalTokens || totalTokens
|
|
356
360
|
}
|
|
357
361
|
} catch {}
|
|
358
362
|
}
|
|
@@ -419,6 +423,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
419
423
|
accountId: targetAccount.id || targetAccount.label,
|
|
420
424
|
promptTokens,
|
|
421
425
|
completionTokens,
|
|
426
|
+
reasoningTokens,
|
|
422
427
|
totalTokens: totalTokens || (promptTokens + completionTokens),
|
|
423
428
|
isError: isStreamFailed,
|
|
424
429
|
statsPath: pub.statsPath
|
|
@@ -495,12 +500,14 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
cleanupClientListeners()
|
|
503
|
+
const nonStreamUsage = extractUsageMetrics(resJson?.usage)
|
|
498
504
|
recordUsage({
|
|
499
505
|
model: body.model,
|
|
500
506
|
accountId: targetAccount.id || targetAccount.label,
|
|
501
|
-
promptTokens:
|
|
502
|
-
completionTokens:
|
|
503
|
-
|
|
507
|
+
promptTokens: nonStreamUsage.promptTokens,
|
|
508
|
+
completionTokens: nonStreamUsage.completionTokens,
|
|
509
|
+
reasoningTokens: nonStreamUsage.reasoningTokens,
|
|
510
|
+
totalTokens: nonStreamUsage.totalTokens,
|
|
504
511
|
isError: false,
|
|
505
512
|
statsPath: pub.statsPath
|
|
506
513
|
})
|
|
@@ -582,4 +589,4 @@ export function registerProxyRoutes(ctx, routeEnv) {
|
|
|
582
589
|
path: '/dsh-clinebot/v1/models',
|
|
583
590
|
handler: modelsHandler,
|
|
584
591
|
}), 'dsh-clinebot: /v1/models')
|
|
585
|
-
}
|
|
592
|
+
}
|
package/lib/routes/settings.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertTrustedSettingsRequest } from '../access.js'
|
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { buildStatus } from '../provider-sync.js'
|
|
5
5
|
import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
|
|
6
|
-
import { getStatsSummary, resetStats } from '../stats-storage.js'
|
|
6
|
+
import { getStatsSummary, resetStats, exportStatsAsCsv } from '../stats-storage.js'
|
|
7
7
|
|
|
8
8
|
export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProviderState, triggerAutoDiscover }) {
|
|
9
9
|
// 1. GET /dsh-clinebot/status
|
|
@@ -212,4 +212,23 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
212
212
|
}
|
|
213
213
|
},
|
|
214
214
|
}), 'dsh-clinebot: /stats/reset')
|
|
215
|
+
|
|
216
|
+
// 6. GET /dsh-clinebot/stats/export — export token telemetry as CSV
|
|
217
|
+
ctx.effect(() => ctx.webServer.register({
|
|
218
|
+
kind: 'exact',
|
|
219
|
+
path: '/dsh-clinebot/stats/export',
|
|
220
|
+
handler: async (req, res) => {
|
|
221
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
222
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
223
|
+
try {
|
|
224
|
+
const csv = exportStatsAsCsv()
|
|
225
|
+
res.statusCode = 200
|
|
226
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8')
|
|
227
|
+
res.setHeader('Content-Disposition', 'attachment; filename="clinebot-stats.csv"')
|
|
228
|
+
res.end(csv)
|
|
229
|
+
} catch (err) {
|
|
230
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
}), 'dsh-clinebot: /stats/export')
|
|
215
234
|
}
|
package/lib/session-router.js
CHANGED
|
@@ -132,14 +132,6 @@ export function getAccountUsageMetric(acc) {
|
|
|
132
132
|
|
|
133
133
|
let roundRobinCursor = 0
|
|
134
134
|
|
|
135
|
-
export function getRoundRobinCursor() {
|
|
136
|
-
return roundRobinCursor
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function resetRoundRobinCursor() {
|
|
140
|
-
roundRobinCursor = 0
|
|
141
|
-
}
|
|
142
|
-
|
|
143
135
|
export function getNextRoundRobinAccount(accounts = []) {
|
|
144
136
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
145
137
|
return null
|
|
@@ -188,7 +180,31 @@ export function selectLeastUsedAccount(accounts = []) {
|
|
|
188
180
|
* @param {string} [defaultActiveId] Explicit active account ID, envName or 'auto'
|
|
189
181
|
* @returns {{ account: Object|null, isReassigned: boolean, reason: string }}
|
|
190
182
|
*/
|
|
191
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Match a model ID against modelRoutes map (supports exact match or prefix without cline-pass/).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} [modelId] Target model ID
|
|
187
|
+
* @param {Object} [modelRoutes] Model routing table
|
|
188
|
+
* @param {Array<Object>} [safeAccounts] Available accounts
|
|
189
|
+
* @returns {Object|null}
|
|
190
|
+
*/
|
|
191
|
+
export function resolveModelRouteAccount(modelId, modelRoutes = {}, safeAccounts = []) {
|
|
192
|
+
if (!modelId || !modelRoutes || typeof modelRoutes !== 'object' || !Array.isArray(safeAccounts)) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
const cleanId = String(modelId).trim()
|
|
196
|
+
const shortId = cleanId.replace(/^cline-pass\//, '')
|
|
197
|
+
const targetKey = modelRoutes[cleanId] || modelRoutes[shortId] || null
|
|
198
|
+
if (!targetKey) return null
|
|
199
|
+
|
|
200
|
+
const targetAcc = safeAccounts.find((a) => matchesAccount(a, targetKey))
|
|
201
|
+
if (targetAcc && !isAccountBlocked(targetAcc)) {
|
|
202
|
+
return targetAcc
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId = '', accountMode = 'least_used', model = '', modelRoutes = {}) {
|
|
192
208
|
const safeAccounts = Array.isArray(accounts) ? accounts.filter(a => a && a.present !== false) : []
|
|
193
209
|
if (safeAccounts.length === 0) {
|
|
194
210
|
const fallback = Array.isArray(accounts) && accounts.length > 0 ? accounts[0] : null
|
|
@@ -209,7 +225,12 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
209
225
|
|
|
210
226
|
const pinnedAvailable = pinnedAccount && !isAccountBlocked(pinnedAccount) ? pinnedAccount : null
|
|
211
227
|
|
|
228
|
+
const routedAccount = resolveModelRouteAccount(model, modelRoutes, safeAccounts)
|
|
229
|
+
|
|
212
230
|
const pickCandidate = () => {
|
|
231
|
+
if (routedAccount) {
|
|
232
|
+
return routedAccount
|
|
233
|
+
}
|
|
213
234
|
if (accountMode === 'round_robin') {
|
|
214
235
|
return getNextRoundRobinAccount(safeAccounts) || safeAccounts[0]
|
|
215
236
|
}
|
|
@@ -222,10 +243,13 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
222
243
|
return { account: pinnedAvailable, isReassigned: false, reason: 'pinned_active' }
|
|
223
244
|
}
|
|
224
245
|
const chosen = pickCandidate()
|
|
246
|
+
const noSessReason = routedAccount
|
|
247
|
+
? 'model_route'
|
|
248
|
+
: (pinnedAccount ? 'pinned_in_cooldown_fallback' : (accountMode === 'round_robin' ? 'round_robin_ad_hoc' : 'no_session_id'))
|
|
225
249
|
return {
|
|
226
250
|
account: chosen,
|
|
227
251
|
isReassigned: false,
|
|
228
|
-
reason:
|
|
252
|
+
reason: noSessReason
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
255
|
|
|
@@ -260,6 +284,21 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
260
284
|
}
|
|
261
285
|
}
|
|
262
286
|
|
|
287
|
+
// If model route is specified and available, enforce routed account:
|
|
288
|
+
if (routedAccount) {
|
|
289
|
+
const reassigned = Boolean(existing && !matchesAccount(routedAccount, existing.accountId))
|
|
290
|
+
sessions.set(sessionId, {
|
|
291
|
+
accountId: routedAccount.apiKeyEnv || routedAccount.id,
|
|
292
|
+
assignedAt: existing ? existing.assignedAt : now,
|
|
293
|
+
lastUsedAt: now
|
|
294
|
+
})
|
|
295
|
+
return {
|
|
296
|
+
account: routedAccount,
|
|
297
|
+
isReassigned: reassigned,
|
|
298
|
+
reason: 'model_route'
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
263
302
|
// If in auto mode or pinned account is unavailable:
|
|
264
303
|
if (existing) {
|
|
265
304
|
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.23",
|
|
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",
|