@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 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.17</b> syncs dated ClinePass variants such as <code>deepseek-v4.1-flash-1017</code>, accepts the full DSH <code>0.2.1</code> prerelease line and keeps 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>
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.17</b> синхронизирует dated-варианты ClinePass, включая <code>deepseek-v4.1-flash-1017</code>, поддерживает всю линию DSH <code>0.2.1</code> и сохраняет улучшения мультимодальности, моделей и цен. Чат работает через нативный адаптер <code>ctx.llm</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
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.17</b> 同步 ClinePass 日期变体(包括 <code>deepseek-v4.1-flash-1017</code>),支持 DSH <code>0.2.1</code> 整条预发布线,并保留多模态、模型与价格改进。聊天使用原生 <code>ctx.llm</code> 适配器;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
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
- markAccountCooldown(accountId, COOLDOWN_MS, null, 'upstream_pre_token_error')
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({
@@ -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
- export function isAccountInCooldown(accountId) {
79
- if (!accountId) return false
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(accountId)
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
- cooldowns.set(accountId, { until, reason })
112
- emitAccountEvent({ type: 'cooldown', accountId, reason, until })
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.17'
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 || /quota|credit|usage limit|insufficient/i.test(lower)) {
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.17",
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": {