@goodandready/dsh-clinebot 0.6.17 → 0.6.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 +9 -0
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/adapter.js +16 -4
- package/lib/session-router.js +28 -21
- package/lib/transport/cline-transport.js +8 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.6.18] - 2026-10-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **DSH 0.2.1-rc.4** is admitted and recorded in `dsh.compatibility.dshReleases`; the health matrix now covers rc.2, rc.3 and rc.4.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Insufficient credits** (HTTP 402): classified separately as `INSUFFICIENT_CREDITS`; the account is cooled down for 5 minutes only for the affected model, so other models keep working.
|
|
15
|
+
- **Oversized requests** (HTTP 413 / JSON node limit): classified as non-retryable `REQUEST_TOO_LARGE` with an actionable message instead of a generic upstream error.
|
|
16
|
+
|
|
8
17
|
## [0.6.17] - 2026-10-28
|
|
9
18
|
|
|
10
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.18</b> adds DSH <code>0.2.1-rc.4</code> coverage, model-scoped credit cooldowns and actionable 402/413 errors. It keeps dated ClinePass model sync, the full <code>0.2.1</code> prerelease line and the multimodal/model/price improvements. Chat runs on the native <code>ctx.llm</code> adapter; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/README.ru.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.18</b> добавляет DSH <code>0.2.1-rc.4</code>, cooldown недостатка кредитов только для конкретной модели и понятные ошибки 402/413. Сохраняются синхронизация dated-моделей ClinePass, вся линия DSH <code>0.2.1</code> и улучшения мультимодальности, моделей и цен. Чат работает через нативный адаптер <code>ctx.llm</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.18</b> 增加 DSH <code>0.2.1-rc.4</code>、按模型隔离的余额不足冷却和明确的 402/413 错误。保留 ClinePass 日期变体同步、DSH <code>0.2.1</code> 整条预发布线以及多模态、模型与价格改进。聊天使用原生 <code>ctx.llm</code> 适配器;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/lib/adapter.js
CHANGED
|
@@ -31,6 +31,7 @@ const PROVIDER_NAME = 'ClineBot (ClinePass)'
|
|
|
31
31
|
const DEFAULT_CONTEXT_WINDOW = 200000
|
|
32
32
|
const DEFAULT_MAX_TOKENS = 8192
|
|
33
33
|
const COOLDOWN_MS = 60000
|
|
34
|
+
const CREDIT_COOLDOWN_MS = 5 * 60 * 1000
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Yield one already-consumed item before the rest of an async iterator.
|
|
@@ -370,11 +371,13 @@ export class ClineBotAdapter extends LlmAdapter {
|
|
|
370
371
|
...(retryAfter ? { providerRetryAfterMs: retryAfter } : {}),
|
|
371
372
|
})
|
|
372
373
|
if (classified.retryable && attempt + 1 < maxAttempts) {
|
|
374
|
+
const isCredit = classified.code === 'INSUFFICIENT_CREDITS'
|
|
373
375
|
markAccountCooldown(
|
|
374
376
|
accountId,
|
|
375
|
-
retryAfter || COOLDOWN_MS,
|
|
376
|
-
retryAfter ? Date.now() + retryAfter : null,
|
|
377
|
-
`upstream_${response.status}`,
|
|
377
|
+
isCredit ? CREDIT_COOLDOWN_MS : (retryAfter || COOLDOWN_MS),
|
|
378
|
+
isCredit ? null : (retryAfter ? Date.now() + retryAfter : null),
|
|
379
|
+
isCredit ? 'insufficient_credits' : `upstream_${response.status}`,
|
|
380
|
+
isCredit ? model : '',
|
|
378
381
|
)
|
|
379
382
|
continue
|
|
380
383
|
}
|
|
@@ -505,9 +508,18 @@ export class ClineBotAdapter extends LlmAdapter {
|
|
|
505
508
|
|| normalized.code === 'PROVIDER_ERROR'
|
|
506
509
|
|| normalized.code === 'TRANSPORT_ERROR'
|
|
507
510
|
|| normalized.code === 'EMPTY_RESPONSE'
|
|
511
|
+
|| normalized.code === 'INSUFFICIENT_CREDITS'
|
|
512
|
+
|| normalized.code === 'QUOTA_EXCEEDED'
|
|
508
513
|
lastError = normalized
|
|
509
514
|
if (retryable && attempt + 1 < maxAttempts) {
|
|
510
|
-
|
|
515
|
+
const isCredit = normalized.code === 'INSUFFICIENT_CREDITS'
|
|
516
|
+
markAccountCooldown(
|
|
517
|
+
accountId,
|
|
518
|
+
isCredit ? CREDIT_COOLDOWN_MS : COOLDOWN_MS,
|
|
519
|
+
null,
|
|
520
|
+
isCredit ? 'insufficient_credits' : 'upstream_pre_token_error',
|
|
521
|
+
isCredit ? model : '',
|
|
522
|
+
)
|
|
511
523
|
continue
|
|
512
524
|
}
|
|
513
525
|
this.recordAttempt({
|
package/lib/session-router.js
CHANGED
|
@@ -73,21 +73,26 @@ export function pruneSessions(now = Date.now(), maxLimit = MAX_SESSIONS, ttlMs =
|
|
|
73
73
|
* Performs automatic recovery if the cooldown duration has passed.
|
|
74
74
|
*
|
|
75
75
|
* @param {string} accountId
|
|
76
|
+
* @param {string} [modelId] Optional model scope
|
|
76
77
|
* @returns {boolean} true if account is currently blocked
|
|
77
78
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const entry = cooldowns.get(accountId)
|
|
79
|
+
function readCooldown(key, accountId) {
|
|
80
|
+
const entry = cooldowns.get(key)
|
|
81
81
|
if (!entry) return false
|
|
82
|
-
|
|
83
82
|
if (Date.now() >= entry.until) {
|
|
84
|
-
cooldowns.delete(
|
|
83
|
+
cooldowns.delete(key)
|
|
85
84
|
emitAccountEvent({ type: 'recover', accountId, reason: entry.reason })
|
|
86
85
|
return false
|
|
87
86
|
}
|
|
88
87
|
return true
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
export function isAccountInCooldown(accountId, modelId = '') {
|
|
91
|
+
if (!accountId) return false
|
|
92
|
+
if (readCooldown(accountId, accountId)) return true
|
|
93
|
+
return Boolean(modelId && readCooldown(`${accountId}::${modelId}`, accountId))
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
/**
|
|
92
97
|
* Mark an account as in cooldown (rate-limited or quota exhausted).
|
|
93
98
|
*
|
|
@@ -95,8 +100,9 @@ export function isAccountInCooldown(accountId) {
|
|
|
95
100
|
* @param {number} [durationMs=60000] Default 60s cooldown if resetsAt is not provided
|
|
96
101
|
* @param {string|number|null} [resetsAt] ISO string or timestamp of quota window reset
|
|
97
102
|
* @param {string} [reason='rate_limit']
|
|
103
|
+
* @param {string} [modelId] Optional model scope
|
|
98
104
|
*/
|
|
99
|
-
export function markAccountCooldown(accountId, durationMs = 60000, resetsAt = null, reason = 'rate_limit') {
|
|
105
|
+
export function markAccountCooldown(accountId, durationMs = 60000, resetsAt = null, reason = 'rate_limit', modelId = '') {
|
|
100
106
|
if (!accountId) return
|
|
101
107
|
|
|
102
108
|
let until = Date.now() + durationMs
|
|
@@ -108,8 +114,9 @@ export function markAccountCooldown(accountId, durationMs = 60000, resetsAt = nu
|
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const key = modelId ? `${accountId}::${modelId}` : accountId
|
|
118
|
+
cooldowns.set(key, { until, reason, accountId, ...(modelId ? { modelId } : {}) })
|
|
119
|
+
emitAccountEvent({ type: 'cooldown', accountId, reason, until, ...(modelId ? { modelId } : {}) })
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
export function matchesAccount(acc, identifier) {
|
|
@@ -117,12 +124,12 @@ export function matchesAccount(acc, identifier) {
|
|
|
117
124
|
return acc.id === identifier || acc.apiKeyEnv === identifier || acc.label === identifier
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
export function isAccountBlocked(acc) {
|
|
127
|
+
export function isAccountBlocked(acc, modelId = '') {
|
|
121
128
|
if (!acc) return false
|
|
122
129
|
return Boolean(
|
|
123
|
-
(acc.id && isAccountInCooldown(acc.id)) ||
|
|
124
|
-
(acc.apiKeyEnv && isAccountInCooldown(acc.apiKeyEnv)) ||
|
|
125
|
-
(acc.label && isAccountInCooldown(acc.label))
|
|
130
|
+
(acc.id && isAccountInCooldown(acc.id, modelId)) ||
|
|
131
|
+
(acc.apiKeyEnv && isAccountInCooldown(acc.apiKeyEnv, modelId)) ||
|
|
132
|
+
(acc.label && isAccountInCooldown(acc.label, modelId))
|
|
126
133
|
)
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -143,11 +150,11 @@ export function getAccountUsageMetric(acc) {
|
|
|
143
150
|
|
|
144
151
|
let roundRobinCursor = 0
|
|
145
152
|
|
|
146
|
-
export function getNextRoundRobinAccount(accounts = []) {
|
|
153
|
+
export function getNextRoundRobinAccount(accounts = [], modelId = '') {
|
|
147
154
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
148
155
|
return null
|
|
149
156
|
}
|
|
150
|
-
const available = accounts.filter(acc => acc && acc.present !== false && !isAccountBlocked(acc))
|
|
157
|
+
const available = accounts.filter(acc => acc && acc.present !== false && !isAccountBlocked(acc, modelId))
|
|
151
158
|
const pool = available.length > 0 ? available : accounts.filter(acc => acc && acc.present !== false)
|
|
152
159
|
if (pool.length === 0) {
|
|
153
160
|
return accounts[0] || null
|
|
@@ -157,7 +164,7 @@ export function getNextRoundRobinAccount(accounts = []) {
|
|
|
157
164
|
return chosen
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
export function selectLeastUsedAccount(accounts = []) {
|
|
167
|
+
export function selectLeastUsedAccount(accounts = [], modelId = '') {
|
|
161
168
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
162
169
|
return null
|
|
163
170
|
}
|
|
@@ -165,7 +172,7 @@ export function selectLeastUsedAccount(accounts = []) {
|
|
|
165
172
|
// Filter available accounts that have keys present and are not in cooldown
|
|
166
173
|
const available = accounts.filter(acc => {
|
|
167
174
|
if (!acc || acc.present === false) return false
|
|
168
|
-
return !isAccountBlocked(acc)
|
|
175
|
+
return !isAccountBlocked(acc, modelId)
|
|
169
176
|
})
|
|
170
177
|
|
|
171
178
|
const candidatePool = available.length > 0 ? available : accounts.filter(a => a && a.present !== false)
|
|
@@ -209,7 +216,7 @@ export function resolveModelRouteAccount(modelId, modelRoutes = {}, safeAccounts
|
|
|
209
216
|
if (!targetKey) return null
|
|
210
217
|
|
|
211
218
|
const targetAcc = safeAccounts.find((a) => matchesAccount(a, targetKey))
|
|
212
|
-
if (targetAcc && !isAccountBlocked(targetAcc)) {
|
|
219
|
+
if (targetAcc && !isAccountBlocked(targetAcc, modelId)) {
|
|
213
220
|
return targetAcc
|
|
214
221
|
}
|
|
215
222
|
return null
|
|
@@ -234,7 +241,7 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
234
241
|
}
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
const pinnedAvailable = pinnedAccount && !isAccountBlocked(pinnedAccount) ? pinnedAccount : null
|
|
244
|
+
const pinnedAvailable = pinnedAccount && !isAccountBlocked(pinnedAccount, model) ? pinnedAccount : null
|
|
238
245
|
|
|
239
246
|
const routedAccount = resolveModelRouteAccount(model, modelRoutes, safeAccounts)
|
|
240
247
|
|
|
@@ -243,9 +250,9 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
243
250
|
return routedAccount
|
|
244
251
|
}
|
|
245
252
|
if (accountMode === 'round_robin') {
|
|
246
|
-
return getNextRoundRobinAccount(safeAccounts) || safeAccounts[0]
|
|
253
|
+
return getNextRoundRobinAccount(safeAccounts, model) || safeAccounts[0]
|
|
247
254
|
}
|
|
248
|
-
return selectLeastUsedAccount(safeAccounts) || safeAccounts[0]
|
|
255
|
+
return selectLeastUsedAccount(safeAccounts, model) || safeAccounts[0]
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
// If no sessionId provided:
|
|
@@ -324,7 +331,7 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
324
331
|
// If in auto mode or pinned account is unavailable:
|
|
325
332
|
if (existing) {
|
|
326
333
|
const assigned = safeAccounts.find(a => matchesAccount(a, existing.accountId))
|
|
327
|
-
if (assigned && !isAccountBlocked(assigned)) {
|
|
334
|
+
if (assigned && !isAccountBlocked(assigned, model)) {
|
|
328
335
|
existing.lastUsedAt = now
|
|
329
336
|
return { account: assigned, isReassigned: false, reason: 'sticky_pinned' }
|
|
330
337
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url)
|
|
12
|
-
let packageVersion = '0.6.
|
|
12
|
+
let packageVersion = '0.6.18'
|
|
13
13
|
try {
|
|
14
14
|
packageVersion = require('../../package.json').version || packageVersion
|
|
15
15
|
} catch {
|
|
@@ -106,9 +106,15 @@ export function classifyUpstreamStatus(status, bodyText = '') {
|
|
|
106
106
|
if (status === 429) {
|
|
107
107
|
return { code: 'RATE_LIMIT', retryable: true, message: `ClinePass rate limit exceeded (HTTP 429)${detail ? `: ${detail}` : ''}` }
|
|
108
108
|
}
|
|
109
|
-
if (status === 402 || /
|
|
109
|
+
if (status === 402 || /insufficient\s+credits?|not enough\s+credits?|credit balance|out of credits/i.test(lower)) {
|
|
110
|
+
return { code: 'INSUFFICIENT_CREDITS', retryable: true, message: `ClinePass account has insufficient credits (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
111
|
+
}
|
|
112
|
+
if (/quota|usage limit/i.test(lower)) {
|
|
110
113
|
return { code: 'QUOTA_EXCEEDED', retryable: true, message: `ClinePass quota exhausted (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
111
114
|
}
|
|
115
|
+
if (status === 413 || /too large|too many (?:json )?nodes?|request body.*(?:large|exceed)|payload too large/i.test(lower)) {
|
|
116
|
+
return { code: 'REQUEST_TOO_LARGE', retryable: false, message: `ClinePass request is too large (HTTP ${status}). Start a new session or reduce messages and images.${detail ? ` ${detail}` : ''}` }
|
|
117
|
+
}
|
|
112
118
|
if (status === 400 && /context|too long|maximum context|token limit/i.test(lower)) {
|
|
113
119
|
return { code: 'CONTEXT_WINDOW_EXCEEDED', retryable: false, message: `Context window exceeded (HTTP 400)${detail ? `: ${detail}` : ''}` }
|
|
114
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"dsh": ">=0.2.1-rc.2 <0.2.2-0",
|
|
54
54
|
"dshReleases": {
|
|
55
55
|
"0.2.1-rc.2": "compatible",
|
|
56
|
-
"0.2.1-rc.3": "compatible"
|
|
56
|
+
"0.2.1-rc.3": "compatible",
|
|
57
|
+
"0.2.1-rc.4": "compatible"
|
|
57
58
|
}
|
|
58
59
|
},
|
|
59
60
|
"client": {
|