@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/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 or force re-sync.',
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.proxy_mode': 'Transparent loopback proxy', 'settings.proxy_mode_hint': 'Legacy transport only: route requests through the local failover proxy.',
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.proxy_mode': '透明回环代理', 'settings.proxy_mode_hint': '仅旧传输:通过本地故障转移代理发送请求。',
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 ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
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, transport, t }) {
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, transport, t }) {
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
 
@@ -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,