@goodandready/dsh-clinebot 0.6.0 → 0.6.2
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 +17 -0
- package/README.md +6 -14
- package/README.ru.md +2 -13
- package/README.zh.md +2 -13
- package/lib/client.js +99 -157
- package/lib/cline-client.js +0 -102
- package/lib/config.js +0 -8
- package/lib/index.js +7 -201
- package/lib/provider-sync.js +2 -141
- package/lib/routes/accounts.js +1 -37
- package/lib/routes/settings.js +19 -1
- package/lib/stats-storage.js +16 -1
- package/lib/transport/cline-transport.js +1 -1
- 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
|
@@ -206,19 +206,9 @@ const en = {
|
|
|
206
206
|
'stats.confirm_reset': 'Confirm Reset?',
|
|
207
207
|
'stats.cancel_reset': 'Cancel',
|
|
208
208
|
'diag.title': 'Diagnostics & Auto-Registration',
|
|
209
|
-
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test
|
|
209
|
+
'diag.desc': 'ClineBot registers into DSH Models automatically when a key is configured. You can run a connection smoke test here.',
|
|
210
210
|
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
211
211
|
'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
212
|
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
223
213
|
'update.btn': 'Update Now',
|
|
224
214
|
'update.updating': 'Updating…',
|
|
@@ -241,7 +231,9 @@ const en = {
|
|
|
241
231
|
'dashboard.account_pool': 'Active Routing Account',
|
|
242
232
|
'dashboard.credits': 'Credit Balance',
|
|
243
233
|
'dashboard.updated_at': 'Updated at',
|
|
244
|
-
'composer.cost_title': '
|
|
234
|
+
'composer.cost_title': 'ClineBot Session Cost',
|
|
235
|
+
'composer.last_request': 'Last request',
|
|
236
|
+
'composer.free': 'Free',
|
|
245
237
|
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
246
238
|
'composer.prompt_tok': 'Prompt Tokens',
|
|
247
239
|
'composer.completion_tok': 'Completion Tokens',
|
|
@@ -260,13 +252,11 @@ const en = {
|
|
|
260
252
|
'settings.web_search': 'Serve web search with ClineBot', 'settings.web_search_hint': 'Use the ClinePass endpoint for the DSH web_search tool.',
|
|
261
253
|
'settings.sidebar_quota': 'Show quota meter in the sidebar', 'settings.sidebar_quota_hint': 'Display the compact plan and rolling-window meter above Settings.',
|
|
262
254
|
'settings.composer_cost': 'Show session cost in the composer', 'settings.composer_cost_hint': 'Display the estimated token cost beside the composer.',
|
|
263
|
-
'settings.
|
|
264
|
-
'settings.advanced': 'Advanced', 'settings.advanced_desc': 'Connection, timeouts, storage paths and the temporary transport switch.',
|
|
255
|
+
'settings.advanced': 'Advanced', 'settings.advanced_desc': 'Connection, timeouts and storage paths.',
|
|
265
256
|
'settings.base_url': 'API base URL', 'settings.base_url_hint': 'ClinePass API root. Remote endpoints must use https://.',
|
|
266
257
|
'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.',
|
|
267
258
|
'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.',
|
|
268
259
|
'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 ~/).',
|
|
269
|
-
'settings.transport': 'Transport', 'settings.transport_hint': 'Native ctx.llm adapter (recommended) or the legacy llm-pi-ai + loopback proxy.',
|
|
270
260
|
'providerCard.title': 'ClineBot (ClinePass)',
|
|
271
261
|
'providerCard.hint': 'Paste a ClinePass API key or manage the full account pool in Settings → ClineBot.',
|
|
272
262
|
'providerCard.keyPresent': 'Key configured',
|
|
@@ -493,19 +483,9 @@ const zh = {
|
|
|
493
483
|
'stats.confirm_reset': '确认重置?',
|
|
494
484
|
'stats.cancel_reset': '取消',
|
|
495
485
|
'diag.title': '诊断工具与服务接入',
|
|
496
|
-
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH
|
|
486
|
+
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试。',
|
|
497
487
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
498
488
|
'diag.smoke_testing': '正在测试…',
|
|
499
|
-
'diag.resync_btn': '强制重新同步到 DSH',
|
|
500
|
-
'diag.resyncing': '正在同步…',
|
|
501
|
-
'diag.resynced_msg': '模型服务已重新同步到 DSH ({count} 个模型)',
|
|
502
|
-
'diag.unregister_btn': '从 DSH 注销该服务',
|
|
503
|
-
'diag.unregistering': '正在注销…',
|
|
504
|
-
'diag.unregistered_msg': '已从 DSH 模型注册表中移除',
|
|
505
|
-
'diag.proxy_title': '本地透明回环代理',
|
|
506
|
-
'diag.proxy_desc': '通过本地回环代理支持多账户故障转移、会话保持与实时统计。关闭后直连上游端点。',
|
|
507
|
-
'diag.proxy_enabled': '代理模式已开启(支持故障转移与遥测)',
|
|
508
|
-
'diag.proxy_disabled': '直连模式(直接连接上游,无本地代理)',
|
|
509
489
|
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
510
490
|
'update.btn': '立即更新',
|
|
511
491
|
'update.updating': '正在更新…',
|
|
@@ -528,7 +508,9 @@ const zh = {
|
|
|
528
508
|
'dashboard.account_pool': '当前承载账户',
|
|
529
509
|
'dashboard.credits': '点数余额',
|
|
530
510
|
'dashboard.updated_at': '更新于',
|
|
531
|
-
'composer.cost_title': '
|
|
511
|
+
'composer.cost_title': 'ClineBot 会话成本',
|
|
512
|
+
'composer.last_request': '最近一次请求',
|
|
513
|
+
'composer.free': '免费',
|
|
532
514
|
'composer.cost_breakdown': 'Token 与费用明细',
|
|
533
515
|
'composer.prompt_tok': '提示词 Token',
|
|
534
516
|
'composer.completion_tok': '补全 Token',
|
|
@@ -547,13 +529,11 @@ const zh = {
|
|
|
547
529
|
'settings.web_search': '使用 ClineBot 提供网页搜索', 'settings.web_search_hint': '为 DSH web_search 工具使用 ClinePass 端点。',
|
|
548
530
|
'settings.sidebar_quota': '在侧边栏显示配额', 'settings.sidebar_quota_hint': '在设置上方显示紧凑的套餐与滚动窗口配额。',
|
|
549
531
|
'settings.composer_cost': '在输入框显示会话成本', 'settings.composer_cost_hint': '在输入框旁显示预估 token 成本。',
|
|
550
|
-
'settings.
|
|
551
|
-
'settings.advanced': '高级', 'settings.advanced_desc': '连接、超时、存储路径和临时传输开关。',
|
|
532
|
+
'settings.advanced': '高级', 'settings.advanced_desc': '连接、超时和存储路径。',
|
|
552
533
|
'settings.base_url': 'API 基础地址', 'settings.base_url_hint': 'ClinePass API 根地址。远程端点必须使用 https://。',
|
|
553
534
|
'settings.timeout': '连接超时(毫秒)', 'settings.timeout_hint': '等待上游响应头的最大时间。', 'settings.smoke_timeout': '烟雾测试超时(毫秒)', 'settings.smoke_timeout_hint': '等待诊断聊天补全的最大时间。',
|
|
554
535
|
'settings.stream_idle': '流空闲超时(毫秒)', 'settings.stream_idle_hint': 'SSE 数据块之间的最大间隔,超过后视为断流。', 'settings.max_payload': '多模态负载上限(MB)', 'settings.max_payload_hint': '请求超过预算时会淘汰最旧的图片。',
|
|
555
536
|
'settings.stats_path': '统计文件路径', 'settings.stats_path_hint': '持久化 JSON 遥测路径(支持 ~/)。', 'settings.models_cache_path': '模型缓存路径', 'settings.models_cache_path_hint': '离线模型目录缓存(支持 ~/)。',
|
|
556
|
-
'settings.transport': '传输方式', 'settings.transport_hint': '原生 ctx.llm 适配器(推荐)或旧版 llm-pi-ai + 回环代理。',
|
|
557
537
|
'providerCard.title': 'ClineBot(ClinePass)',
|
|
558
538
|
'providerCard.hint': '粘贴 ClinePass API 密钥,或前往“设置 → ClineBot”管理完整账户池。',
|
|
559
539
|
'providerCard.keyPresent': '密钥已配置',
|
|
@@ -1633,7 +1613,7 @@ function IntegrationToggle({ id, label, hint, checked, onChange }) {
|
|
|
1633
1613
|
})
|
|
1634
1614
|
}
|
|
1635
1615
|
|
|
1636
|
-
function IntegrationsSection({ draft, onStage,
|
|
1616
|
+
function IntegrationsSection({ draft, onStage, t }) {
|
|
1637
1617
|
const current = draft || {}
|
|
1638
1618
|
return React.createElement(
|
|
1639
1619
|
SettingsGroup,
|
|
@@ -1662,16 +1642,7 @@ function IntegrationsSection({ draft, onStage, transport, t }) {
|
|
|
1662
1642
|
hint: t('settings.composer_cost_hint'),
|
|
1663
1643
|
checked: current.showComposerCost !== false,
|
|
1664
1644
|
onChange: (value) => onStage({ showComposerCost: value }),
|
|
1665
|
-
})
|
|
1666
|
-
transport === 'pi-ai'
|
|
1667
|
-
? React.createElement(IntegrationToggle, {
|
|
1668
|
-
id: 'cb-proxy-mode',
|
|
1669
|
-
label: t('settings.proxy_mode'),
|
|
1670
|
-
hint: t('settings.proxy_mode_hint'),
|
|
1671
|
-
checked: current.proxyMode !== false,
|
|
1672
|
-
onChange: (value) => onStage({ proxyMode: value }),
|
|
1673
|
-
})
|
|
1674
|
-
: null
|
|
1645
|
+
})
|
|
1675
1646
|
)
|
|
1676
1647
|
}
|
|
1677
1648
|
|
|
@@ -1692,7 +1663,7 @@ function AdvancedField({ id, label, hint, value, type, onChange }) {
|
|
|
1692
1663
|
})
|
|
1693
1664
|
}
|
|
1694
1665
|
|
|
1695
|
-
function AdvancedSection({ draft, onStage,
|
|
1666
|
+
function AdvancedSection({ draft, onStage, t }) {
|
|
1696
1667
|
const current = draft || {}
|
|
1697
1668
|
const numeric = (key, minimum) => (raw) => {
|
|
1698
1669
|
if (raw === '') return
|
|
@@ -1759,22 +1730,6 @@ function AdvancedSection({ draft, onStage, transport, t }) {
|
|
|
1759
1730
|
hint: t('settings.models_cache_path_hint'),
|
|
1760
1731
|
value: current.modelsCachePath,
|
|
1761
1732
|
onChange: (value) => onStage({ modelsCachePath: value }),
|
|
1762
|
-
}),
|
|
1763
|
-
React.createElement(SettingRow, {
|
|
1764
|
-
title: t('settings.transport'),
|
|
1765
|
-
description: t('settings.transport_hint'),
|
|
1766
|
-
htmlFor: 'cb-transport',
|
|
1767
|
-
control: React.createElement(
|
|
1768
|
-
'select',
|
|
1769
|
-
{
|
|
1770
|
-
id: 'cb-transport',
|
|
1771
|
-
className: 'cb-select',
|
|
1772
|
-
value: current.transport === 'pi-ai' ? 'pi-ai' : 'adapter',
|
|
1773
|
-
onChange: (event) => onStage({ transport: event.target.value }),
|
|
1774
|
-
},
|
|
1775
|
-
React.createElement('option', { value: 'adapter' }, 'Native adapter'),
|
|
1776
|
-
React.createElement('option', { value: 'pi-ai' }, 'Legacy llm-pi-ai + proxy')
|
|
1777
|
-
),
|
|
1778
1733
|
})
|
|
1779
1734
|
)
|
|
1780
1735
|
}
|
|
@@ -3601,42 +3556,12 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
3601
3556
|
}
|
|
3602
3557
|
|
|
3603
3558
|
// --- src/client/components/diag-section.js ---
|
|
3604
|
-
function DiagSection({
|
|
3605
|
-
keyPresent,
|
|
3606
|
-
isRegistered,
|
|
3607
|
-
busy,
|
|
3608
|
-
smokeResult,
|
|
3609
|
-
handleSmoke,
|
|
3610
|
-
handleUnregister,
|
|
3611
|
-
handleRegister,
|
|
3612
|
-
proxyMode,
|
|
3613
|
-
onToggleProxy,
|
|
3614
|
-
t,
|
|
3615
|
-
}) {
|
|
3559
|
+
function DiagSection({ keyPresent, busy, smokeResult, handleSmoke, t }) {
|
|
3616
3560
|
return React.createElement(
|
|
3617
3561
|
'section',
|
|
3618
3562
|
{ className: 'cb-section-card' },
|
|
3619
3563
|
React.createElement('div', { className: 'cb-section-title' }, t('diag.title')),
|
|
3620
3564
|
React.createElement('div', { className: 'cb-section-desc' }, t('diag.desc')),
|
|
3621
|
-
React.createElement(SettingRow, {
|
|
3622
|
-
title: t('diag.proxy_title'),
|
|
3623
|
-
description: t('diag.proxy_desc'),
|
|
3624
|
-
control: React.createElement(
|
|
3625
|
-
React.Fragment,
|
|
3626
|
-
null,
|
|
3627
|
-
React.createElement(
|
|
3628
|
-
'span',
|
|
3629
|
-
{ className: proxyMode ? 'cb-state-ok' : 'cb-muted' },
|
|
3630
|
-
proxyMode ? t('diag.proxy_enabled') : t('diag.proxy_disabled')
|
|
3631
|
-
),
|
|
3632
|
-
React.createElement(BooleanControl, {
|
|
3633
|
-
checked: !!proxyMode,
|
|
3634
|
-
disabled: !!busy,
|
|
3635
|
-
label: t('diag.proxy_title'),
|
|
3636
|
-
onChange: (next) => onToggleProxy && onToggleProxy(next),
|
|
3637
|
-
})
|
|
3638
|
-
),
|
|
3639
|
-
}),
|
|
3640
3565
|
React.createElement(
|
|
3641
3566
|
'div',
|
|
3642
3567
|
{ className: 'cb-row cb-row-actions' },
|
|
@@ -3644,34 +3569,11 @@ function DiagSection({
|
|
|
3644
3569
|
'button',
|
|
3645
3570
|
{
|
|
3646
3571
|
type: 'button',
|
|
3647
|
-
className: 'cb-btn',
|
|
3572
|
+
className: 'cb-btn cb-btn-primary',
|
|
3648
3573
|
disabled: !!busy || !keyPresent,
|
|
3649
3574
|
onClick: handleSmoke,
|
|
3650
3575
|
},
|
|
3651
3576
|
busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
|
|
3652
|
-
),
|
|
3653
|
-
isRegistered
|
|
3654
|
-
? React.createElement(
|
|
3655
|
-
'button',
|
|
3656
|
-
{
|
|
3657
|
-
type: 'button',
|
|
3658
|
-
className: 'cb-btn cb-btn-danger',
|
|
3659
|
-
disabled: !!busy,
|
|
3660
|
-
onClick: handleUnregister,
|
|
3661
|
-
},
|
|
3662
|
-
busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
|
|
3663
|
-
)
|
|
3664
|
-
: null,
|
|
3665
|
-
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
3666
|
-
React.createElement(
|
|
3667
|
-
'button',
|
|
3668
|
-
{
|
|
3669
|
-
type: 'button',
|
|
3670
|
-
className: 'cb-btn cb-btn-primary',
|
|
3671
|
-
disabled: !!busy,
|
|
3672
|
-
onClick: handleRegister,
|
|
3673
|
-
},
|
|
3674
|
-
busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
|
|
3675
3577
|
)
|
|
3676
3578
|
),
|
|
3677
3579
|
smokeResult
|
|
@@ -4710,10 +4612,12 @@ function ComposerCostEntry(props) {
|
|
|
4710
4612
|
const configSnapshot = useConfigFormsSnapshot(props?.ctx)
|
|
4711
4613
|
const [stats, setStats] = React.useState(null)
|
|
4712
4614
|
const [prices, setPrices] = React.useState([])
|
|
4615
|
+
const [sessionStats, setSessionStats] = React.useState(null)
|
|
4713
4616
|
const markerRef = React.useRef(null)
|
|
4714
4617
|
|
|
4715
4618
|
const projectedTokens = props.useProjection('tokenUsage')
|
|
4716
4619
|
const selection = props.useProjection('modelSelection')
|
|
4620
|
+
const sessionId = String(props?.sessionId || '')
|
|
4717
4621
|
const priceOverridesKey = JSON.stringify(configSnapshot?.value?.priceOverrides || {})
|
|
4718
4622
|
|
|
4719
4623
|
React.useEffect(() => {
|
|
@@ -4728,7 +4632,40 @@ function ComposerCostEntry(props) {
|
|
|
4728
4632
|
}, [priceOverridesKey])
|
|
4729
4633
|
|
|
4730
4634
|
React.useEffect(() => {
|
|
4731
|
-
if (
|
|
4635
|
+
if (!sessionId) {
|
|
4636
|
+
setSessionStats(null)
|
|
4637
|
+
return undefined
|
|
4638
|
+
}
|
|
4639
|
+
let active = true
|
|
4640
|
+
const loadSession = async () => {
|
|
4641
|
+
try {
|
|
4642
|
+
const res = await fetchWithTimeout(
|
|
4643
|
+
`${ROUTE_PREFIX}/stats/session?sessionId=${encodeURIComponent(sessionId)}`,
|
|
4644
|
+
{ cache: 'no-store' },
|
|
4645
|
+
)
|
|
4646
|
+
const data = await res.json().catch(() => null)
|
|
4647
|
+
if (active && data?.ok) setSessionStats(data.session || null)
|
|
4648
|
+
} catch {
|
|
4649
|
+
/* quiet */
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
loadSession()
|
|
4653
|
+
let timer = null
|
|
4654
|
+
const schedule = () => {
|
|
4655
|
+
timer = setTimeout(async () => {
|
|
4656
|
+
await loadSession()
|
|
4657
|
+
if (active) schedule()
|
|
4658
|
+
}, 10000)
|
|
4659
|
+
}
|
|
4660
|
+
schedule()
|
|
4661
|
+
return () => {
|
|
4662
|
+
active = false
|
|
4663
|
+
if (timer) clearTimeout(timer)
|
|
4664
|
+
}
|
|
4665
|
+
}, [sessionId, projectedTokens])
|
|
4666
|
+
|
|
4667
|
+
React.useEffect(() => {
|
|
4668
|
+
if (projectedTokens || sessionId) return undefined
|
|
4732
4669
|
let active = true
|
|
4733
4670
|
const loadStats = async () => {
|
|
4734
4671
|
try {
|
|
@@ -4744,7 +4681,7 @@ function ComposerCostEntry(props) {
|
|
|
4744
4681
|
loadStats()
|
|
4745
4682
|
const timer = setTimeout(loadStats, 30000)
|
|
4746
4683
|
return () => { active = false; clearTimeout(timer) }
|
|
4747
|
-
}, [projectedTokens])
|
|
4684
|
+
}, [projectedTokens, sessionId])
|
|
4748
4685
|
|
|
4749
4686
|
const selected = selection?.lastUsed
|
|
4750
4687
|
?? selection?.next
|
|
@@ -4754,32 +4691,65 @@ function ComposerCostEntry(props) {
|
|
|
4754
4691
|
const isClineBot = (provider === '' || provider === 'clinebot' || provider === 'llm-clinebot')
|
|
4755
4692
|
&& (provider === 'clinebot' || provider === 'llm-clinebot' || modelId.startsWith('cline-pass/'))
|
|
4756
4693
|
|
|
4757
|
-
|
|
4694
|
+
// ClineBot requests are recorded in the plugin's own per-session bucket. That
|
|
4695
|
+
// bucket is provider-scoped, unlike the host's session-wide tokenUsage, so a
|
|
4696
|
+
// session that also used OpenCode cannot inflate the ClineBot read-out.
|
|
4697
|
+
const session = sessionStats && Number(sessionStats.requests || 0) > 0 ? sessionStats : null
|
|
4698
|
+
const sessionTokens = session
|
|
4699
|
+
? {
|
|
4700
|
+
promptTokens: Number(session.promptTokens || 0),
|
|
4701
|
+
completionTokens: Number(session.completionTokens || 0),
|
|
4702
|
+
reasoningTokens: Number(session.reasoningTokens || 0),
|
|
4703
|
+
cacheReadTokens: Number(session.cacheReadTokens || 0),
|
|
4704
|
+
cacheWriteTokens: Number(session.cacheWriteTokens || 0),
|
|
4705
|
+
}
|
|
4706
|
+
: null
|
|
4707
|
+
const rawTokens = sessionTokens || (sessionId ? null : (projectedTokens || {
|
|
4758
4708
|
promptTokens: stats?.promptTokens || 0,
|
|
4759
4709
|
completionTokens: stats?.completionTokens || 0,
|
|
4760
4710
|
reasoningTokens: stats?.reasoningTokens || 0,
|
|
4761
4711
|
cacheReadTokens: stats?.cacheReadTokens || 0,
|
|
4762
4712
|
cacheWriteTokens: stats?.cacheWriteTokens || 0,
|
|
4763
|
-
}
|
|
4764
|
-
const cacheRead = Number(rawTokens
|
|
4765
|
-
const cacheWrite = Number(rawTokens
|
|
4766
|
-
const uncachedInput = Number(rawTokens
|
|
4767
|
-
const promptTokens =
|
|
4768
|
-
? uncachedInput
|
|
4769
|
-
|
|
4713
|
+
}))
|
|
4714
|
+
const cacheRead = Number(rawTokens?.cacheReadTokens || 0)
|
|
4715
|
+
const cacheWrite = Number(rawTokens?.cacheWriteTokens || 0)
|
|
4716
|
+
const uncachedInput = Number(rawTokens?.uncachedInputTokens)
|
|
4717
|
+
const promptTokens = rawTokens
|
|
4718
|
+
? (Number.isFinite(uncachedInput)
|
|
4719
|
+
? uncachedInput + cacheRead + cacheWrite
|
|
4720
|
+
: Number(rawTokens.promptTokens || rawTokens.inputTokens || 0))
|
|
4721
|
+
: 0
|
|
4770
4722
|
const tokens = {
|
|
4771
4723
|
promptTokens,
|
|
4772
|
-
completionTokens: Number(rawTokens
|
|
4773
|
-
reasoningTokens: Number(rawTokens
|
|
4724
|
+
completionTokens: Number(rawTokens?.completionTokens || rawTokens?.outputTokens || 0),
|
|
4725
|
+
reasoningTokens: Number(rawTokens?.reasoningTokens || 0),
|
|
4774
4726
|
cacheReadTokens: cacheRead,
|
|
4775
4727
|
cacheWriteTokens: cacheWrite,
|
|
4776
4728
|
}
|
|
4777
|
-
const
|
|
4778
|
-
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4729
|
+
const calculated = calculateCost(tokens, session?.model || modelId, prices)
|
|
4730
|
+
const cost = session ? Number(session.costUsd || 0) : calculated.cost
|
|
4731
|
+
const priced = session ? Number(session.pricedRequests || 0) > 0 : calculated.priced
|
|
4732
|
+
const source = session
|
|
4733
|
+
? (findPrice(session.model || modelId, prices)?.source || 'estimate')
|
|
4734
|
+
: calculated.source
|
|
4735
|
+
const { pTok, cTok, rTok } = calculated
|
|
4736
|
+
const total = session ? Number(session.totalTokens || pTok + cTok) : calculated.total
|
|
4737
|
+
const last = session?.last || null
|
|
4738
|
+
const lastCost = last ? Number(last.costUsd || 0) : 0
|
|
4739
|
+
const lastPriced = Boolean(last?.priced)
|
|
4740
|
+
|
|
4741
|
+
const costValue = priced
|
|
4742
|
+
? (cost === 0 ? t('composer.free') : (cost < 0.0001 ? '< $0.0001' : `$${cost.toFixed(4)}`))
|
|
4743
|
+
: t('composer.unpriced')
|
|
4744
|
+
const costDisplay = priced && source === 'estimate' && cost > 0 ? `≈ ${costValue}` : costValue
|
|
4745
|
+
const lastCostValue = last
|
|
4746
|
+
? (lastPriced
|
|
4747
|
+
? (lastCost === 0 ? t('composer.free') : (lastCost < 0.0001 ? '< $0.0001' : `$${lastCost.toFixed(4)}`))
|
|
4748
|
+
: t('composer.unpriced'))
|
|
4749
|
+
: ''
|
|
4781
4750
|
const title = [
|
|
4782
4751
|
`${t('composer.cost_title')} ${costDisplay}`,
|
|
4752
|
+
...(last ? [`${t('composer.last_request')} ${lastCostValue}`] : []),
|
|
4783
4753
|
`${t('composer.prompt_tok')} ${formatTokens(pTok)}`,
|
|
4784
4754
|
`${t('composer.completion_tok')} ${formatTokens(cTok)}`,
|
|
4785
4755
|
...(rTok > 0 ? [`${t('composer.reasoning_tok')} ${formatTokens(rTok)}`] : []),
|
|
@@ -5051,27 +5021,6 @@ function SettingsPage(props) {
|
|
|
5051
5021
|
})
|
|
5052
5022
|
}
|
|
5053
5023
|
|
|
5054
|
-
async function handleRegister() {
|
|
5055
|
-
await performAction('register', async () => {
|
|
5056
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/register`, { method: 'POST' })
|
|
5057
|
-
const data = await res.json().catch(() => ({}))
|
|
5058
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
5059
|
-
const count = status?.availableModels?.filter((m) => !(draft?.disabledModels || []).includes(m.id)).length || 0
|
|
5060
|
-
setMsg(t('diag.resynced_msg', { count }))
|
|
5061
|
-
await load()
|
|
5062
|
-
})
|
|
5063
|
-
}
|
|
5064
|
-
|
|
5065
|
-
async function handleUnregister() {
|
|
5066
|
-
await performAction('unregister', async () => {
|
|
5067
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
|
|
5068
|
-
const data = await res.json().catch(() => ({}))
|
|
5069
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
5070
|
-
setMsg(t('diag.unregistered_msg'))
|
|
5071
|
-
await load()
|
|
5072
|
-
})
|
|
5073
|
-
}
|
|
5074
|
-
|
|
5075
5024
|
async function handleSmoke() {
|
|
5076
5025
|
setSmokeResult(null)
|
|
5077
5026
|
await performAction('smoke', async () => {
|
|
@@ -5300,28 +5249,21 @@ function SettingsPage(props) {
|
|
|
5300
5249
|
React.createElement(IntegrationsSection, {
|
|
5301
5250
|
draft,
|
|
5302
5251
|
onStage: staged.stage,
|
|
5303
|
-
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5304
5252
|
t,
|
|
5305
5253
|
}),
|
|
5306
5254
|
|
|
5307
5255
|
// Diagnostics & Sync with DSH
|
|
5308
5256
|
React.createElement(DiagSection, {
|
|
5309
5257
|
keyPresent,
|
|
5310
|
-
isRegistered,
|
|
5311
5258
|
busy,
|
|
5312
5259
|
smokeResult,
|
|
5313
5260
|
handleSmoke,
|
|
5314
|
-
handleUnregister,
|
|
5315
|
-
handleRegister,
|
|
5316
|
-
proxyMode: draft?.proxyMode !== false,
|
|
5317
|
-
onToggleProxy: (value) => staged.stage({ proxyMode: value }),
|
|
5318
5261
|
t,
|
|
5319
5262
|
}),
|
|
5320
5263
|
|
|
5321
5264
|
React.createElement(AdvancedSection, {
|
|
5322
5265
|
draft,
|
|
5323
5266
|
onStage: staged.stage,
|
|
5324
|
-
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5325
5267
|
t,
|
|
5326
5268
|
}),
|
|
5327
5269
|
|
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,8 +97,6 @@ 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(),
|
|
108
102
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
@@ -229,7 +223,6 @@ export function publicConfig(cfg) {
|
|
|
229
223
|
|
|
230
224
|
return {
|
|
231
225
|
enabled: !!cfg?.enabled,
|
|
232
|
-
transport: cfg?.transport === 'pi-ai' ? 'pi-ai' : 'adapter',
|
|
233
226
|
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
234
227
|
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
235
228
|
defaultModel,
|
|
@@ -244,7 +237,6 @@ export function publicConfig(cfg) {
|
|
|
244
237
|
modelReasoningDefaults: cfg?.modelReasoningDefaults && typeof cfg.modelReasoningDefaults === 'object' ? cfg.modelReasoningDefaults : {},
|
|
245
238
|
modelRoutes: cfg?.modelRoutes && typeof cfg.modelRoutes === 'object' ? cfg.modelRoutes : {},
|
|
246
239
|
customModels: custom,
|
|
247
|
-
proxyMode: cfg?.proxyMode !== false,
|
|
248
240
|
statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
|
|
249
241
|
disabledModels: disabledList,
|
|
250
242
|
filterModelsByPlan,
|