@goodandready/dsh-clinebot 0.3.16 → 0.3.18
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 +28 -0
- package/README.md +2 -2
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/access.js +2 -1
- package/lib/client.js +12 -8
- package/lib/cline-client.js +46 -14
- package/lib/config.js +3 -2
- package/lib/index.js +8 -1
- package/lib/models.js +22 -11
- package/lib/provider-sync.js +4 -4
- package/lib/routes/accounts.js +4 -10
- package/lib/routes/auth.js +3 -7
- package/lib/routes/models.js +3 -7
- package/lib/routes/settings.js +2 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.3.18] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Reasoning Effort (思考强度) Selection (#59)**: Enabled reasoning effort selection (`Off` / `Low` / `Medium` / `High` / `Max`) across DSH composer model picker and `dsh-effort-slider` for all supported reasoning models (Qwen 3.7 Max/Plus, MiMo 2.5/Pro, GLM 5.2, Kimi K3, DeepSeek V4 Pro/Flash, MiniMax M3).
|
|
12
|
+
- **Protocol Compliance**: Injected `off: null` into wire dictionary and declared `compat: { supportsReasoningEffort: true }` at provider and model levels in accordance with `@earendil-works/pi-ai` protocol specifications.
|
|
13
|
+
- **Dynamic Plan Model Reasoning Heuristics**: Added automatic detection of reasoning capabilities for dynamically discovered models matching `/qwen3\.7|mimo|glm-5|kimi-k3|deepseek-v4|minimax-m3/i`.
|
|
14
|
+
- **Reasoning Tooltip Localization**: Added localized explanatory tooltips (`models.reasoning_tooltip`) across English, Russian, and Chinese in the Settings UI.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Settings UI Reasoning Badge**: Fixed reasoning badge check in `models-section.js` to correctly detect object dictionary configurations instead of relying solely on array length.
|
|
18
|
+
- **Context Window Capacity Clarification**: Updated catalog documentation and descriptions to clarify that the uniform 200k tokens context parameter reflects the official ClinePass upstream completion proxy gateway limit.
|
|
19
|
+
|
|
20
|
+
## [0.3.17] - 2026-09-21
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Cache Resilience (#50)**: Fixed permanent `isRevalidating` flag lock in `usageCache` and `probeCache` upon background SWR revalidation failures. The flag is now guaranteed to reset in `finally` and `catch` blocks.
|
|
24
|
+
- **Immediate Abort Handling (#52)**: Eliminated artificial 250ms delay and futile retry upon `AbortError` or `signal.aborted` in `fetchUsageLimits`.
|
|
25
|
+
- **Lifecycle Timer Cleanup (#53)**: Wrapped `autoDiscover` timer in `ctx.effect` with cleanup handler to avoid orphaned background callbacks upon plugin context unload.
|
|
26
|
+
- **UI Version Hardcode Removal (#56)**: Replaced hardcoded legacy `'0.3.12'` string in initial React state with dynamic retrieval from `/update` endpoint and conditional badge rendering.
|
|
27
|
+
|
|
28
|
+
### Performance
|
|
29
|
+
- **Single Account Pool Resolution (#51)**: Eliminated duplicate sequential IPC queries to DSH Credentials by passing the pre-resolved account pool into `resolveActiveAccountKey` within `buildStatus`.
|
|
30
|
+
- **Bounded Cache with Auto-Eviction (#57)**: Enforced `MAX_CACHE_ENTRIES = 50` and automatic eviction of expired entries in `usageCache` and `probeCache` to prevent long-term memory leaks.
|
|
31
|
+
|
|
32
|
+
### Refactored
|
|
33
|
+
- **Active Route Guard (#54)**: Converted `assertTrustedSettingsRequest` into an active route guard writing `403 Forbidden` and applied it across all write endpoints in `lib/routes/`.
|
|
34
|
+
- **Dead Export Wiring (#55)**: Wired `getDefaultModelIds` helper in `lib/config.js` to compute `allDefaultIds`, clearing preflight dead-export warnings.
|
|
35
|
+
|
|
8
36
|
## [0.3.16] - 2026-09-20
|
|
9
37
|
|
|
10
38
|
### Fixed
|
package/README.md
CHANGED
|
@@ -108,13 +108,13 @@ graph LR
|
|
|
108
108
|
## ✨ Features & Module Breakdown
|
|
109
109
|
|
|
110
110
|
* **`lib/models.js`**:
|
|
111
|
-
Manages the curated catalog (11 built-in models) and
|
|
111
|
+
Manages the curated catalog (11 built-in models) with full reasoning effort mappings (`off: null`, `low`, `medium`, `high`, `max`), upstream 200k context limit declarations, and dynamic subscription plan model parsing (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
112
112
|
* **`lib/cline-client.js`**:
|
|
113
113
|
* `fetchUsageLimits`: queries `GET /users/me/plan/usage-limits`, `GET /users/me/plan`, and `GET /users/me` with in-memory caching.
|
|
114
114
|
* `sessionStats` / `recordSessionRequest`: in-memory telemetry recording requests count, tokens, latency, and timestamps.
|
|
115
115
|
* `saveCredentialKey`: writes credentials directly into `~/.dsh/.credentials.yaml`.
|
|
116
116
|
* `smokeChat`: tests latency via non-streaming ping and updates session metrics.
|
|
117
|
-
* `buildPiAiProvider`: builds the DSH `llm-pi-ai` structure (`api: 'openai-completions'`).
|
|
117
|
+
* `buildPiAiProvider`: builds the DSH `llm-pi-ai` structure (`api: 'openai-completions'`) with full reasoning effort protocol compliance (`compat.supportsReasoningEffort: true`).
|
|
118
118
|
* **`lib/index.js`**:
|
|
119
119
|
Cordis service module managing routes (including `POST /dsh-clinebot/models/sync`), quota warnings threshold evaluation, credentials, and registering the `/cline` slash command.
|
|
120
120
|
* **`lib/client.js`**:
|
package/README.ru.md
CHANGED
|
@@ -107,7 +107,7 @@ graph LR
|
|
|
107
107
|
|
|
108
108
|
## ✨ Структура модулей и возможности
|
|
109
109
|
|
|
110
|
-
* **`lib/models.js`**: каталог встроенных моделей ClinePass (11 моделей) и парсер моделей подписки (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
110
|
+
* **`lib/models.js`**: каталог встроенных моделей ClinePass (11 моделей с полной поддержкой уровней размышлений `Off` / `Low` / `Medium` / `High` / `Max` и фиксацией лимита шлюза в 200k токенов) и парсер моделей подписки (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
111
111
|
* **`lib/cline-client.js`**:
|
|
112
112
|
* `fetchUsageLimits`: параллельный опрос `GET /users/me/plan/usage-limits`, `GET /users/me/plan` и `GET /users/me` с кэшированием в памяти.
|
|
113
113
|
* `sessionStats` / `recordSessionRequest`: счетчики сессии (запросы, токены, задержка, время).
|
package/README.zh.md
CHANGED
|
@@ -107,7 +107,7 @@ graph LR
|
|
|
107
107
|
|
|
108
108
|
## ✨ 核心模块与功能
|
|
109
109
|
|
|
110
|
-
* **`lib/models.js`**:管理 11
|
|
110
|
+
* **`lib/models.js`**:管理 11 款官方精选内置模型(提供完整的 `Off` / `Low` / `Medium` / `High` / `Max` 思考强度映射、上游 200k 上下文容量标注)以及套餐模型动态解析器(`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`)。
|
|
111
111
|
* **`lib/cline-client.js`**:
|
|
112
112
|
* `fetchUsageLimits`:高效并发轮询 `GET /users/me/plan/usage-limits`、`GET /users/me/plan` 与 `GET /users/me` 并进行内存缓存。
|
|
113
113
|
* `sessionStats` / `recordSessionRequest`:内存级会话度量记录器(请求次数、Token 估算、延迟、时间戳)。
|
package/lib/access.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { isTrustedSettingsRequest } from './http.js'
|
|
1
|
+
import { isTrustedSettingsRequest, writeJson } from './http.js'
|
|
2
2
|
|
|
3
3
|
export { isTrustedSettingsRequest }
|
|
4
4
|
|
|
5
5
|
export function assertTrustedSettingsRequest(req, res) {
|
|
6
6
|
if (!isTrustedSettingsRequest(req)) {
|
|
7
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
7
8
|
return false
|
|
8
9
|
}
|
|
9
10
|
return true
|
package/lib/client.js
CHANGED
|
@@ -65,7 +65,8 @@ const en = {
|
|
|
65
65
|
'quota.remaining': 'Remaining: {pct}%',
|
|
66
66
|
'quota.reset_at': 'Reset: {time}',
|
|
67
67
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
68
|
-
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker.',
|
|
68
|
+
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
|
|
69
|
+
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
69
70
|
'models.sync': '🔄 Check Plan Models',
|
|
70
71
|
'models.syncing': 'Checking…',
|
|
71
72
|
'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
|
|
@@ -154,7 +155,8 @@ const en = {
|
|
|
154
155
|
'quota.remaining': '剩余可用:{pct}%',
|
|
155
156
|
'quota.reset_at': '重置时间:{time}',
|
|
156
157
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
157
|
-
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH
|
|
158
|
+
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
159
|
+
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
158
160
|
'models.sync': '🔄 同步订阅模型',
|
|
159
161
|
'models.syncing': '正在同步…',
|
|
160
162
|
'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
|
|
@@ -402,9 +404,11 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
402
404
|
React.createElement(
|
|
403
405
|
'div',
|
|
404
406
|
{ style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' } },
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
updateState.currentVersion
|
|
408
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
|
|
409
|
+
`v${updateState.currentVersion}`
|
|
410
|
+
)
|
|
411
|
+
: null,
|
|
408
412
|
updateState.checking
|
|
409
413
|
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
|
|
410
414
|
t('update.checking')
|
|
@@ -725,8 +729,8 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
|
|
|
725
729
|
m.input?.includes('image') || m.input?.includes('vision')
|
|
726
730
|
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
|
|
727
731
|
: null,
|
|
728
|
-
m.reasoningEfforts
|
|
729
|
-
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, '🧠 Reasoning')
|
|
732
|
+
(Array.isArray(m.reasoningEfforts) ? m.reasoningEfforts.length > 0 : (m.reasoningEfforts && typeof m.reasoningEfforts === 'object' ? Object.keys(m.reasoningEfforts).length > 0 : Boolean(m.reasoning)))
|
|
733
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '4px' }, title: t('models.reasoning_tooltip') }, '🧠 Reasoning')
|
|
730
734
|
: null
|
|
731
735
|
)
|
|
732
736
|
)
|
|
@@ -882,7 +886,7 @@ function SettingsPage(props) {
|
|
|
882
886
|
const [updateState, setUpdateState] = React.useState({
|
|
883
887
|
checking: false,
|
|
884
888
|
updating: false,
|
|
885
|
-
currentVersion: '
|
|
889
|
+
currentVersion: '',
|
|
886
890
|
latestVersion: '',
|
|
887
891
|
updateAvailable: false,
|
|
888
892
|
canAutoUpdate: true,
|
package/lib/cline-client.js
CHANGED
|
@@ -132,9 +132,26 @@ function abortAfter(ms) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// In-memory caches for quota queries and host health probes
|
|
135
|
+
export const MAX_CACHE_ENTRIES = 50
|
|
135
136
|
export const usageCache = new Map()
|
|
136
137
|
export const probeCache = new Map()
|
|
137
138
|
|
|
139
|
+
export function setBoundedCache(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
|
|
140
|
+
if (map.size >= maxEntries) {
|
|
141
|
+
const now = Date.now()
|
|
142
|
+
for (const [k, v] of map.entries()) {
|
|
143
|
+
if (v.expiresAt && v.expiresAt < now) {
|
|
144
|
+
map.delete(k)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (map.size >= maxEntries) {
|
|
148
|
+
const oldestKey = map.keys().next().value
|
|
149
|
+
if (oldestKey) map.delete(oldestKey)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
map.set(key, value)
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
export function clearProbeCache() {
|
|
139
156
|
probeCache.clear()
|
|
140
157
|
}
|
|
@@ -212,8 +229,10 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
212
229
|
keepalive: true,
|
|
213
230
|
})
|
|
214
231
|
} catch (netErr) {
|
|
232
|
+
if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
|
|
215
233
|
// Quick 1-retry fallback on transient network drop
|
|
216
234
|
await new Promise((r) => setTimeout(r, 250))
|
|
235
|
+
if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
|
|
217
236
|
limitsRes = await fetchImpl(`${base}/users/me/plan/usage-limits`, {
|
|
218
237
|
method: 'GET',
|
|
219
238
|
headers,
|
|
@@ -299,7 +318,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
299
318
|
checkedAt: Date.now(),
|
|
300
319
|
}
|
|
301
320
|
|
|
302
|
-
usageCache
|
|
321
|
+
setBoundedCache(usageCache, cacheKey, { data: result, expiresAt: Date.now() + ttlMs, isRevalidating: false })
|
|
303
322
|
return result
|
|
304
323
|
} catch (err) {
|
|
305
324
|
const outcome = {
|
|
@@ -308,10 +327,15 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
308
327
|
checkedAt: Date.now(),
|
|
309
328
|
}
|
|
310
329
|
if (!usageCache.has(cacheKey)) {
|
|
311
|
-
usageCache
|
|
330
|
+
setBoundedCache(usageCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
|
|
331
|
+
} else {
|
|
332
|
+
const existing = usageCache.get(cacheKey)
|
|
333
|
+
if (existing) existing.isRevalidating = false
|
|
312
334
|
}
|
|
313
335
|
return outcome
|
|
314
336
|
} finally {
|
|
337
|
+
const existing = usageCache.get(cacheKey)
|
|
338
|
+
if (existing) existing.isRevalidating = false
|
|
315
339
|
cancel()
|
|
316
340
|
}
|
|
317
341
|
}
|
|
@@ -362,7 +386,7 @@ export async function probeHealth(baseUrl, {
|
|
|
362
386
|
error: reachable ? null : `HTTP status ${res.status}`,
|
|
363
387
|
checkedAt: Date.now(),
|
|
364
388
|
}
|
|
365
|
-
probeCache
|
|
389
|
+
setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + ttlMs, isRevalidating: false })
|
|
366
390
|
return outcome
|
|
367
391
|
} catch (err) {
|
|
368
392
|
const latencyMs = Date.now() - start
|
|
@@ -372,9 +396,16 @@ export async function probeHealth(baseUrl, {
|
|
|
372
396
|
error: String(err?.message || err),
|
|
373
397
|
checkedAt: Date.now(),
|
|
374
398
|
}
|
|
375
|
-
probeCache.
|
|
399
|
+
if (!probeCache.has(cacheKey)) {
|
|
400
|
+
setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
|
|
401
|
+
} else {
|
|
402
|
+
const existing = probeCache.get(cacheKey)
|
|
403
|
+
if (existing) existing.isRevalidating = false
|
|
404
|
+
}
|
|
376
405
|
return outcome
|
|
377
406
|
} finally {
|
|
407
|
+
const existing = probeCache.get(cacheKey)
|
|
408
|
+
if (existing) existing.isRevalidating = false
|
|
378
409
|
cancel()
|
|
379
410
|
}
|
|
380
411
|
}
|
|
@@ -495,24 +526,24 @@ export function buildPiAiProvider({
|
|
|
495
526
|
provider: PROVIDER_ID,
|
|
496
527
|
}
|
|
497
528
|
const validEfforts = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
529
|
+
let efforts = null
|
|
498
530
|
if (Array.isArray(item.reasoningEfforts) && item.reasoningEfforts.length) {
|
|
499
|
-
|
|
531
|
+
efforts = {}
|
|
500
532
|
for (const effort of item.reasoningEfforts) {
|
|
501
533
|
const key = String(effort).trim().toLowerCase()
|
|
502
|
-
if (validEfforts.includes(key))
|
|
503
|
-
efforts[key] = key
|
|
504
|
-
}
|
|
534
|
+
if (validEfforts.includes(key)) efforts[key] = key === 'off' ? null : key
|
|
505
535
|
}
|
|
506
|
-
res.reasoningEfforts = Object.keys(efforts).length ? efforts : false
|
|
507
536
|
} else if (item.reasoningEfforts && typeof item.reasoningEfforts === 'object' && !Array.isArray(item.reasoningEfforts)) {
|
|
508
|
-
|
|
537
|
+
efforts = {}
|
|
509
538
|
for (const [k, v] of Object.entries(item.reasoningEfforts)) {
|
|
510
539
|
const key = String(k).trim().toLowerCase()
|
|
511
|
-
if (validEfforts.includes(key))
|
|
512
|
-
efforts[key] = typeof v === 'string' && v.trim() ? v.trim() : key
|
|
513
|
-
}
|
|
540
|
+
if (validEfforts.includes(key)) efforts[key] = key === 'off' ? (v === undefined ? null : v) : (typeof v === 'string' && v.trim() ? v.trim() : key)
|
|
514
541
|
}
|
|
515
|
-
|
|
542
|
+
}
|
|
543
|
+
if (efforts && Object.keys(efforts).length) {
|
|
544
|
+
if (!('off' in efforts)) efforts.off = null
|
|
545
|
+
res.reasoningEfforts = efforts
|
|
546
|
+
res.compat = { supportsReasoningEffort: true }
|
|
516
547
|
} else {
|
|
517
548
|
res.reasoningEfforts = false
|
|
518
549
|
}
|
|
@@ -524,6 +555,7 @@ export function buildPiAiProvider({
|
|
|
524
555
|
api: 'openai-completions',
|
|
525
556
|
baseURL: normalizeBaseUrl(baseUrl),
|
|
526
557
|
apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
558
|
+
compat: { supportsReasoningEffort: true },
|
|
527
559
|
models: modelList,
|
|
528
560
|
}
|
|
529
561
|
}
|
package/lib/config.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_MODEL_ID,
|
|
11
11
|
getAllModels,
|
|
12
|
+
getDefaultModelIds,
|
|
12
13
|
getActiveModelIds,
|
|
13
14
|
} from './models.js'
|
|
14
15
|
|
|
@@ -37,6 +38,7 @@ export const Config = z.object({
|
|
|
37
38
|
input: z.array(z.string()).default(['text']),
|
|
38
39
|
category: z.string().default('general'),
|
|
39
40
|
isCustom: z.boolean().default(false),
|
|
41
|
+
reasoningEfforts: z.any().default(undefined),
|
|
40
42
|
})).default([])
|
|
41
43
|
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
42
44
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
@@ -56,8 +58,7 @@ export const Config = z.object({
|
|
|
56
58
|
|
|
57
59
|
export function publicConfig(cfg) {
|
|
58
60
|
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
59
|
-
const
|
|
60
|
-
const allDefaultIds = allModels.map((m) => m.id)
|
|
61
|
+
const allDefaultIds = getDefaultModelIds(dynamic)
|
|
61
62
|
|
|
62
63
|
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
63
64
|
if (!Array.isArray(cfg?.disabledModels) || (cfg.disabledModels.length === 0 && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0)) {
|
package/lib/index.js
CHANGED
|
@@ -83,7 +83,14 @@ export function apply(ctx, config) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
syncProviderState(live())
|
|
86
|
-
|
|
86
|
+
if (typeof ctx.effect === 'function') {
|
|
87
|
+
ctx.effect(() => {
|
|
88
|
+
const timer = setTimeout(triggerAutoDiscover, 500)
|
|
89
|
+
return () => clearTimeout(timer)
|
|
90
|
+
}, 'dsh-clinebot: auto-discover')
|
|
91
|
+
} else {
|
|
92
|
+
setTimeout(triggerAutoDiscover, 500)
|
|
93
|
+
}
|
|
87
94
|
|
|
88
95
|
if (ctx.webServer?.register) {
|
|
89
96
|
const unregisterUpdater = registerPluginUpdater(ctx, {
|
package/lib/models.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Curated and extensible models catalog for ClineBot / ClinePass.
|
|
3
3
|
*
|
|
4
4
|
* Notice: GET /v1/models returns 404 on api.cline.bot, so DSH cannot rely on
|
|
5
|
-
* dynamic OpenAI model discovery.
|
|
6
|
-
*
|
|
5
|
+
* dynamic OpenAI model discovery. Furthermore, ClinePass upstream proxy enforces
|
|
6
|
+
* a uniform 200,000 token context window cap across subscription completion routes,
|
|
7
|
+
* and /users/me/plan returns only feature strings without per-model context specs.
|
|
8
|
+
* This catalogue provides the official ClinePass models (200k gateway capacity) plus
|
|
9
|
+
* support for user-defined custom models with arbitrary context length.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
export const PROVIDER_ID = 'clinebot'
|
|
@@ -45,6 +48,7 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
45
48
|
category: 'general',
|
|
46
49
|
recommended: false,
|
|
47
50
|
isCustom: false,
|
|
51
|
+
reasoningEfforts: ['low', 'medium', 'high'],
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
id: 'cline-pass/kimi-k3',
|
|
@@ -56,7 +60,7 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
56
60
|
category: 'reasoning',
|
|
57
61
|
recommended: true,
|
|
58
62
|
isCustom: false,
|
|
59
|
-
reasoningEfforts: ['low', 'high', 'max'],
|
|
63
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
60
64
|
},
|
|
61
65
|
{
|
|
62
66
|
id: 'cline-pass/kimi-k2.7-code',
|
|
@@ -83,24 +87,26 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
83
87
|
{
|
|
84
88
|
id: 'cline-pass/qwen3.7-max',
|
|
85
89
|
name: 'Qwen 3.7 Max',
|
|
86
|
-
description: 'Large-scale multimodal foundation model from Alibaba Cloud.',
|
|
90
|
+
description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
|
|
87
91
|
contextLength: 200000,
|
|
88
92
|
maxTokens: 8192,
|
|
89
93
|
input: ['text', 'image'],
|
|
90
94
|
category: 'multimodal',
|
|
91
95
|
recommended: true,
|
|
92
96
|
isCustom: false,
|
|
97
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
93
98
|
},
|
|
94
99
|
{
|
|
95
100
|
id: 'cline-pass/qwen3.7-plus',
|
|
96
101
|
name: 'Qwen 3.7 Plus',
|
|
97
|
-
description: 'Fast, capable multimodal model with strong multilingual skills.',
|
|
102
|
+
description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
|
|
98
103
|
contextLength: 200000,
|
|
99
104
|
maxTokens: 8192,
|
|
100
105
|
input: ['text', 'image'],
|
|
101
106
|
category: 'multimodal',
|
|
102
107
|
recommended: false,
|
|
103
108
|
isCustom: false,
|
|
109
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
104
110
|
},
|
|
105
111
|
{
|
|
106
112
|
id: 'cline-pass/minimax-m3',
|
|
@@ -117,24 +123,26 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
117
123
|
{
|
|
118
124
|
id: 'cline-pass/mimo-v2.5',
|
|
119
125
|
name: 'MiMo V2.5',
|
|
120
|
-
description: 'Xiaomi MiMo efficient instruction model.',
|
|
126
|
+
description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
|
|
121
127
|
contextLength: 200000,
|
|
122
128
|
maxTokens: 8192,
|
|
123
|
-
input: ['text'],
|
|
129
|
+
input: ['text', 'image'],
|
|
124
130
|
category: 'general',
|
|
125
131
|
recommended: false,
|
|
126
132
|
isCustom: false,
|
|
133
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
127
134
|
},
|
|
128
135
|
{
|
|
129
136
|
id: 'cline-pass/mimo-v2.5-pro',
|
|
130
137
|
name: 'MiMo V2.5 Pro',
|
|
131
|
-
description: 'Xiaomi MiMo advanced agentic reasoning model.',
|
|
138
|
+
description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
|
|
132
139
|
contextLength: 200000,
|
|
133
140
|
maxTokens: 8192,
|
|
134
|
-
input: ['text'],
|
|
141
|
+
input: ['text', 'image'],
|
|
135
142
|
category: 'coding',
|
|
136
143
|
recommended: false,
|
|
137
144
|
isCustom: false,
|
|
145
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
138
146
|
},
|
|
139
147
|
])
|
|
140
148
|
|
|
@@ -186,16 +194,19 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
186
194
|
} else {
|
|
187
195
|
// Dynamic fallback for newly introduced models mentioned in plan
|
|
188
196
|
const idPart = name.toLowerCase().replace(/[\s_]+/g, '-')
|
|
197
|
+
const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
|
|
198
|
+
const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
|
|
189
199
|
matched.push({
|
|
190
200
|
id: `cline-pass/${idPart}`,
|
|
191
201
|
name,
|
|
192
202
|
description: `Official ClinePass subscription model: ${name}`,
|
|
193
203
|
contextLength: 200000,
|
|
194
204
|
maxTokens: 8192,
|
|
195
|
-
input: ['text'],
|
|
196
|
-
category: 'general',
|
|
205
|
+
input: isVision ? ['text', 'image'] : ['text'],
|
|
206
|
+
category: hasReasoning ? 'reasoning' : (isVision ? 'multimodal' : 'general'),
|
|
197
207
|
recommended: false,
|
|
198
208
|
isCustom: false,
|
|
209
|
+
...(hasReasoning ? { reasoningEfforts: ['low', 'medium', 'high', 'max'] } : {}),
|
|
199
210
|
})
|
|
200
211
|
}
|
|
201
212
|
}
|
package/lib/provider-sync.js
CHANGED
|
@@ -37,9 +37,9 @@ export async function checkRegisteredInPiAi(ctx) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function resolveActiveAccountKey(ctx, cfg) {
|
|
41
|
-
const
|
|
42
|
-
const configured =
|
|
40
|
+
export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
|
|
41
|
+
const accountPool = pool || await resolveAccountPool(ctx, cfg)
|
|
42
|
+
const configured = accountPool.filter((acc) => acc.present && acc.value)
|
|
43
43
|
if (!configured.length) {
|
|
44
44
|
return { envName: publicConfig(cfg).apiKeyEnv, value: '', source: 'none', id: 'default' }
|
|
45
45
|
}
|
|
@@ -62,7 +62,7 @@ export async function buildStatus(ctx, cfg) {
|
|
|
62
62
|
probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
|
|
63
63
|
])
|
|
64
64
|
|
|
65
|
-
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
65
|
+
const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
|
|
66
66
|
const allModels = getAllModels(pub.dynamicModels)
|
|
67
67
|
|
|
68
68
|
let usage = null
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import { isTrustedSettingsRequest } from '../access.js'
|
|
2
|
+
import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig, Config } from '../config.js'
|
|
4
4
|
import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
|
|
5
5
|
import { clearUsageCache, clearProbeCache } from '../cline-client.js'
|
|
@@ -11,9 +11,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
11
11
|
path: '/dsh-clinebot/accounts/active',
|
|
12
12
|
handler: async (req, res) => {
|
|
13
13
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
14
|
-
if (!
|
|
15
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
16
|
-
}
|
|
14
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
17
15
|
try {
|
|
18
16
|
const bodyBuf = await readBody(req)
|
|
19
17
|
let body = {}
|
|
@@ -43,9 +41,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
43
41
|
path: '/dsh-clinebot/register',
|
|
44
42
|
handler: async (req, res) => {
|
|
45
43
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
46
|
-
if (!
|
|
47
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
48
|
-
}
|
|
44
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
49
45
|
try {
|
|
50
46
|
const bodyBuf = await readBody(req)
|
|
51
47
|
let body = {}
|
|
@@ -65,9 +61,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
65
61
|
path: '/dsh-clinebot/unregister',
|
|
66
62
|
handler: async (req, res) => {
|
|
67
63
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
68
|
-
if (!
|
|
69
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
70
|
-
}
|
|
64
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
71
65
|
try {
|
|
72
66
|
await removePiAiProvider(ctx)
|
|
73
67
|
writeJson(res, 200, { ok: true })
|
package/lib/routes/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import { isTrustedSettingsRequest } from '../access.js'
|
|
2
|
+
import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig } from '../config.js'
|
|
4
4
|
import { resolveActiveAccountKey } from '../provider-sync.js'
|
|
5
5
|
import { smokeChat, recordSessionRequest, rotateToNextAccount, DEFAULT_MODEL_ID } from '../cline-client.js'
|
|
@@ -13,9 +13,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
13
13
|
path: '/dsh-clinebot/auth/begin',
|
|
14
14
|
handler: async (req, res) => {
|
|
15
15
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
16
|
-
if (!
|
|
17
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
18
|
-
}
|
|
16
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
19
17
|
try {
|
|
20
18
|
const authUrl = 'https://app.cline.bot'
|
|
21
19
|
authSession = {
|
|
@@ -54,9 +52,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
54
52
|
path: '/dsh-clinebot/smoke',
|
|
55
53
|
handler: async (req, res) => {
|
|
56
54
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
57
|
-
if (!
|
|
58
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
59
|
-
}
|
|
55
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
60
56
|
try {
|
|
61
57
|
const bodyBuf = await readBody(req)
|
|
62
58
|
let body = {}
|
package/lib/routes/models.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import { isTrustedSettingsRequest } from '../access.js'
|
|
2
|
+
import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig, Config } from '../config.js'
|
|
4
4
|
import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
|
|
5
5
|
import { getAllModels, saveModelsDiskCache } from '../models.js'
|
|
@@ -11,9 +11,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
11
11
|
kind: 'exact',
|
|
12
12
|
path: '/dsh-clinebot/models/sync',
|
|
13
13
|
handler: async (req, res) => {
|
|
14
|
-
if (!
|
|
15
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
16
|
-
}
|
|
14
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
17
15
|
if (req.method !== 'POST') {
|
|
18
16
|
return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
19
17
|
}
|
|
@@ -69,9 +67,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
69
67
|
path: '/dsh-clinebot/models/toggle',
|
|
70
68
|
handler: async (req, res) => {
|
|
71
69
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
72
|
-
if (!
|
|
73
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
74
|
-
}
|
|
70
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
75
71
|
try {
|
|
76
72
|
const bodyBuf = await readBody(req)
|
|
77
73
|
let body = {}
|
package/lib/routes/settings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import { isTrustedSettingsRequest } from '../access.js'
|
|
2
|
+
import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
|
|
3
3
|
import { publicConfig, 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'
|
|
@@ -62,9 +62,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
62
62
|
path: '/dsh-clinebot/save-key',
|
|
63
63
|
handler: async (req, res) => {
|
|
64
64
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
65
|
-
if (!
|
|
66
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
67
|
-
}
|
|
65
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
68
66
|
try {
|
|
69
67
|
const bodyBuf = await readBody(req)
|
|
70
68
|
let body = {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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",
|