@goodandready/dsh-clinebot 0.6.2 → 0.6.3
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 +7 -0
- package/README.md +5 -1
- package/README.ru.md +5 -1
- package/README.zh.md +5 -1
- package/lib/adapter.js +6 -1
- package/lib/budget.js +37 -0
- package/lib/client.js +77 -3
- package/lib/config.js +6 -0
- package/lib/stats-storage.js +15 -0
- package/lib/transport/cline-transport.js +1 -1
- package/lib/tui-settings.js +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.3] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Quota forecast**: each 5-hour, weekly and monthly quota row now shows an estimated burn-down (`~2h 15m left`, `holds until reset`, `no usage yet`) based on the current pace and reset time. Forecasts appear in the sidebar and the center-column panel.
|
|
12
|
+
- **Budget guard**: optional `dailyBudgetUsd` and `monthlyBudgetUsd` limits (0 disables). The adapter rejects new ClineBot requests with `BUDGET_EXCEEDED` once the local estimated spend for the UTC day/month reaches the limit; both fields are available in Settings → ClineBot → Advanced and the TUI.
|
|
13
|
+
- Budget fields are wired through `publicConfig`, and the budget guard is covered by unit/integration tests.
|
|
14
|
+
|
|
8
15
|
## [0.6.2] - 2026-10-21
|
|
9
16
|
|
|
10
17
|
### Removed
|
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.3</b> adds quota burn-down forecasts and optional daily/monthly spend guards on top of the adapter-only line. Chat runs on the native <code>ctx.llm</code> adapter and requires DSH <code>0.2.1-rc.2</code>; 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>
|
|
@@ -197,6 +197,8 @@ dsh-clinebot:
|
|
|
197
197
|
your-reasoning-model: high
|
|
198
198
|
modelContextOverrides: []
|
|
199
199
|
statsPath: ~/.dsh/clinebot-stats.json
|
|
200
|
+
dailyBudgetUsd: 0
|
|
201
|
+
monthlyBudgetUsd: 0
|
|
200
202
|
timeoutMs: 30000
|
|
201
203
|
smokeTimeoutMs: 60000
|
|
202
204
|
streamIdleTimeoutMs: 60000
|
|
@@ -218,6 +220,8 @@ dsh-clinebot:
|
|
|
218
220
|
| `modelReasoningDefaults` | `object` | `{}` | Configured default reasoning effort per model (`low`, `medium`, `high`, `max`) |
|
|
219
221
|
| `modelContextOverrides` | `array` | `[]` | User-defined model context length and max token overrides |
|
|
220
222
|
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | Persistent on-disk path for token usage analytics |
|
|
223
|
+
| `dailyBudgetUsd` | `number` | `0` | Optional estimated daily spend guard; 0 disables it |
|
|
224
|
+
| `monthlyBudgetUsd` | `number` | `0` | Optional estimated monthly spend guard; 0 disables it |
|
|
221
225
|
| `accounts` | `array` | `[]` | Additional accounts for multi-account failover and quota rotation (`[{ label, apiKeyEnv }]`) |
|
|
222
226
|
| `activeAccount` | `string` | `""` | Manually pinned active account env name (empty for least-used auto routing) |
|
|
223
227
|
| `timeoutMs` | `number` | `15000` | HTTP probe timeout in milliseconds |
|
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.3</b> добавляет прогноз расхода квоты и опциональные дневной/месячный лимиты трат. Чат работает через нативный адаптер <code>ctx.llm</code>, требуется DSH <code>0.2.1-rc.2</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>
|
|
@@ -166,6 +166,8 @@ dsh-clinebot:
|
|
|
166
166
|
your-reasoning-model: high
|
|
167
167
|
modelContextOverrides: []
|
|
168
168
|
statsPath: ~/.dsh/clinebot-stats.json
|
|
169
|
+
dailyBudgetUsd: 0
|
|
170
|
+
monthlyBudgetUsd: 0
|
|
169
171
|
timeoutMs: 30000
|
|
170
172
|
smokeTimeoutMs: 60000
|
|
171
173
|
streamIdleTimeoutMs: 60000
|
|
@@ -187,6 +189,8 @@ dsh-clinebot:
|
|
|
187
189
|
| `modelReasoningDefaults` | `object` | `{}` | Значения thinking effort по умолчанию для моделей (`low`, `medium`, `high`, `max`) |
|
|
188
190
|
| `modelContextOverrides` | `array` | `[]` | Пользовательские переопределения размера контекста и лимита токенов |
|
|
189
191
|
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | Путь к файлу персистентной аналитики использования токенов |
|
|
192
|
+
| `dailyBudgetUsd` | `number` | `0` | Опциональный дневной лимит оценочных трат; 0 отключает |
|
|
193
|
+
| `monthlyBudgetUsd` | `number` | `0` | Опциональный месячный лимит оценочных трат; 0 отключает |
|
|
190
194
|
| `accounts` | `array` | `[]` | Дополнительные аккаунты для ротации квоты и failover (`[{ label, apiKeyEnv }]`) |
|
|
191
195
|
| `activeAccount` | `string` | `""` | Принудительно закреплённый аккаунт (пусто для автоматического выбора least-used) |
|
|
192
196
|
| `timeoutMs` | `number` | `15000` | Таймаут HTTP-запросов (мс) |
|
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.3</b> 添加配额消耗预测和可选的每日/每月花费上限。聊天使用原生 <code>ctx.llm</code> 适配器,需要 DSH <code>0.2.1-rc.2</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>
|
|
@@ -168,6 +168,8 @@ dsh-clinebot:
|
|
|
168
168
|
your-reasoning-model: high
|
|
169
169
|
modelContextOverrides: []
|
|
170
170
|
statsPath: ~/.dsh/clinebot-stats.json
|
|
171
|
+
dailyBudgetUsd: 0
|
|
172
|
+
monthlyBudgetUsd: 0
|
|
171
173
|
timeoutMs: 30000
|
|
172
174
|
smokeTimeoutMs: 60000
|
|
173
175
|
streamIdleTimeoutMs: 60000
|
|
@@ -189,6 +191,8 @@ dsh-clinebot:
|
|
|
189
191
|
| `modelReasoningDefaults` | `object` | `{}` | 各模型默认思考强度配置 (`low`, `medium`, `high`, `max`) |
|
|
190
192
|
| `modelContextOverrides` | `array` | `[]` | 用户自定义模型上下文窗口与最大 Token 数量覆盖 |
|
|
191
193
|
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | 本地持久化 Token 用量统计文件路径 |
|
|
194
|
+
| `dailyBudgetUsd` | `number` | `0` | 可选的每日估算花费上限;0 表示关闭 |
|
|
195
|
+
| `monthlyBudgetUsd` | `number` | `0` | 可选的每月估算花费上限;0 表示关闭 |
|
|
192
196
|
| `accounts` | `array` | `[]` | 额外轮转账号池 (`[{ label, apiKeyEnv }]`) |
|
|
193
197
|
| `activeAccount` | `string` | `""` | 手动指定的主账号(为空时自动按最低配额消耗优先路由) |
|
|
194
198
|
| `timeoutMs` | `number` | `15000` | HTTP 请求探测超时时间(毫秒) |
|
package/lib/adapter.js
CHANGED
|
@@ -12,7 +12,8 @@ import { getAllModels, findModel, formatModelDescription } from './models.js'
|
|
|
12
12
|
import { resolveAccountPool } from './account-pool.js'
|
|
13
13
|
import { resolveSessionAccount, markAccountCooldown } from './session-router.js'
|
|
14
14
|
import { recordSessionRequest } from './telemetry.js'
|
|
15
|
-
import { recordUsage } from './stats-storage.js'
|
|
15
|
+
import { recordUsage, getBudgetSpend } from './stats-storage.js'
|
|
16
|
+
import { budgetStatus, budgetErrorMessage } from './budget.js'
|
|
16
17
|
import { estimateCost } from './model-prices.js'
|
|
17
18
|
import { buildOpenAiPayload } from './adapter-messages.js'
|
|
18
19
|
import { streamJsonCompletion, streamOpenAiChunks } from './adapter-stream.js'
|
|
@@ -244,6 +245,10 @@ export class ClineBotAdapter extends LlmAdapter {
|
|
|
244
245
|
if (!pub.enabled) {
|
|
245
246
|
throw new LlmError('ClineBot provider is disabled', 'PROVIDER_DISABLED')
|
|
246
247
|
}
|
|
248
|
+
const budget = budgetStatus(pub, getBudgetSpend())
|
|
249
|
+
if (budget.blocked) {
|
|
250
|
+
throw new LlmError(budgetErrorMessage(budget), 'BUDGET_EXCEEDED')
|
|
251
|
+
}
|
|
247
252
|
|
|
248
253
|
const signal = options.signal
|
|
249
254
|
const sessionId = options.sessionId ? String(options.sessionId) : ''
|
package/lib/budget.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimated-spend budget guard.
|
|
3
|
+
*
|
|
4
|
+
* ClinePass does not expose a per-token invoice; spend comes from the local
|
|
5
|
+
* price table and is therefore explicit about being an estimate. A guard of 0
|
|
6
|
+
* disables the corresponding limit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ dailyBudgetUsd?: number, monthlyBudgetUsd?: number }} config
|
|
11
|
+
* @param {{ dayCost?: number, monthCost?: number }} spend
|
|
12
|
+
* @returns {{ blocked: boolean, scope: 'daily'|'monthly'|null, spent: number, limit: number }}
|
|
13
|
+
*/
|
|
14
|
+
export function budgetStatus(config, spend) {
|
|
15
|
+
const dailyLimit = Math.max(0, Number(config?.dailyBudgetUsd) || 0)
|
|
16
|
+
const monthlyLimit = Math.max(0, Number(config?.monthlyBudgetUsd) || 0)
|
|
17
|
+
const dayCost = Math.max(0, Number(spend?.dayCost) || 0)
|
|
18
|
+
const monthCost = Math.max(0, Number(spend?.monthCost) || 0)
|
|
19
|
+
|
|
20
|
+
if (dailyLimit > 0 && dayCost >= dailyLimit) {
|
|
21
|
+
return { blocked: true, scope: 'daily', spent: dayCost, limit: dailyLimit }
|
|
22
|
+
}
|
|
23
|
+
if (monthlyLimit > 0 && monthCost >= monthlyLimit) {
|
|
24
|
+
return { blocked: true, scope: 'monthly', spent: monthCost, limit: monthlyLimit }
|
|
25
|
+
}
|
|
26
|
+
return { blocked: false, scope: null, spent: 0, limit: 0 }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable error for an exhausted budget guard.
|
|
31
|
+
* @param {ReturnType<typeof budgetStatus>} status
|
|
32
|
+
*/
|
|
33
|
+
export function budgetErrorMessage(status) {
|
|
34
|
+
if (!status?.blocked) return ''
|
|
35
|
+
const label = status.scope === 'monthly' ? 'Monthly' : 'Daily'
|
|
36
|
+
return `${label} ClineBot budget reached (estimated $${status.spent.toFixed(4)} of $${status.limit.toFixed(2)}). Raise the limit in Settings -> ClineBot -> Advanced.`
|
|
37
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -112,6 +112,10 @@ const en = {
|
|
|
112
112
|
'quota.countdown': 'Resets in {time}',
|
|
113
113
|
'quota.resetting': 'Resetting…',
|
|
114
114
|
'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
|
|
115
|
+
'quota.forecast_no_usage': 'No usage yet',
|
|
116
|
+
'quota.forecast_holds': 'Holds until reset at the current pace',
|
|
117
|
+
'quota.forecast_left': '~{time} left at the current pace',
|
|
118
|
+
'quota.forecast_exhausted': 'Exhausted',
|
|
115
119
|
'quota.exhausted_5h': '5-hour rolling limit is almost exhausted ({percentUsed}%). New requests may be rejected until quota reset.',
|
|
116
120
|
'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
|
|
117
121
|
'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
@@ -256,6 +260,7 @@ const en = {
|
|
|
256
260
|
'settings.base_url': 'API base URL', 'settings.base_url_hint': 'ClinePass API root. Remote endpoints must use https://.',
|
|
257
261
|
'settings.timeout': 'Connect timeout (ms)', 'settings.timeout_hint': 'Maximum wait for the upstream response headers.', 'settings.smoke_timeout': 'Smoke-test timeout (ms)', 'settings.smoke_timeout_hint': 'Maximum wait for the diagnostic chat completion.',
|
|
258
262
|
'settings.stream_idle': 'Stream idle timeout (ms)', 'settings.stream_idle_hint': 'Maximum gap between SSE chunks before the stream is treated as dead.', 'settings.max_payload': 'Max multimodal payload (MB)', 'settings.max_payload_hint': 'Oldest images are evicted once the request exceeds this budget.',
|
|
263
|
+
'settings.daily_budget': 'Daily budget (USD)', 'settings.daily_budget_hint': 'Estimated spend guard for the current UTC day. 0 disables it.', 'settings.monthly_budget': 'Monthly budget (USD)', 'settings.monthly_budget_hint': 'Estimated spend guard for the current UTC month. 0 disables it.',
|
|
259
264
|
'settings.stats_path': 'Stats file path', 'settings.stats_path_hint': 'Persistent JSON telemetry path (supports ~/).', 'settings.models_cache_path': 'Models cache path', 'settings.models_cache_path_hint': 'Offline model catalogue cache (supports ~/).',
|
|
260
265
|
'providerCard.title': 'ClineBot (ClinePass)',
|
|
261
266
|
'providerCard.hint': 'Paste a ClinePass API key or manage the full account pool in Settings → ClineBot.',
|
|
@@ -389,6 +394,10 @@ const zh = {
|
|
|
389
394
|
'quota.countdown': '{time}后重置',
|
|
390
395
|
'quota.resetting': '正在重置',
|
|
391
396
|
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
397
|
+
'quota.forecast_no_usage': '尚未使用',
|
|
398
|
+
'quota.forecast_holds': '按当前速度可坚持到重置',
|
|
399
|
+
'quota.forecast_left': '按当前速度约剩 {time}',
|
|
400
|
+
'quota.forecast_exhausted': '已耗尽',
|
|
392
401
|
'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
|
|
393
402
|
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
394
403
|
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
@@ -533,6 +542,7 @@ const zh = {
|
|
|
533
542
|
'settings.base_url': 'API 基础地址', 'settings.base_url_hint': 'ClinePass API 根地址。远程端点必须使用 https://。',
|
|
534
543
|
'settings.timeout': '连接超时(毫秒)', 'settings.timeout_hint': '等待上游响应头的最大时间。', 'settings.smoke_timeout': '烟雾测试超时(毫秒)', 'settings.smoke_timeout_hint': '等待诊断聊天补全的最大时间。',
|
|
535
544
|
'settings.stream_idle': '流空闲超时(毫秒)', 'settings.stream_idle_hint': 'SSE 数据块之间的最大间隔,超过后视为断流。', 'settings.max_payload': '多模态负载上限(MB)', 'settings.max_payload_hint': '请求超过预算时会淘汰最旧的图片。',
|
|
545
|
+
'settings.daily_budget': '每日预算(USD)', 'settings.daily_budget_hint': '按当前 UTC 日估算的花费上限。0 表示关闭。', 'settings.monthly_budget': '每月预算(USD)', 'settings.monthly_budget_hint': '按当前 UTC 月估算的花费上限。0 表示关闭。',
|
|
536
546
|
'settings.stats_path': '统计文件路径', 'settings.stats_path_hint': '持久化 JSON 遥测路径(支持 ~/)。', 'settings.models_cache_path': '模型缓存路径', 'settings.models_cache_path_hint': '离线模型目录缓存(支持 ~/)。',
|
|
537
547
|
'providerCard.title': 'ClineBot(ClinePass)',
|
|
538
548
|
'providerCard.hint': '粘贴 ClinePass API 密钥,或前往“设置 → ClineBot”管理完整账户池。',
|
|
@@ -785,6 +795,7 @@ function ensureCss() {
|
|
|
785
795
|
.cb-progress-meta, .cb-bar-meta{flex:1 1 140px;min-width:0;margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-align:right;white-space:normal;overflow-wrap:anywhere}
|
|
786
796
|
.cb-progress-meta-bad{color:var(--dsw-alias-state-error-primary)}
|
|
787
797
|
.cb-progress-low{display:inline-flex;align-items:center;gap:4px;margin-left:6px;font-size:11px;color:var(--dsw-alias-state-error-primary)}
|
|
798
|
+
.cb-progress-forecast{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
788
799
|
.cb-progress-track, .cb-bar-track{width:100%;height:6px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
789
800
|
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
790
801
|
.cb-progress-bar-warn{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-state-warn-primary);transition:width .3s ease}
|
|
@@ -827,6 +838,7 @@ function ensureCss() {
|
|
|
827
838
|
.cb-footBar{display:block;height:4px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
828
839
|
.cb-footFill{display:block;height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
829
840
|
.cb-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
841
|
+
.cb-footForecast{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:14px}
|
|
830
842
|
.cb-footPct{margin-left:auto;flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px;font-weight:600;line-height:16px;font-variant-numeric:tabular-nums}
|
|
831
843
|
.cb-footPctWarn{color:var(--dsw-alias-state-error-primary)}
|
|
832
844
|
.cb-railButton{box-sizing:border-box;flex:none;display:inline-flex;justify-content:center;align-items:center;width:36px;height:36px;margin:0 0 4px;padding:0;border:.5px solid transparent;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
@@ -1453,13 +1465,45 @@ function formatResetTime(isoString) {
|
|
|
1453
1465
|
}
|
|
1454
1466
|
}
|
|
1455
1467
|
|
|
1456
|
-
function
|
|
1468
|
+
function formatForecastDuration(ms) {
|
|
1469
|
+
const totalMinutes = Math.max(1, Math.round(Number(ms) / 60000))
|
|
1470
|
+
const days = Math.floor(totalMinutes / 1440)
|
|
1471
|
+
const hours = Math.floor((totalMinutes % 1440) / 60)
|
|
1472
|
+
const minutes = totalMinutes % 60
|
|
1473
|
+
if (days > 0) return `${days}d ${hours}h`
|
|
1474
|
+
if (hours > 0) return `${hours}h ${minutes}m`
|
|
1475
|
+
return `${minutes}m`
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function quotaForecast(window, windowMs, t) {
|
|
1479
|
+
const used = Number(window?.percentUsed)
|
|
1480
|
+
const remaining = Number.isFinite(Number(window?.remainingPercent))
|
|
1481
|
+
? Number(window.remainingPercent)
|
|
1482
|
+
: 100 - used
|
|
1483
|
+
const resetAt = new Date(window?.resetsAt).getTime()
|
|
1484
|
+
const span = Number(windowMs)
|
|
1485
|
+
if (!Number.isFinite(used) || !Number.isFinite(resetAt) || !Number.isFinite(span) || span <= 0) return ''
|
|
1486
|
+
const untilReset = resetAt - Date.now()
|
|
1487
|
+
if (untilReset <= 0) return ''
|
|
1488
|
+
if (remaining <= 0) return t('quota.forecast_exhausted')
|
|
1489
|
+
if (used <= 0) return t('quota.forecast_no_usage')
|
|
1490
|
+
const elapsed = Math.max(1, span - untilReset)
|
|
1491
|
+
const burnPerMs = used / elapsed
|
|
1492
|
+
if (!Number.isFinite(burnPerMs) || burnPerMs <= 0) return ''
|
|
1493
|
+
const etaMs = remaining / burnPerMs
|
|
1494
|
+
if (!Number.isFinite(etaMs) || etaMs <= 0) return ''
|
|
1495
|
+
if (etaMs >= untilReset) return t('quota.forecast_holds')
|
|
1496
|
+
return t('quota.forecast_left', { time: formatForecastDuration(etaMs) })
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, windowMs, t }) {
|
|
1457
1500
|
const pct = Math.max(0, Math.min(100, Math.round(percentUsed || 0)))
|
|
1458
1501
|
const isWarn = pct >= 80 && pct < 90
|
|
1459
1502
|
const isBad = pct >= 90
|
|
1460
1503
|
const barClass = isBad ? 'cb-progress-bar-bad' : isWarn ? 'cb-progress-bar-warn' : 'cb-progress-bar-fill'
|
|
1461
1504
|
const resetCountdown = formatResetCountdown(resetsAt)
|
|
1462
1505
|
const resetClock = formatResetTime(resetsAt)
|
|
1506
|
+
const forecast = windowMs ? quotaForecast({ percentUsed, remainingPercent, resetsAt }, windowMs, t) : ''
|
|
1463
1507
|
|
|
1464
1508
|
return React.createElement(
|
|
1465
1509
|
'div',
|
|
@@ -1493,7 +1537,8 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
1493
1537
|
'div',
|
|
1494
1538
|
{ className: 'cb-progress-track' },
|
|
1495
1539
|
React.createElement('div', { className: barClass, style: { width: `${pct}%` } })
|
|
1496
|
-
)
|
|
1540
|
+
),
|
|
1541
|
+
forecast ? React.createElement('div', { className: 'cb-progress-forecast' }, forecast) : null
|
|
1497
1542
|
)
|
|
1498
1543
|
}
|
|
1499
1544
|
|
|
@@ -1717,6 +1762,22 @@ function AdvancedSection({ draft, onStage, t }) {
|
|
|
1717
1762
|
type: 'number',
|
|
1718
1763
|
onChange: numeric('maxPayloadMb', 0),
|
|
1719
1764
|
}),
|
|
1765
|
+
React.createElement(AdvancedField, {
|
|
1766
|
+
id: 'cb-daily-budget',
|
|
1767
|
+
label: t('settings.daily_budget'),
|
|
1768
|
+
hint: t('settings.daily_budget_hint'),
|
|
1769
|
+
value: current.dailyBudgetUsd,
|
|
1770
|
+
type: 'number',
|
|
1771
|
+
onChange: numeric('dailyBudgetUsd', -1),
|
|
1772
|
+
}),
|
|
1773
|
+
React.createElement(AdvancedField, {
|
|
1774
|
+
id: 'cb-monthly-budget',
|
|
1775
|
+
label: t('settings.monthly_budget'),
|
|
1776
|
+
hint: t('settings.monthly_budget_hint'),
|
|
1777
|
+
value: current.monthlyBudgetUsd,
|
|
1778
|
+
type: 'number',
|
|
1779
|
+
onChange: numeric('monthlyBudgetUsd', -1),
|
|
1780
|
+
}),
|
|
1720
1781
|
React.createElement(AdvancedField, {
|
|
1721
1782
|
id: 'cb-stats-path',
|
|
1722
1783
|
label: t('settings.stats_path'),
|
|
@@ -2444,6 +2505,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2444
2505
|
percentUsed: usage.windows.fiveHour.percentUsed || 0,
|
|
2445
2506
|
remainingPercent: usage.windows.fiveHour.remainingPercent || 100,
|
|
2446
2507
|
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
2508
|
+
windowMs: 5 * 60 * 60 * 1000,
|
|
2447
2509
|
t,
|
|
2448
2510
|
})
|
|
2449
2511
|
: null,
|
|
@@ -2452,6 +2514,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2452
2514
|
percentUsed: usage?.windows?.weekly?.percentUsed || 0,
|
|
2453
2515
|
remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
|
|
2454
2516
|
resetsAt: usage?.windows?.weekly?.resetsAt,
|
|
2517
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
2455
2518
|
t,
|
|
2456
2519
|
}),
|
|
2457
2520
|
usage?.windows?.monthly
|
|
@@ -2460,6 +2523,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2460
2523
|
percentUsed: usage.windows.monthly.percentUsed || 0,
|
|
2461
2524
|
remainingPercent: usage.windows.monthly.remainingPercent || 100,
|
|
2462
2525
|
resetsAt: usage.windows.monthly.resetsAt,
|
|
2526
|
+
windowMs: 30 * 24 * 60 * 60 * 1000,
|
|
2463
2527
|
t,
|
|
2464
2528
|
})
|
|
2465
2529
|
: null
|
|
@@ -3780,7 +3844,14 @@ function SidebarQuota({ ctx, wide, t: propT, open: openPanel }) {
|
|
|
3780
3844
|
className: warn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
|
|
3781
3845
|
style: { width: hasData ? `${Math.min(100, Math.max(2, value))}%` : '0%' },
|
|
3782
3846
|
})
|
|
3783
|
-
)
|
|
3847
|
+
),
|
|
3848
|
+
hasData
|
|
3849
|
+
? React.createElement(
|
|
3850
|
+
'span',
|
|
3851
|
+
{ className: 'cb-footForecast' },
|
|
3852
|
+
quotaForecast(window.value, window.key === 'fiveHour' ? 5 * 60 * 60 * 1000 : window.key === 'weekly' ? 7 * 24 * 60 * 60 * 1000 : 30 * 24 * 60 * 60 * 1000, t)
|
|
3853
|
+
)
|
|
3854
|
+
: null
|
|
3784
3855
|
)
|
|
3785
3856
|
}),
|
|
3786
3857
|
rows.some((window) => windowPct(window) !== null)
|
|
@@ -4068,6 +4139,7 @@ function QuotaPanelView({
|
|
|
4068
4139
|
percentUsed: selectedUsage.windows.fiveHour.percentUsed || 0,
|
|
4069
4140
|
remainingPercent: selectedUsage.windows.fiveHour.remainingPercent || 100,
|
|
4070
4141
|
resetsAt: selectedUsage.windows.fiveHour.resetsAt,
|
|
4142
|
+
windowMs: 5 * 60 * 60 * 1000,
|
|
4071
4143
|
t,
|
|
4072
4144
|
})
|
|
4073
4145
|
: null,
|
|
@@ -4077,6 +4149,7 @@ function QuotaPanelView({
|
|
|
4077
4149
|
percentUsed: selectedUsage.windows.weekly.percentUsed || 0,
|
|
4078
4150
|
remainingPercent: selectedUsage.windows.weekly.remainingPercent || 100,
|
|
4079
4151
|
resetsAt: selectedUsage.windows.weekly.resetsAt,
|
|
4152
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
4080
4153
|
t,
|
|
4081
4154
|
})
|
|
4082
4155
|
: null,
|
|
@@ -4086,6 +4159,7 @@ function QuotaPanelView({
|
|
|
4086
4159
|
percentUsed: selectedUsage.windows.monthly.percentUsed || 0,
|
|
4087
4160
|
remainingPercent: selectedUsage.windows.monthly.remainingPercent || 100,
|
|
4088
4161
|
resetsAt: selectedUsage.windows.monthly.resetsAt,
|
|
4162
|
+
windowMs: 30 * 24 * 60 * 60 * 1000,
|
|
4089
4163
|
t,
|
|
4090
4164
|
})
|
|
4091
4165
|
: null,
|
package/lib/config.js
CHANGED
|
@@ -99,6 +99,10 @@ export const Config = z.object({
|
|
|
99
99
|
.description('User-defined custom gateway models.').volatile(),
|
|
100
100
|
statsPath: z.string().default('~/.dsh/clinebot-stats.json')
|
|
101
101
|
.description('Local on-disk persistent path for token usage analytics.').volatile(),
|
|
102
|
+
dailyBudgetUsd: z.number().min(0).default(0)
|
|
103
|
+
.description('Optional daily estimated-spend guard in USD. 0 disables the guard.').volatile(),
|
|
104
|
+
monthlyBudgetUsd: z.number().min(0).default(0)
|
|
105
|
+
.description('Optional monthly estimated-spend guard in USD. 0 disables the guard.').volatile(),
|
|
102
106
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
103
107
|
.description('HTTP probe timeout in milliseconds.').volatile(),
|
|
104
108
|
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
@@ -238,6 +242,8 @@ export function publicConfig(cfg) {
|
|
|
238
242
|
modelRoutes: cfg?.modelRoutes && typeof cfg.modelRoutes === 'object' ? cfg.modelRoutes : {},
|
|
239
243
|
customModels: custom,
|
|
240
244
|
statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
|
|
245
|
+
dailyBudgetUsd: Math.max(0, Number(cfg?.dailyBudgetUsd) || 0),
|
|
246
|
+
monthlyBudgetUsd: Math.max(0, Number(cfg?.monthlyBudgetUsd) || 0),
|
|
241
247
|
disabledModels: disabledList,
|
|
242
248
|
filterModelsByPlan,
|
|
243
249
|
visibleModels,
|
package/lib/stats-storage.js
CHANGED
|
@@ -361,6 +361,21 @@ export function getSessionStats(sessionId) {
|
|
|
361
361
|
return bucket ? JSON.parse(JSON.stringify(bucket)) : null
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Current UTC day/month estimated spend for budget guards.
|
|
366
|
+
* @param {number} [now]
|
|
367
|
+
*/
|
|
368
|
+
export function getBudgetSpend(now = Date.now()) {
|
|
369
|
+
const dayKey = new Date(now).toISOString().slice(0, 10)
|
|
370
|
+
const monthKey = dayKey.slice(0, 7)
|
|
371
|
+
return {
|
|
372
|
+
dayKey,
|
|
373
|
+
monthKey,
|
|
374
|
+
dayCost: Number(inMemoryStats.byDay?.[dayKey]?.costUsd || 0),
|
|
375
|
+
monthCost: Number(inMemoryStats.byMonth?.[monthKey]?.costUsd || 0),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
364
379
|
export function resetStats(customPath) {
|
|
365
380
|
inMemoryStats = createEmptyStats()
|
|
366
381
|
saveStatsSync(customPath)
|
package/lib/tui-settings.js
CHANGED
|
@@ -114,6 +114,30 @@ export function buildTuiSection({ live }) {
|
|
|
114
114
|
return Number.isInteger(value) && value >= -1 ? { kind: 'set', value } : undefined
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
|
+
{
|
|
118
|
+
path: ['dailyBudgetUsd'],
|
|
119
|
+
group: 'advanced',
|
|
120
|
+
kind: 'number',
|
|
121
|
+
label: 'Daily budget (USD)',
|
|
122
|
+
hint: 'Estimated spend guard; 0 disables it.',
|
|
123
|
+
format: (value) => (Number(value) > 0 ? String(Number(value)) : ''),
|
|
124
|
+
parse: (text) => {
|
|
125
|
+
const value = Number(text)
|
|
126
|
+
return Number.isFinite(value) && value >= 0 ? { kind: 'set', value } : { kind: 'clear' }
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: ['monthlyBudgetUsd'],
|
|
131
|
+
group: 'advanced',
|
|
132
|
+
kind: 'number',
|
|
133
|
+
label: 'Monthly budget (USD)',
|
|
134
|
+
hint: 'Estimated spend guard; 0 disables it.',
|
|
135
|
+
format: (value) => (Number(value) > 0 ? String(Number(value)) : ''),
|
|
136
|
+
parse: (text) => {
|
|
137
|
+
const value = Number(text)
|
|
138
|
+
return Number.isFinite(value) && value >= 0 ? { kind: 'set', value } : { kind: 'clear' }
|
|
139
|
+
},
|
|
140
|
+
},
|
|
117
141
|
]
|
|
118
142
|
|
|
119
143
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|