@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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.22] - 2026-10-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **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.
|
|
12
|
+
- **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`.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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`.
|
|
15
|
+
|
|
16
|
+
## [0.5.21] - 2026-10-12
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Configurable Model Max Output Tokens (maxTokens) & Form Controls (Issue #216, GH #13)**: Exposed editable `maxTokens` (Max Output Tokens) in the Models settings UI alongside context window, complete with preset Default/Original buttons, input controls, and badge status displays. Added context length and max output token configuration fields to the Custom Models form.
|
|
20
|
+
- **Wildcard Override Defaults and Apply-to-All Action (Issue #216, GH #13)**: Implemented wildcard model context override (`modelId: '*'`) in `getAllModels` and an "All" action in the settings UI calling `/dsh-clinebot/models/context` (`mode: 'apply-all'`) to batch-apply preferred context window and max output tokens across all models.
|
|
21
|
+
- **Authentic Vendor Model Limits (Issue #216, GH #13)**: Added accurate `originalMaxTokens` specifications across models (Qwen 131072, GLM 128000, MiniMax 128000, Kimi 65536, DeepSeek-V4.1 65536, MiMo 16384) and updated original context windows for DeepSeek-V4.1 and GLM-5.3.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Preservation of Manual Provider Token Limits Across Sync (Issue #216, GH #13)**: `buildPiAiProvider` and `upsertPiAiProvider` now query existing provider models from `llm-pi-ai.providers.clinebot.models` and preserve user edits to `contextWindow` and `maxTokens` during dynamic catalog synchronization when not explicitly overridden by plugin configuration.
|
|
25
|
+
- **Context Override Detection for Output Limits (Issue #216, GH #13)**: Updated `isContextOverridden` calculation to consider custom `maxTokens` deviations (`activeMaxTokens !== defaultMaxTokens`), properly reflecting override badges in the UI.
|
|
26
|
+
|
|
8
27
|
## [0.5.20] - 2026-10-11
|
|
9
28
|
|
|
10
29
|
### Fixed
|
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,23 +147,34 @@ 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',
|
|
148
156
|
'models.reset_contexts_all': '↺ Reset Contexts',
|
|
149
157
|
'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
|
|
150
|
-
'models.ctx_default': 'Default ({ctx})',
|
|
151
|
-
'models.ctx_original': 'Original ({ctx} · {provider})',
|
|
152
|
-
'models.ctx_edit': 'Edit
|
|
158
|
+
'models.ctx_default': 'Default ({ctx} / {max})',
|
|
159
|
+
'models.ctx_original': 'Original ({ctx} / {max} · {provider})',
|
|
160
|
+
'models.ctx_edit': 'Edit Limits',
|
|
161
|
+
'models.ctx_window_label': 'Context (e.g. 200000)',
|
|
162
|
+
'models.ctx_max_tokens_label': 'Max Out (e.g. 16384)',
|
|
163
|
+
'models.ctx_apply_all': 'All',
|
|
164
|
+
'models.ctx_apply_all_title': 'Apply these limits across all models',
|
|
153
165
|
'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
|
|
154
166
|
'models.ctx_save': 'Save',
|
|
155
167
|
'models.ctx_cancel': 'Cancel',
|
|
168
|
+
'models.custom_ctx_label': 'Context',
|
|
169
|
+
'models.custom_max_label': 'Max Out',
|
|
156
170
|
'models.ctx_tag_original': 'Original',
|
|
157
171
|
'models.ctx_tag_custom': 'Custom',
|
|
158
172
|
'stats.title': '📈 Current DSH Session Telemetry',
|
|
159
173
|
'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
|
|
160
174
|
'stats.requests': 'Successful / Total Requests',
|
|
161
175
|
'stats.tokens': 'Estimated Session Tokens',
|
|
176
|
+
'stats.reasoning_tokens': 'Reasoning Tokens (Thinking)',
|
|
177
|
+
'stats.export_csv': 'Export CSV',
|
|
162
178
|
'stats.latency': 'Last Request Latency',
|
|
163
179
|
'stats.last_req': 'Last Request Time',
|
|
164
180
|
'models.custom_add_title': 'Custom Models',
|
|
@@ -243,6 +259,11 @@ const en = {
|
|
|
243
259
|
'accounts.mode_label': '调度策略',
|
|
244
260
|
'accounts.mode_least_used': '低占用优先 (Least Used)',
|
|
245
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': '添加路由',
|
|
246
267
|
'quota.title': '📊 ClinePass 用量配额与限额监控',
|
|
247
268
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
248
269
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
@@ -299,23 +320,34 @@ const en = {
|
|
|
299
320
|
'models.th_id': '模型标识符 (ID)',
|
|
300
321
|
'models.th_ctx': '上下文窗口',
|
|
301
322
|
'models.th_caps': '特性能力',
|
|
323
|
+
'models.th_route': '指定路由账号',
|
|
324
|
+
'models.route_auto': '自动 / 默认池',
|
|
325
|
+
'models.btn_probe': '测试延迟',
|
|
302
326
|
'models.star': '推荐',
|
|
303
327
|
'models.auto_new': '新上线',
|
|
304
328
|
'models.apply_original_all': '⚡ 原厂规格',
|
|
305
329
|
'models.reset_contexts_all': '↺ 重置上下文',
|
|
306
330
|
'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
|
|
307
|
-
'models.ctx_default': '默认 ({ctx})',
|
|
308
|
-
'models.ctx_original': '原厂 ({ctx} · {provider})',
|
|
309
|
-
'models.ctx_edit': '
|
|
331
|
+
'models.ctx_default': '默认 ({ctx} / {max})',
|
|
332
|
+
'models.ctx_original': '原厂 ({ctx} / {max} · {provider})',
|
|
333
|
+
'models.ctx_edit': '编辑限制',
|
|
334
|
+
'models.ctx_window_label': '上下文 (如 200000)',
|
|
335
|
+
'models.ctx_max_tokens_label': '最大输出 (如 16384)',
|
|
336
|
+
'models.ctx_apply_all': '全部',
|
|
337
|
+
'models.ctx_apply_all_title': '将此限制批量应用到所有模型',
|
|
310
338
|
'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
|
|
311
339
|
'models.ctx_save': '保存',
|
|
312
340
|
'models.ctx_cancel': '取消',
|
|
341
|
+
'models.custom_ctx_label': '上下文',
|
|
342
|
+
'models.custom_max_label': '最大输出',
|
|
313
343
|
'models.ctx_tag_original': '原厂',
|
|
314
344
|
'models.ctx_tag_custom': '自定义',
|
|
315
345
|
'stats.title': '📈 当前 DSH 会话请求遥测',
|
|
316
346
|
'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
|
|
317
347
|
'stats.requests': '成功 / 总请求次数',
|
|
318
348
|
'stats.tokens': '会话累计 Token 估算',
|
|
349
|
+
'stats.reasoning_tokens': '深度思考 Token 消耗',
|
|
350
|
+
'stats.export_csv': '导出 CSV',
|
|
319
351
|
'stats.latency': '最近一次请求延迟',
|
|
320
352
|
'stats.last_req': '最近请求时间',
|
|
321
353
|
'models.custom_add_title': '自定义模型',
|
|
@@ -804,7 +836,18 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
804
836
|
}
|
|
805
837
|
|
|
806
838
|
// --- src/client/components/accounts-section.js ---
|
|
807
|
-
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
|
+
}) {
|
|
808
851
|
if (!status.accounts || !status.accounts.length) return null
|
|
809
852
|
|
|
810
853
|
const [showAdd, setShowAdd] = React.useState(false)
|
|
@@ -813,6 +856,8 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
813
856
|
const [newKey, setNewKey] = React.useState('')
|
|
814
857
|
const [showKey, setShowKey] = React.useState(false)
|
|
815
858
|
const [deleteSecret, setDeleteSecret] = React.useState(true)
|
|
859
|
+
const [routeModel, setRouteModel] = React.useState('')
|
|
860
|
+
const [routeAccount, setRouteAccount] = React.useState('')
|
|
816
861
|
|
|
817
862
|
function handleOpenAdd() {
|
|
818
863
|
let max = 1
|
|
@@ -1114,7 +1159,112 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
1114
1159
|
reason: status.lastRotation.reason,
|
|
1115
1160
|
})
|
|
1116
1161
|
)
|
|
1117
|
-
: 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
|
+
)
|
|
1118
1268
|
)
|
|
1119
1269
|
}
|
|
1120
1270
|
|
|
@@ -1219,17 +1369,21 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
1219
1369
|
function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
1220
1370
|
const [newId, setNewId] = React.useState('')
|
|
1221
1371
|
const [newName, setNewName] = React.useState('')
|
|
1372
|
+
const [newCtx, setNewCtx] = React.useState('')
|
|
1373
|
+
const [newMax, setNewMax] = React.useState('')
|
|
1222
1374
|
const [isExpanded, setIsExpanded] = React.useState(false)
|
|
1223
1375
|
|
|
1224
1376
|
function handleAdd() {
|
|
1225
1377
|
const trimmedId = newId.trim()
|
|
1226
1378
|
const trimmedName = newName.trim() || trimmedId
|
|
1227
1379
|
if (!trimmedId) return
|
|
1380
|
+
const parsedCtx = parseInt(newCtx, 10)
|
|
1381
|
+
const parsedMax = parseInt(newMax, 10)
|
|
1228
1382
|
const nextList = [...customModels.filter((m) => m.id !== trimmedId), {
|
|
1229
1383
|
id: trimmedId,
|
|
1230
1384
|
name: trimmedName,
|
|
1231
|
-
contextLength: 128000,
|
|
1232
|
-
maxTokens: 8192,
|
|
1385
|
+
contextLength: parsedCtx > 0 ? parsedCtx : 128000,
|
|
1386
|
+
maxTokens: parsedMax > 0 ? parsedMax : 8192,
|
|
1233
1387
|
category: 'general',
|
|
1234
1388
|
isCustom: true,
|
|
1235
1389
|
}]
|
|
@@ -1238,6 +1392,8 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1238
1392
|
}
|
|
1239
1393
|
setNewId('')
|
|
1240
1394
|
setNewName('')
|
|
1395
|
+
setNewCtx('')
|
|
1396
|
+
setNewMax('')
|
|
1241
1397
|
}
|
|
1242
1398
|
|
|
1243
1399
|
function handleDelete(id) {
|
|
@@ -1288,7 +1444,7 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1288
1444
|
React.createElement('input', {
|
|
1289
1445
|
type: 'text',
|
|
1290
1446
|
className: 'cb-input',
|
|
1291
|
-
style: { flex: '
|
|
1447
|
+
style: { flex: '2 1 140px' },
|
|
1292
1448
|
placeholder: t('models.custom_id'),
|
|
1293
1449
|
value: newId,
|
|
1294
1450
|
disabled: !!busy,
|
|
@@ -1297,12 +1453,32 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1297
1453
|
React.createElement('input', {
|
|
1298
1454
|
type: 'text',
|
|
1299
1455
|
className: 'cb-input',
|
|
1300
|
-
style: { flex: '
|
|
1456
|
+
style: { flex: '2 1 140px' },
|
|
1301
1457
|
placeholder: t('models.custom_name'),
|
|
1302
1458
|
value: newName,
|
|
1303
1459
|
disabled: !!busy,
|
|
1304
1460
|
onChange: (e) => setNewName(e.target.value),
|
|
1305
1461
|
}),
|
|
1462
|
+
React.createElement('input', {
|
|
1463
|
+
type: 'number',
|
|
1464
|
+
className: 'cb-input',
|
|
1465
|
+
style: { flex: '1 1 90px' },
|
|
1466
|
+
placeholder: t('models.custom_ctx_label'),
|
|
1467
|
+
title: t('models.custom_ctx_label'),
|
|
1468
|
+
value: newCtx,
|
|
1469
|
+
disabled: !!busy,
|
|
1470
|
+
onChange: (e) => setNewCtx(e.target.value),
|
|
1471
|
+
}),
|
|
1472
|
+
React.createElement('input', {
|
|
1473
|
+
type: 'number',
|
|
1474
|
+
className: 'cb-input',
|
|
1475
|
+
style: { flex: '1 1 80px' },
|
|
1476
|
+
placeholder: t('models.custom_max_label'),
|
|
1477
|
+
title: t('models.custom_max_label'),
|
|
1478
|
+
value: newMax,
|
|
1479
|
+
disabled: !!busy,
|
|
1480
|
+
onChange: (e) => setNewMax(e.target.value),
|
|
1481
|
+
}),
|
|
1306
1482
|
React.createElement(
|
|
1307
1483
|
'button',
|
|
1308
1484
|
{
|
|
@@ -1338,7 +1514,8 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1338
1514
|
'div',
|
|
1339
1515
|
null,
|
|
1340
1516
|
React.createElement('span', { style: { fontWeight: 500 } }, cm.name || cm.id),
|
|
1341
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px' } }, `(${cm.id})`)
|
|
1517
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px' } }, `(${cm.id})`),
|
|
1518
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px', fontSize: '11px' } }, `[${cm.contextLength || 128000} / ${cm.maxTokens || 8192}]`)
|
|
1342
1519
|
),
|
|
1343
1520
|
React.createElement(
|
|
1344
1521
|
'button',
|
|
@@ -1385,6 +1562,7 @@ function ModelsSection({
|
|
|
1385
1562
|
const [viewFilter, setViewFilter] = React.useState('all')
|
|
1386
1563
|
const [editingModelId, setEditingModelId] = React.useState(null)
|
|
1387
1564
|
const [customCtxVal, setCustomCtxVal] = React.useState('')
|
|
1565
|
+
const [customMaxVal, setCustomMaxVal] = React.useState('')
|
|
1388
1566
|
|
|
1389
1567
|
function formatCtx(num) {
|
|
1390
1568
|
num = Number(num) || 200000
|
|
@@ -1580,9 +1758,11 @@ function ModelsSection({
|
|
|
1580
1758
|
const isEnabled = !disabledSet.has(m.id)
|
|
1581
1759
|
const defaultCtx = m.defaultContextLength || m.clineContextLength || 200000
|
|
1582
1760
|
const origCtx = m.originalContextLength || 200000
|
|
1761
|
+
const defaultMax = m.defaultMaxTokens || 8192
|
|
1762
|
+
const origMax = m.originalMaxTokens || defaultMax
|
|
1583
1763
|
const origProv = m.originalProvider || 'Original'
|
|
1584
1764
|
const isOverridden = Boolean(m.isContextOverridden)
|
|
1585
|
-
const isOriginal = isOverridden && (m.contextLength === origCtx)
|
|
1765
|
+
const isOriginal = isOverridden && (m.contextLength === origCtx && (m.maxTokens || defaultMax) === origMax)
|
|
1586
1766
|
|
|
1587
1767
|
return React.createElement(
|
|
1588
1768
|
'tr',
|
|
@@ -1641,7 +1821,7 @@ function ModelsSection({
|
|
|
1641
1821
|
setEditingModelId(null)
|
|
1642
1822
|
},
|
|
1643
1823
|
},
|
|
1644
|
-
t('models.ctx_default', { ctx: formatCtx(defaultCtx) })
|
|
1824
|
+
t('models.ctx_default', { ctx: formatCtx(defaultCtx), max: formatCtx(defaultMax) })
|
|
1645
1825
|
),
|
|
1646
1826
|
React.createElement(
|
|
1647
1827
|
'button',
|
|
@@ -1651,41 +1831,76 @@ function ModelsSection({
|
|
|
1651
1831
|
style: { padding: '2px 6px', fontSize: '11px' },
|
|
1652
1832
|
onClick: () => {
|
|
1653
1833
|
if (typeof handleUpdateModelContext === 'function') {
|
|
1654
|
-
handleUpdateModelContext({ modelId: m.id, contextLength: origCtx })
|
|
1834
|
+
handleUpdateModelContext({ modelId: m.id, contextLength: origCtx, maxTokens: origMax })
|
|
1655
1835
|
}
|
|
1656
1836
|
setEditingModelId(null)
|
|
1657
1837
|
},
|
|
1658
1838
|
},
|
|
1659
|
-
t('models.ctx_original', { ctx: formatCtx(origCtx), provider: origProv })
|
|
1839
|
+
t('models.ctx_original', { ctx: formatCtx(origCtx), max: formatCtx(origMax), provider: origProv })
|
|
1660
1840
|
)
|
|
1661
1841
|
),
|
|
1662
1842
|
React.createElement(
|
|
1663
1843
|
'div',
|
|
1664
|
-
{ style: { display: 'flex', gap: '4px', alignItems: 'center' } },
|
|
1844
|
+
{ style: { display: 'flex', gap: '4px', alignItems: 'center', flexWrap: 'wrap' } },
|
|
1665
1845
|
React.createElement('input', {
|
|
1666
1846
|
type: 'number',
|
|
1667
1847
|
className: 'cb-input',
|
|
1668
|
-
style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '
|
|
1669
|
-
placeholder: t('models.
|
|
1848
|
+
style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '90px' },
|
|
1849
|
+
placeholder: t('models.ctx_window_label'),
|
|
1850
|
+
title: t('models.ctx_window_label'),
|
|
1670
1851
|
value: customCtxVal,
|
|
1671
1852
|
onChange: (e) => setCustomCtxVal(e.target.value),
|
|
1672
1853
|
}),
|
|
1854
|
+
React.createElement('input', {
|
|
1855
|
+
type: 'number',
|
|
1856
|
+
className: 'cb-input',
|
|
1857
|
+
style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '75px' },
|
|
1858
|
+
placeholder: t('models.ctx_max_tokens_label'),
|
|
1859
|
+
title: t('models.ctx_max_tokens_label'),
|
|
1860
|
+
value: customMaxVal,
|
|
1861
|
+
onChange: (e) => setCustomMaxVal(e.target.value),
|
|
1862
|
+
}),
|
|
1673
1863
|
React.createElement(
|
|
1674
1864
|
'button',
|
|
1675
1865
|
{
|
|
1676
1866
|
type: 'button',
|
|
1677
1867
|
className: 'cb-btn cb-btn-primary',
|
|
1678
|
-
style: { padding: '2px
|
|
1868
|
+
style: { padding: '2px 6px', fontSize: '11px' },
|
|
1679
1869
|
onClick: () => {
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1870
|
+
const cVal = parseInt(customCtxVal, 10)
|
|
1871
|
+
const mVal = parseInt(customMaxVal, 10)
|
|
1872
|
+
if (typeof handleUpdateModelContext === 'function') {
|
|
1873
|
+
const payload = { modelId: m.id }
|
|
1874
|
+
if (cVal > 0) payload.contextLength = cVal
|
|
1875
|
+
if (mVal > 0) payload.maxTokens = mVal
|
|
1876
|
+
handleUpdateModelContext(payload)
|
|
1683
1877
|
}
|
|
1684
1878
|
setEditingModelId(null)
|
|
1685
1879
|
},
|
|
1686
1880
|
},
|
|
1687
1881
|
t('models.ctx_save')
|
|
1688
1882
|
),
|
|
1883
|
+
React.createElement(
|
|
1884
|
+
'button',
|
|
1885
|
+
{
|
|
1886
|
+
type: 'button',
|
|
1887
|
+
className: 'cb-btn',
|
|
1888
|
+
title: t('models.ctx_apply_all_title'),
|
|
1889
|
+
style: { padding: '2px 5px', fontSize: '11px' },
|
|
1890
|
+
onClick: () => {
|
|
1891
|
+
const cVal = parseInt(customCtxVal, 10)
|
|
1892
|
+
const mVal = parseInt(customMaxVal, 10)
|
|
1893
|
+
if (typeof handleUpdateModelContext === 'function') {
|
|
1894
|
+
const payload = { mode: 'apply-all' }
|
|
1895
|
+
if (cVal > 0) payload.contextLength = cVal
|
|
1896
|
+
if (mVal > 0) payload.maxTokens = mVal
|
|
1897
|
+
handleUpdateModelContext(payload)
|
|
1898
|
+
}
|
|
1899
|
+
setEditingModelId(null)
|
|
1900
|
+
},
|
|
1901
|
+
},
|
|
1902
|
+
t('models.ctx_apply_all')
|
|
1903
|
+
),
|
|
1689
1904
|
React.createElement(
|
|
1690
1905
|
'button',
|
|
1691
1906
|
{
|
|
@@ -1707,10 +1922,11 @@ function ModelsSection({
|
|
|
1707
1922
|
style: { fontWeight: '600', cursor: 'pointer' },
|
|
1708
1923
|
onClick: () => {
|
|
1709
1924
|
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1925
|
+
setCustomMaxVal(String(m.maxTokens || defaultMax))
|
|
1710
1926
|
setEditingModelId(m.id)
|
|
1711
1927
|
},
|
|
1712
1928
|
},
|
|
1713
|
-
formatCtx(m.contextLength || defaultCtx)
|
|
1929
|
+
`${formatCtx(m.contextLength || defaultCtx)} / ${formatCtx(m.maxTokens || defaultMax)}`
|
|
1714
1930
|
),
|
|
1715
1931
|
isOverridden
|
|
1716
1932
|
? React.createElement(
|
|
@@ -1720,6 +1936,7 @@ function ModelsSection({
|
|
|
1720
1936
|
style: { fontSize: '10px', padding: '1px 6px', cursor: 'pointer' },
|
|
1721
1937
|
onClick: () => {
|
|
1722
1938
|
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1939
|
+
setCustomMaxVal(String(m.maxTokens || defaultMax))
|
|
1723
1940
|
setEditingModelId(m.id)
|
|
1724
1941
|
},
|
|
1725
1942
|
},
|
|
@@ -1735,6 +1952,7 @@ function ModelsSection({
|
|
|
1735
1952
|
title: t('models.ctx_edit'),
|
|
1736
1953
|
onClick: () => {
|
|
1737
1954
|
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1955
|
+
setCustomMaxVal(String(m.maxTokens || defaultMax))
|
|
1738
1956
|
setEditingModelId(m.id)
|
|
1739
1957
|
},
|
|
1740
1958
|
},
|
|
@@ -1835,7 +2053,17 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1835
2053
|
React.createElement('div', { className: 'cb-section-title' }, t('stats.title')),
|
|
1836
2054
|
React.createElement(
|
|
1837
2055
|
'div',
|
|
1838
|
-
|
|
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
|
+
),
|
|
1839
2067
|
confirmReset
|
|
1840
2068
|
? React.createElement(
|
|
1841
2069
|
React.Fragment,
|
|
@@ -1891,8 +2119,18 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1891
2119
|
React.createElement(
|
|
1892
2120
|
'div',
|
|
1893
2121
|
{ className: 'cb-stat-box' },
|
|
1894
|
-
React.createElement(
|
|
1895
|
-
|
|
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
|
+
)
|
|
1896
2134
|
),
|
|
1897
2135
|
React.createElement(
|
|
1898
2136
|
'div',
|
|
@@ -1930,7 +2168,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1930
2168
|
},
|
|
1931
2169
|
},
|
|
1932
2170
|
React.createElement('span', { style: { fontFamily: 'monospace' } }, modId),
|
|
1933
|
-
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)` : ''}`)
|
|
1934
2172
|
)
|
|
1935
2173
|
)
|
|
1936
2174
|
)
|
|
@@ -2498,6 +2736,8 @@ function SettingsPage(props) {
|
|
|
2498
2736
|
status, busy, handlePinAccount, handleAddAccount, handleDeleteAccount,
|
|
2499
2737
|
accountMode: draft?.accountMode || status?.config?.accountMode || 'least_used',
|
|
2500
2738
|
handleUpdateAccountMode: (mode) => handleConfigPatch({ accountMode: mode }),
|
|
2739
|
+
modelRoutes: draft?.modelRoutes || status?.config?.modelRoutes || {},
|
|
2740
|
+
onUpdateModelRoutes: (routes) => handleConfigPatch({ modelRoutes: routes }),
|
|
2501
2741
|
t,
|
|
2502
2742
|
}),
|
|
2503
2743
|
|
package/lib/cline-client.js
CHANGED
|
@@ -490,10 +490,15 @@ export function buildPiAiProvider({
|
|
|
490
490
|
modelContextOverrides = [],
|
|
491
491
|
models = [],
|
|
492
492
|
customModels = [],
|
|
493
|
+
existingModels = [],
|
|
493
494
|
displayName = PROVIDER_DISPLAY_NAME,
|
|
494
495
|
} = {}) {
|
|
495
496
|
const allAvailable = getAllModels(dynamicModels, modelContextOverrides, customModels)
|
|
496
497
|
const customModelsMap = new Map((Array.isArray(customModels) ? customModels : []).filter(Boolean).map(cm => [cm.id, cm]))
|
|
498
|
+
const existingMap = new Map((Array.isArray(existingModels) ? existingModels : []).filter(Boolean).map(m => [m.id, m]))
|
|
499
|
+
const explicitOverridesSet = new Set((Array.isArray(modelContextOverrides) ? modelContextOverrides : []).map(o => o?.modelId))
|
|
500
|
+
const hasWildcardOverride = explicitOverridesSet.has('*')
|
|
501
|
+
|
|
497
502
|
const modelList = (Array.isArray(models) && models.length ? models : allAvailable).map((m) => {
|
|
498
503
|
let item
|
|
499
504
|
if (typeof m === 'string') {
|
|
@@ -504,12 +509,26 @@ export function buildPiAiProvider({
|
|
|
504
509
|
item = m
|
|
505
510
|
}
|
|
506
511
|
const hasImage = item.input?.includes('image') || item.input?.includes('vision')
|
|
512
|
+
|
|
513
|
+
// Preserve manual provider edits if user configured contextWindow or maxTokens in llm-pi-ai without explicit override in clinebot
|
|
514
|
+
const existingEntry = existingMap.get(item.id)
|
|
515
|
+
let ctxWin = Number(item.contextLength || item.contextWindow) || 200000
|
|
516
|
+
let maxTok = Number(item.maxTokens) || 8192
|
|
517
|
+
if (existingEntry && !explicitOverridesSet.has(item.id) && !hasWildcardOverride) {
|
|
518
|
+
if (typeof existingEntry.contextWindow === 'number' && existingEntry.contextWindow > 0) {
|
|
519
|
+
ctxWin = existingEntry.contextWindow
|
|
520
|
+
}
|
|
521
|
+
if (typeof existingEntry.maxTokens === 'number' && existingEntry.maxTokens > 0) {
|
|
522
|
+
maxTok = existingEntry.maxTokens
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
507
526
|
const res = {
|
|
508
527
|
id: item.id,
|
|
509
528
|
name: item.name || item.id,
|
|
510
529
|
description: formatModelDescription(item),
|
|
511
|
-
contextWindow:
|
|
512
|
-
maxTokens:
|
|
530
|
+
contextWindow: ctxWin,
|
|
531
|
+
maxTokens: maxTok,
|
|
513
532
|
input: hasImage ? ['text', 'image'] : ['text'],
|
|
514
533
|
provider: PROVIDER_ID,
|
|
515
534
|
}
|
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'),
|