@goodandready/dsh-clinebot 0.6.1 → 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 +14 -0
- package/README.md +10 -14
- package/README.ru.md +6 -13
- package/README.zh.md +6 -13
- package/lib/adapter.js +6 -1
- package/lib/budget.js +37 -0
- package/lib/client.js +86 -142
- package/lib/cline-client.js +0 -102
- package/lib/config.js +6 -8
- package/lib/index.js +7 -201
- package/lib/provider-sync.js +2 -141
- package/lib/routes/accounts.js +1 -37
- 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/lib/proxy-server.js +0 -138
- package/lib/proxy-token.js +0 -187
- package/lib/routes/proxy.js +0 -578
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).',
|
|
@@ -206,19 +210,9 @@ const en = {
|
|
|
206
210
|
'stats.confirm_reset': 'Confirm Reset?',
|
|
207
211
|
'stats.cancel_reset': 'Cancel',
|
|
208
212
|
'diag.title': 'Diagnostics & Auto-Registration',
|
|
209
|
-
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test
|
|
213
|
+
'diag.desc': 'ClineBot registers into DSH Models automatically when a key is configured. You can run a connection smoke test here.',
|
|
210
214
|
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
211
215
|
'diag.smoke_testing': 'Testing…',
|
|
212
|
-
'diag.resync_btn': 'Force Re-sync to DSH',
|
|
213
|
-
'diag.resyncing': 'Syncing…',
|
|
214
|
-
'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
|
|
215
|
-
'diag.unregister_btn': 'Unregister from DSH',
|
|
216
|
-
'diag.unregistering': 'Removing…',
|
|
217
|
-
'diag.unregistered_msg': 'Provider removed from DSH Models',
|
|
218
|
-
'diag.proxy_title': 'Transparent Loopback Proxy',
|
|
219
|
-
'diag.proxy_desc': 'Enables multi-account failover, sticky sessions, and telemetry via local loopback proxy. Disable to connect directly to upstream endpoint.',
|
|
220
|
-
'diag.proxy_enabled': 'Proxy Mode Enabled (Failover & Telemetry Active)',
|
|
221
|
-
'diag.proxy_disabled': 'Direct Mode (Direct Upstream, No Proxy)',
|
|
222
216
|
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
223
217
|
'update.btn': 'Update Now',
|
|
224
218
|
'update.updating': 'Updating…',
|
|
@@ -262,13 +256,12 @@ const en = {
|
|
|
262
256
|
'settings.web_search': 'Serve web search with ClineBot', 'settings.web_search_hint': 'Use the ClinePass endpoint for the DSH web_search tool.',
|
|
263
257
|
'settings.sidebar_quota': 'Show quota meter in the sidebar', 'settings.sidebar_quota_hint': 'Display the compact plan and rolling-window meter above Settings.',
|
|
264
258
|
'settings.composer_cost': 'Show session cost in the composer', 'settings.composer_cost_hint': 'Display the estimated token cost beside the composer.',
|
|
265
|
-
'settings.
|
|
266
|
-
'settings.advanced': 'Advanced', 'settings.advanced_desc': 'Connection, timeouts, storage paths and the temporary transport switch.',
|
|
259
|
+
'settings.advanced': 'Advanced', 'settings.advanced_desc': 'Connection, timeouts and storage paths.',
|
|
267
260
|
'settings.base_url': 'API base URL', 'settings.base_url_hint': 'ClinePass API root. Remote endpoints must use https://.',
|
|
268
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.',
|
|
269
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.',
|
|
270
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 ~/).',
|
|
271
|
-
'settings.transport': 'Transport', 'settings.transport_hint': 'Native ctx.llm adapter (recommended) or the legacy llm-pi-ai + loopback proxy.',
|
|
272
265
|
'providerCard.title': 'ClineBot (ClinePass)',
|
|
273
266
|
'providerCard.hint': 'Paste a ClinePass API key or manage the full account pool in Settings → ClineBot.',
|
|
274
267
|
'providerCard.keyPresent': 'Key configured',
|
|
@@ -401,6 +394,10 @@ const zh = {
|
|
|
401
394
|
'quota.countdown': '{time}后重置',
|
|
402
395
|
'quota.resetting': '正在重置',
|
|
403
396
|
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
397
|
+
'quota.forecast_no_usage': '尚未使用',
|
|
398
|
+
'quota.forecast_holds': '按当前速度可坚持到重置',
|
|
399
|
+
'quota.forecast_left': '按当前速度约剩 {time}',
|
|
400
|
+
'quota.forecast_exhausted': '已耗尽',
|
|
404
401
|
'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
|
|
405
402
|
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
406
403
|
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
@@ -495,19 +492,9 @@ const zh = {
|
|
|
495
492
|
'stats.confirm_reset': '确认重置?',
|
|
496
493
|
'stats.cancel_reset': '取消',
|
|
497
494
|
'diag.title': '诊断工具与服务接入',
|
|
498
|
-
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH
|
|
495
|
+
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试。',
|
|
499
496
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
500
497
|
'diag.smoke_testing': '正在测试…',
|
|
501
|
-
'diag.resync_btn': '强制重新同步到 DSH',
|
|
502
|
-
'diag.resyncing': '正在同步…',
|
|
503
|
-
'diag.resynced_msg': '模型服务已重新同步到 DSH ({count} 个模型)',
|
|
504
|
-
'diag.unregister_btn': '从 DSH 注销该服务',
|
|
505
|
-
'diag.unregistering': '正在注销…',
|
|
506
|
-
'diag.unregistered_msg': '已从 DSH 模型注册表中移除',
|
|
507
|
-
'diag.proxy_title': '本地透明回环代理',
|
|
508
|
-
'diag.proxy_desc': '通过本地回环代理支持多账户故障转移、会话保持与实时统计。关闭后直连上游端点。',
|
|
509
|
-
'diag.proxy_enabled': '代理模式已开启(支持故障转移与遥测)',
|
|
510
|
-
'diag.proxy_disabled': '直连模式(直接连接上游,无本地代理)',
|
|
511
498
|
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
512
499
|
'update.btn': '立即更新',
|
|
513
500
|
'update.updating': '正在更新…',
|
|
@@ -551,13 +538,12 @@ const zh = {
|
|
|
551
538
|
'settings.web_search': '使用 ClineBot 提供网页搜索', 'settings.web_search_hint': '为 DSH web_search 工具使用 ClinePass 端点。',
|
|
552
539
|
'settings.sidebar_quota': '在侧边栏显示配额', 'settings.sidebar_quota_hint': '在设置上方显示紧凑的套餐与滚动窗口配额。',
|
|
553
540
|
'settings.composer_cost': '在输入框显示会话成本', 'settings.composer_cost_hint': '在输入框旁显示预估 token 成本。',
|
|
554
|
-
'settings.
|
|
555
|
-
'settings.advanced': '高级', 'settings.advanced_desc': '连接、超时、存储路径和临时传输开关。',
|
|
541
|
+
'settings.advanced': '高级', 'settings.advanced_desc': '连接、超时和存储路径。',
|
|
556
542
|
'settings.base_url': 'API 基础地址', 'settings.base_url_hint': 'ClinePass API 根地址。远程端点必须使用 https://。',
|
|
557
543
|
'settings.timeout': '连接超时(毫秒)', 'settings.timeout_hint': '等待上游响应头的最大时间。', 'settings.smoke_timeout': '烟雾测试超时(毫秒)', 'settings.smoke_timeout_hint': '等待诊断聊天补全的最大时间。',
|
|
558
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 表示关闭。',
|
|
559
546
|
'settings.stats_path': '统计文件路径', 'settings.stats_path_hint': '持久化 JSON 遥测路径(支持 ~/)。', 'settings.models_cache_path': '模型缓存路径', 'settings.models_cache_path_hint': '离线模型目录缓存(支持 ~/)。',
|
|
560
|
-
'settings.transport': '传输方式', 'settings.transport_hint': '原生 ctx.llm 适配器(推荐)或旧版 llm-pi-ai + 回环代理。',
|
|
561
547
|
'providerCard.title': 'ClineBot(ClinePass)',
|
|
562
548
|
'providerCard.hint': '粘贴 ClinePass API 密钥,或前往“设置 → ClineBot”管理完整账户池。',
|
|
563
549
|
'providerCard.keyPresent': '密钥已配置',
|
|
@@ -809,6 +795,7 @@ function ensureCss() {
|
|
|
809
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}
|
|
810
796
|
.cb-progress-meta-bad{color:var(--dsw-alias-state-error-primary)}
|
|
811
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}
|
|
812
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}
|
|
813
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}
|
|
814
801
|
.cb-progress-bar-warn{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-state-warn-primary);transition:width .3s ease}
|
|
@@ -851,6 +838,7 @@ function ensureCss() {
|
|
|
851
838
|
.cb-footBar{display:block;height:4px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
852
839
|
.cb-footFill{display:block;height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
853
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}
|
|
854
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}
|
|
855
843
|
.cb-footPctWarn{color:var(--dsw-alias-state-error-primary)}
|
|
856
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}
|
|
@@ -1477,13 +1465,45 @@ function formatResetTime(isoString) {
|
|
|
1477
1465
|
}
|
|
1478
1466
|
}
|
|
1479
1467
|
|
|
1480
|
-
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 }) {
|
|
1481
1500
|
const pct = Math.max(0, Math.min(100, Math.round(percentUsed || 0)))
|
|
1482
1501
|
const isWarn = pct >= 80 && pct < 90
|
|
1483
1502
|
const isBad = pct >= 90
|
|
1484
1503
|
const barClass = isBad ? 'cb-progress-bar-bad' : isWarn ? 'cb-progress-bar-warn' : 'cb-progress-bar-fill'
|
|
1485
1504
|
const resetCountdown = formatResetCountdown(resetsAt)
|
|
1486
1505
|
const resetClock = formatResetTime(resetsAt)
|
|
1506
|
+
const forecast = windowMs ? quotaForecast({ percentUsed, remainingPercent, resetsAt }, windowMs, t) : ''
|
|
1487
1507
|
|
|
1488
1508
|
return React.createElement(
|
|
1489
1509
|
'div',
|
|
@@ -1517,7 +1537,8 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
1517
1537
|
'div',
|
|
1518
1538
|
{ className: 'cb-progress-track' },
|
|
1519
1539
|
React.createElement('div', { className: barClass, style: { width: `${pct}%` } })
|
|
1520
|
-
)
|
|
1540
|
+
),
|
|
1541
|
+
forecast ? React.createElement('div', { className: 'cb-progress-forecast' }, forecast) : null
|
|
1521
1542
|
)
|
|
1522
1543
|
}
|
|
1523
1544
|
|
|
@@ -1637,7 +1658,7 @@ function IntegrationToggle({ id, label, hint, checked, onChange }) {
|
|
|
1637
1658
|
})
|
|
1638
1659
|
}
|
|
1639
1660
|
|
|
1640
|
-
function IntegrationsSection({ draft, onStage,
|
|
1661
|
+
function IntegrationsSection({ draft, onStage, t }) {
|
|
1641
1662
|
const current = draft || {}
|
|
1642
1663
|
return React.createElement(
|
|
1643
1664
|
SettingsGroup,
|
|
@@ -1666,16 +1687,7 @@ function IntegrationsSection({ draft, onStage, transport, t }) {
|
|
|
1666
1687
|
hint: t('settings.composer_cost_hint'),
|
|
1667
1688
|
checked: current.showComposerCost !== false,
|
|
1668
1689
|
onChange: (value) => onStage({ showComposerCost: value }),
|
|
1669
|
-
})
|
|
1670
|
-
transport === 'pi-ai'
|
|
1671
|
-
? React.createElement(IntegrationToggle, {
|
|
1672
|
-
id: 'cb-proxy-mode',
|
|
1673
|
-
label: t('settings.proxy_mode'),
|
|
1674
|
-
hint: t('settings.proxy_mode_hint'),
|
|
1675
|
-
checked: current.proxyMode !== false,
|
|
1676
|
-
onChange: (value) => onStage({ proxyMode: value }),
|
|
1677
|
-
})
|
|
1678
|
-
: null
|
|
1690
|
+
})
|
|
1679
1691
|
)
|
|
1680
1692
|
}
|
|
1681
1693
|
|
|
@@ -1696,7 +1708,7 @@ function AdvancedField({ id, label, hint, value, type, onChange }) {
|
|
|
1696
1708
|
})
|
|
1697
1709
|
}
|
|
1698
1710
|
|
|
1699
|
-
function AdvancedSection({ draft, onStage,
|
|
1711
|
+
function AdvancedSection({ draft, onStage, t }) {
|
|
1700
1712
|
const current = draft || {}
|
|
1701
1713
|
const numeric = (key, minimum) => (raw) => {
|
|
1702
1714
|
if (raw === '') return
|
|
@@ -1750,6 +1762,22 @@ function AdvancedSection({ draft, onStage, transport, t }) {
|
|
|
1750
1762
|
type: 'number',
|
|
1751
1763
|
onChange: numeric('maxPayloadMb', 0),
|
|
1752
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
|
+
}),
|
|
1753
1781
|
React.createElement(AdvancedField, {
|
|
1754
1782
|
id: 'cb-stats-path',
|
|
1755
1783
|
label: t('settings.stats_path'),
|
|
@@ -1763,22 +1791,6 @@ function AdvancedSection({ draft, onStage, transport, t }) {
|
|
|
1763
1791
|
hint: t('settings.models_cache_path_hint'),
|
|
1764
1792
|
value: current.modelsCachePath,
|
|
1765
1793
|
onChange: (value) => onStage({ modelsCachePath: value }),
|
|
1766
|
-
}),
|
|
1767
|
-
React.createElement(SettingRow, {
|
|
1768
|
-
title: t('settings.transport'),
|
|
1769
|
-
description: t('settings.transport_hint'),
|
|
1770
|
-
htmlFor: 'cb-transport',
|
|
1771
|
-
control: React.createElement(
|
|
1772
|
-
'select',
|
|
1773
|
-
{
|
|
1774
|
-
id: 'cb-transport',
|
|
1775
|
-
className: 'cb-select',
|
|
1776
|
-
value: current.transport === 'pi-ai' ? 'pi-ai' : 'adapter',
|
|
1777
|
-
onChange: (event) => onStage({ transport: event.target.value }),
|
|
1778
|
-
},
|
|
1779
|
-
React.createElement('option', { value: 'adapter' }, 'Native adapter'),
|
|
1780
|
-
React.createElement('option', { value: 'pi-ai' }, 'Legacy llm-pi-ai + proxy')
|
|
1781
|
-
),
|
|
1782
1794
|
})
|
|
1783
1795
|
)
|
|
1784
1796
|
}
|
|
@@ -2493,6 +2505,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2493
2505
|
percentUsed: usage.windows.fiveHour.percentUsed || 0,
|
|
2494
2506
|
remainingPercent: usage.windows.fiveHour.remainingPercent || 100,
|
|
2495
2507
|
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
2508
|
+
windowMs: 5 * 60 * 60 * 1000,
|
|
2496
2509
|
t,
|
|
2497
2510
|
})
|
|
2498
2511
|
: null,
|
|
@@ -2501,6 +2514,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2501
2514
|
percentUsed: usage?.windows?.weekly?.percentUsed || 0,
|
|
2502
2515
|
remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
|
|
2503
2516
|
resetsAt: usage?.windows?.weekly?.resetsAt,
|
|
2517
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
2504
2518
|
t,
|
|
2505
2519
|
}),
|
|
2506
2520
|
usage?.windows?.monthly
|
|
@@ -2509,6 +2523,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
2509
2523
|
percentUsed: usage.windows.monthly.percentUsed || 0,
|
|
2510
2524
|
remainingPercent: usage.windows.monthly.remainingPercent || 100,
|
|
2511
2525
|
resetsAt: usage.windows.monthly.resetsAt,
|
|
2526
|
+
windowMs: 30 * 24 * 60 * 60 * 1000,
|
|
2512
2527
|
t,
|
|
2513
2528
|
})
|
|
2514
2529
|
: null
|
|
@@ -3605,42 +3620,12 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
3605
3620
|
}
|
|
3606
3621
|
|
|
3607
3622
|
// --- src/client/components/diag-section.js ---
|
|
3608
|
-
function DiagSection({
|
|
3609
|
-
keyPresent,
|
|
3610
|
-
isRegistered,
|
|
3611
|
-
busy,
|
|
3612
|
-
smokeResult,
|
|
3613
|
-
handleSmoke,
|
|
3614
|
-
handleUnregister,
|
|
3615
|
-
handleRegister,
|
|
3616
|
-
proxyMode,
|
|
3617
|
-
onToggleProxy,
|
|
3618
|
-
t,
|
|
3619
|
-
}) {
|
|
3623
|
+
function DiagSection({ keyPresent, busy, smokeResult, handleSmoke, t }) {
|
|
3620
3624
|
return React.createElement(
|
|
3621
3625
|
'section',
|
|
3622
3626
|
{ className: 'cb-section-card' },
|
|
3623
3627
|
React.createElement('div', { className: 'cb-section-title' }, t('diag.title')),
|
|
3624
3628
|
React.createElement('div', { className: 'cb-section-desc' }, t('diag.desc')),
|
|
3625
|
-
React.createElement(SettingRow, {
|
|
3626
|
-
title: t('diag.proxy_title'),
|
|
3627
|
-
description: t('diag.proxy_desc'),
|
|
3628
|
-
control: React.createElement(
|
|
3629
|
-
React.Fragment,
|
|
3630
|
-
null,
|
|
3631
|
-
React.createElement(
|
|
3632
|
-
'span',
|
|
3633
|
-
{ className: proxyMode ? 'cb-state-ok' : 'cb-muted' },
|
|
3634
|
-
proxyMode ? t('diag.proxy_enabled') : t('diag.proxy_disabled')
|
|
3635
|
-
),
|
|
3636
|
-
React.createElement(BooleanControl, {
|
|
3637
|
-
checked: !!proxyMode,
|
|
3638
|
-
disabled: !!busy,
|
|
3639
|
-
label: t('diag.proxy_title'),
|
|
3640
|
-
onChange: (next) => onToggleProxy && onToggleProxy(next),
|
|
3641
|
-
})
|
|
3642
|
-
),
|
|
3643
|
-
}),
|
|
3644
3629
|
React.createElement(
|
|
3645
3630
|
'div',
|
|
3646
3631
|
{ className: 'cb-row cb-row-actions' },
|
|
@@ -3648,34 +3633,11 @@ function DiagSection({
|
|
|
3648
3633
|
'button',
|
|
3649
3634
|
{
|
|
3650
3635
|
type: 'button',
|
|
3651
|
-
className: 'cb-btn',
|
|
3636
|
+
className: 'cb-btn cb-btn-primary',
|
|
3652
3637
|
disabled: !!busy || !keyPresent,
|
|
3653
3638
|
onClick: handleSmoke,
|
|
3654
3639
|
},
|
|
3655
3640
|
busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
|
|
3656
|
-
),
|
|
3657
|
-
isRegistered
|
|
3658
|
-
? React.createElement(
|
|
3659
|
-
'button',
|
|
3660
|
-
{
|
|
3661
|
-
type: 'button',
|
|
3662
|
-
className: 'cb-btn cb-btn-danger',
|
|
3663
|
-
disabled: !!busy,
|
|
3664
|
-
onClick: handleUnregister,
|
|
3665
|
-
},
|
|
3666
|
-
busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
|
|
3667
|
-
)
|
|
3668
|
-
: null,
|
|
3669
|
-
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
3670
|
-
React.createElement(
|
|
3671
|
-
'button',
|
|
3672
|
-
{
|
|
3673
|
-
type: 'button',
|
|
3674
|
-
className: 'cb-btn cb-btn-primary',
|
|
3675
|
-
disabled: !!busy,
|
|
3676
|
-
onClick: handleRegister,
|
|
3677
|
-
},
|
|
3678
|
-
busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
|
|
3679
3641
|
)
|
|
3680
3642
|
),
|
|
3681
3643
|
smokeResult
|
|
@@ -3882,7 +3844,14 @@ function SidebarQuota({ ctx, wide, t: propT, open: openPanel }) {
|
|
|
3882
3844
|
className: warn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
|
|
3883
3845
|
style: { width: hasData ? `${Math.min(100, Math.max(2, value))}%` : '0%' },
|
|
3884
3846
|
})
|
|
3885
|
-
)
|
|
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
|
|
3886
3855
|
)
|
|
3887
3856
|
}),
|
|
3888
3857
|
rows.some((window) => windowPct(window) !== null)
|
|
@@ -4170,6 +4139,7 @@ function QuotaPanelView({
|
|
|
4170
4139
|
percentUsed: selectedUsage.windows.fiveHour.percentUsed || 0,
|
|
4171
4140
|
remainingPercent: selectedUsage.windows.fiveHour.remainingPercent || 100,
|
|
4172
4141
|
resetsAt: selectedUsage.windows.fiveHour.resetsAt,
|
|
4142
|
+
windowMs: 5 * 60 * 60 * 1000,
|
|
4173
4143
|
t,
|
|
4174
4144
|
})
|
|
4175
4145
|
: null,
|
|
@@ -4179,6 +4149,7 @@ function QuotaPanelView({
|
|
|
4179
4149
|
percentUsed: selectedUsage.windows.weekly.percentUsed || 0,
|
|
4180
4150
|
remainingPercent: selectedUsage.windows.weekly.remainingPercent || 100,
|
|
4181
4151
|
resetsAt: selectedUsage.windows.weekly.resetsAt,
|
|
4152
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
4182
4153
|
t,
|
|
4183
4154
|
})
|
|
4184
4155
|
: null,
|
|
@@ -4188,6 +4159,7 @@ function QuotaPanelView({
|
|
|
4188
4159
|
percentUsed: selectedUsage.windows.monthly.percentUsed || 0,
|
|
4189
4160
|
remainingPercent: selectedUsage.windows.monthly.remainingPercent || 100,
|
|
4190
4161
|
resetsAt: selectedUsage.windows.monthly.resetsAt,
|
|
4162
|
+
windowMs: 30 * 24 * 60 * 60 * 1000,
|
|
4191
4163
|
t,
|
|
4192
4164
|
})
|
|
4193
4165
|
: null,
|
|
@@ -5123,27 +5095,6 @@ function SettingsPage(props) {
|
|
|
5123
5095
|
})
|
|
5124
5096
|
}
|
|
5125
5097
|
|
|
5126
|
-
async function handleRegister() {
|
|
5127
|
-
await performAction('register', async () => {
|
|
5128
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/register`, { method: 'POST' })
|
|
5129
|
-
const data = await res.json().catch(() => ({}))
|
|
5130
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
5131
|
-
const count = status?.availableModels?.filter((m) => !(draft?.disabledModels || []).includes(m.id)).length || 0
|
|
5132
|
-
setMsg(t('diag.resynced_msg', { count }))
|
|
5133
|
-
await load()
|
|
5134
|
-
})
|
|
5135
|
-
}
|
|
5136
|
-
|
|
5137
|
-
async function handleUnregister() {
|
|
5138
|
-
await performAction('unregister', async () => {
|
|
5139
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
|
|
5140
|
-
const data = await res.json().catch(() => ({}))
|
|
5141
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
5142
|
-
setMsg(t('diag.unregistered_msg'))
|
|
5143
|
-
await load()
|
|
5144
|
-
})
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
5098
|
async function handleSmoke() {
|
|
5148
5099
|
setSmokeResult(null)
|
|
5149
5100
|
await performAction('smoke', async () => {
|
|
@@ -5372,28 +5323,21 @@ function SettingsPage(props) {
|
|
|
5372
5323
|
React.createElement(IntegrationsSection, {
|
|
5373
5324
|
draft,
|
|
5374
5325
|
onStage: staged.stage,
|
|
5375
|
-
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5376
5326
|
t,
|
|
5377
5327
|
}),
|
|
5378
5328
|
|
|
5379
5329
|
// Diagnostics & Sync with DSH
|
|
5380
5330
|
React.createElement(DiagSection, {
|
|
5381
5331
|
keyPresent,
|
|
5382
|
-
isRegistered,
|
|
5383
5332
|
busy,
|
|
5384
5333
|
smokeResult,
|
|
5385
5334
|
handleSmoke,
|
|
5386
|
-
handleUnregister,
|
|
5387
|
-
handleRegister,
|
|
5388
|
-
proxyMode: draft?.proxyMode !== false,
|
|
5389
|
-
onToggleProxy: (value) => staged.stage({ proxyMode: value }),
|
|
5390
5335
|
t,
|
|
5391
5336
|
}),
|
|
5392
5337
|
|
|
5393
5338
|
React.createElement(AdvancedSection, {
|
|
5394
5339
|
draft,
|
|
5395
5340
|
onStage: staged.stage,
|
|
5396
|
-
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5397
5341
|
t,
|
|
5398
5342
|
}),
|
|
5399
5343
|
|
package/lib/cline-client.js
CHANGED
|
@@ -18,9 +18,7 @@ import {
|
|
|
18
18
|
DEFAULT_MODEL_ID,
|
|
19
19
|
PROVIDER_ID,
|
|
20
20
|
PROVIDER_DISPLAY_NAME,
|
|
21
|
-
getAllModels,
|
|
22
21
|
parsePlanIncludedModels,
|
|
23
|
-
formatModelDescription,
|
|
24
22
|
} from './models.js'
|
|
25
23
|
|
|
26
24
|
export const DEFAULT_BASE_URL = 'https://api.cline.bot/api/v1'
|
|
@@ -487,103 +485,3 @@ export async function smokeChat(baseUrl, apiKey, {
|
|
|
487
485
|
cancel()
|
|
488
486
|
}
|
|
489
487
|
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Shape for llm-pi-ai.providers.clinebot (openai-completions).
|
|
493
|
-
*/
|
|
494
|
-
export function buildPiAiProvider({
|
|
495
|
-
baseUrl = DEFAULT_BASE_URL,
|
|
496
|
-
apiKeyEnv = DEFAULT_API_KEY_ENV,
|
|
497
|
-
apiKey,
|
|
498
|
-
dynamicModels = [],
|
|
499
|
-
modelContextOverrides = [],
|
|
500
|
-
models = [],
|
|
501
|
-
customModels = [],
|
|
502
|
-
existingModels = [],
|
|
503
|
-
displayName = PROVIDER_DISPLAY_NAME,
|
|
504
|
-
} = {}) {
|
|
505
|
-
const allAvailable = getAllModels(dynamicModels, modelContextOverrides, customModels)
|
|
506
|
-
const customModelsMap = new Map((Array.isArray(customModels) ? customModels : []).filter(Boolean).map(cm => [cm.id, cm]))
|
|
507
|
-
const existingMap = new Map((Array.isArray(existingModels) ? existingModels : []).filter(Boolean).map(m => [m.id, m]))
|
|
508
|
-
const explicitOverridesSet = new Set((Array.isArray(modelContextOverrides) ? modelContextOverrides : []).map(o => o?.modelId))
|
|
509
|
-
const hasWildcardOverride = explicitOverridesSet.has('*')
|
|
510
|
-
|
|
511
|
-
const modelList = (Array.isArray(models) && models.length ? models : allAvailable).map((m) => {
|
|
512
|
-
let item
|
|
513
|
-
if (typeof m === 'string') {
|
|
514
|
-
const customMatch = customModelsMap.get(m)
|
|
515
|
-
const baseMatch = allAvailable.find((x) => x.id === m)
|
|
516
|
-
item = customMatch ? { ...baseMatch, ...customMatch } : (baseMatch || { id: m, name: m })
|
|
517
|
-
} else {
|
|
518
|
-
item = m
|
|
519
|
-
}
|
|
520
|
-
const hasImage = item.input?.includes('image') || item.input?.includes('vision')
|
|
521
|
-
|
|
522
|
-
// Preserve manual provider edits if user configured contextWindow or maxTokens in llm-pi-ai without explicit override in clinebot
|
|
523
|
-
const existingEntry = existingMap.get(item.id)
|
|
524
|
-
let ctxWin = Number(item.contextLength || item.contextWindow) || 200000
|
|
525
|
-
let maxTok = Number(item.maxTokens) || 8192
|
|
526
|
-
if (existingEntry && !explicitOverridesSet.has(item.id) && !hasWildcardOverride) {
|
|
527
|
-
if (typeof existingEntry.contextWindow === 'number' && existingEntry.contextWindow > 0) {
|
|
528
|
-
ctxWin = existingEntry.contextWindow
|
|
529
|
-
}
|
|
530
|
-
if (typeof existingEntry.maxTokens === 'number' && existingEntry.maxTokens > 0) {
|
|
531
|
-
maxTok = existingEntry.maxTokens
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const res = {
|
|
536
|
-
id: item.id,
|
|
537
|
-
name: item.name || item.id,
|
|
538
|
-
description: formatModelDescription(item),
|
|
539
|
-
contextWindow: ctxWin,
|
|
540
|
-
maxTokens: maxTok,
|
|
541
|
-
input: hasImage ? ['text', 'image'] : ['text'],
|
|
542
|
-
provider: PROVIDER_ID,
|
|
543
|
-
}
|
|
544
|
-
const validEfforts = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
545
|
-
let efforts = null
|
|
546
|
-
if (Array.isArray(item.reasoningEfforts) && item.reasoningEfforts.length) {
|
|
547
|
-
efforts = {}
|
|
548
|
-
for (const effort of item.reasoningEfforts) {
|
|
549
|
-
const key = String(effort).trim().toLowerCase()
|
|
550
|
-
if (validEfforts.includes(key)) efforts[key] = key === 'off' ? null : key
|
|
551
|
-
}
|
|
552
|
-
} else if (item.reasoningEfforts && typeof item.reasoningEfforts === 'object' && !Array.isArray(item.reasoningEfforts)) {
|
|
553
|
-
efforts = {}
|
|
554
|
-
for (const [k, v] of Object.entries(item.reasoningEfforts)) {
|
|
555
|
-
const key = String(k).trim().toLowerCase()
|
|
556
|
-
if (validEfforts.includes(key)) efforts[key] = key === 'off' ? (v === undefined ? null : v) : (typeof v === 'string' && v.trim() ? v.trim() : key)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
const modelCompat = {}
|
|
560
|
-
if (efforts && Object.keys(efforts).length) {
|
|
561
|
-
if (!('off' in efforts)) efforts.off = null
|
|
562
|
-
res.reasoningEfforts = efforts
|
|
563
|
-
modelCompat.supportsReasoningEffort = true
|
|
564
|
-
} else {
|
|
565
|
-
res.reasoningEfforts = false
|
|
566
|
-
}
|
|
567
|
-
if (Object.keys(modelCompat).length > 0) {
|
|
568
|
-
res.compat = modelCompat
|
|
569
|
-
}
|
|
570
|
-
return res
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
return {
|
|
574
|
-
displayName,
|
|
575
|
-
api: 'openai-completions',
|
|
576
|
-
baseURL: normalizeBaseUrl(baseUrl),
|
|
577
|
-
apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
578
|
-
cacheRetention: 'long',
|
|
579
|
-
compat: {
|
|
580
|
-
supportsReasoningEffort: true,
|
|
581
|
-
supportsLongCacheRetention: true,
|
|
582
|
-
},
|
|
583
|
-
models: modelList,
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
package/lib/config.js
CHANGED
|
@@ -29,8 +29,6 @@ export const LLM_PI_AI_NS = 'llm-pi-ai'
|
|
|
29
29
|
export const Config = z.object({
|
|
30
30
|
enabled: z.boolean().default(true)
|
|
31
31
|
.description('When true, ClineBot is registered as a model provider in DSH.').volatile(),
|
|
32
|
-
transport: z.union(['adapter', 'pi-ai']).default('adapter')
|
|
33
|
-
.description('Provider transport: native ctx.llm adapter (recommended) or legacy llm-pi-ai + loopback proxy.').volatile(),
|
|
34
32
|
baseUrl: z.string().default(DEFAULT_BASE_URL)
|
|
35
33
|
.description('Base API URL (default: https://api.cline.bot/api/v1).').volatile(),
|
|
36
34
|
apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV)
|
|
@@ -76,8 +74,6 @@ export const Config = z.object({
|
|
|
76
74
|
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
77
75
|
planSyncedAt: z.number().default(0)
|
|
78
76
|
.description('Timestamp when models were last synchronized with active subscription plan.'),
|
|
79
|
-
legacyMigratedAt: z.number().default(0)
|
|
80
|
-
.description('Internal timestamp of the one-time llm-pi-ai provider migration.'),
|
|
81
77
|
modelContextOverrides: z.array(z.object({
|
|
82
78
|
modelId: z.string(),
|
|
83
79
|
contextLength: z.number().default(200000),
|
|
@@ -101,10 +97,12 @@ export const Config = z.object({
|
|
|
101
97
|
isReasoning: z.boolean().default(false),
|
|
102
98
|
})).default([])
|
|
103
99
|
.description('User-defined custom gateway models.').volatile(),
|
|
104
|
-
proxyMode: z.boolean().default(true)
|
|
105
|
-
.description('When true, registers local transparent loopback proxy for failover and token tracking.').volatile(),
|
|
106
100
|
statsPath: z.string().default('~/.dsh/clinebot-stats.json')
|
|
107
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(),
|
|
108
106
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
109
107
|
.description('HTTP probe timeout in milliseconds.').volatile(),
|
|
110
108
|
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
@@ -229,7 +227,6 @@ export function publicConfig(cfg) {
|
|
|
229
227
|
|
|
230
228
|
return {
|
|
231
229
|
enabled: !!cfg?.enabled,
|
|
232
|
-
transport: cfg?.transport === 'pi-ai' ? 'pi-ai' : 'adapter',
|
|
233
230
|
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
234
231
|
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
235
232
|
defaultModel,
|
|
@@ -244,8 +241,9 @@ export function publicConfig(cfg) {
|
|
|
244
241
|
modelReasoningDefaults: cfg?.modelReasoningDefaults && typeof cfg.modelReasoningDefaults === 'object' ? cfg.modelReasoningDefaults : {},
|
|
245
242
|
modelRoutes: cfg?.modelRoutes && typeof cfg.modelRoutes === 'object' ? cfg.modelRoutes : {},
|
|
246
243
|
customModels: custom,
|
|
247
|
-
proxyMode: cfg?.proxyMode !== false,
|
|
248
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),
|
|
249
247
|
disabledModels: disabledList,
|
|
250
248
|
filterModelsByPlan,
|
|
251
249
|
visibleModels,
|