@goodandready/dsh-clinebot 0.5.26 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.27] - 2026-10-17
9
+
10
+ ### Added
11
+ - **Visible Models Multi-Select (`visibleModels`) (Issue #228)**: Users can now select an exact subset of models to expose in the DSH chat model picker. Includes search filtering, quick-select buttons (*All*, *Plan only*, *Clean stale*), and badge indicators for genuine context window and max output limits.
12
+ - **Subscription Plan Auto-Filtering (`filterModelsByPlan`) (Issue #228)**: Added toggle to automatically hide models that are not included in the active Cline account's subscription tier, preventing request failures due to tier mismatch.
13
+ - **Visible Models API Endpoint (Issue #228)**: Added `POST /dsh-clinebot/models/visible` for updating visible model selections with input validation.
14
+
15
+ ### Changed
16
+ - **Genuine Creator Limits Default (Issue #228)**: `createCuratedModel` now defaults context window and max tokens directly to genuine vendor factory limits (`origCtx` / `origMax`) instead of legacy hardcoded 128k/8k limits.
17
+
8
18
  ## [0.5.26] - 2026-10-17
9
19
 
10
20
  ### Fixed
package/lib/client.js CHANGED
@@ -123,7 +123,17 @@ const en = {
123
123
  'ui.retry': 'Retry',
124
124
  'update.check_failed': 'Failed to check for updates',
125
125
  'update.retry': 'Retry check',
126
- 'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
126
+ 'models.filter_by_plan': 'Hide models not in active plan',
127
+ 'models.filter_by_plan_hint': 'Automatically filter chat model picker to models included in your active ClinePass subscription plan.',
128
+ 'models.visible_models': 'Visible Models in Chat',
129
+ 'models.visible_models_hint': 'Choose which models appear in the DSH chat selector (empty means all supported models).',
130
+ 'models.in_plan': 'In Plan',
131
+ 'models.out_of_plan': 'Out of Plan',
132
+ 'models.plan_only': 'Plan Only',
133
+ 'models.show_all': 'Show All',
134
+ 'models.clean_stale': 'Clean Stale ({count})',
135
+ 'models.models_selected': '{selected} of {total} selected for chat',
136
+ 'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
127
137
  'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
128
138
  'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
129
139
  'models.sync': '🔄 Check Plan Models',
@@ -296,7 +306,17 @@ const en = {
296
306
  'ui.retry': '重试',
297
307
  'update.check_failed': '检查更新失败',
298
308
  'update.retry': '重试检查',
299
- 'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
309
+ 'models.filter_by_plan': '隐藏未订阅模型',
310
+ 'models.filter_by_plan_hint': '在聊天模型选择器中自动隐藏当前 ClinePass 订阅套餐未包含的模型。',
311
+ 'models.visible_models': '聊天可见模型',
312
+ 'models.visible_models_hint': '选择在 DSH 聊天选择器中显示的模型(为空时显示全部可用模型)。',
313
+ 'models.in_plan': '套餐内',
314
+ 'models.out_of_plan': '套餐外',
315
+ 'models.plan_only': '仅套餐模型',
316
+ 'models.show_all': '显示全部',
317
+ 'models.clean_stale': '清理过期 ({count})',
318
+ 'models.models_selected': '已选择 {selected} / {total} 个模型用于聊天',
319
+ 'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
300
320
  'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
301
321
  'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
302
322
  'models.sync': '🔄 同步订阅模型',
@@ -1537,6 +1557,254 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1537
1557
  )
1538
1558
  }
1539
1559
 
1560
+ // --- src/client/components/visible-models.js ---
1561
+ function formatModelLimits(num) {
1562
+ num = Number(num) || 0
1563
+ if (num >= 1000000) {
1564
+ const m = num / 1000000
1565
+ return `${m % 1 === 0 ? m : m.toFixed(1)}M`
1566
+ }
1567
+ if (num >= 1000) {
1568
+ const k = num / 1000
1569
+ return `${k % 1 === 0 ? k : k.toFixed(1)}K`
1570
+ }
1571
+ return String(num)
1572
+ }
1573
+
1574
+ function VisibleModelsSub({
1575
+ modelsList = [],
1576
+ visibleModels = [],
1577
+ filterModelsByPlan = true,
1578
+ onConfigUpdate,
1579
+ busy,
1580
+ t,
1581
+ }) {
1582
+ const [open, setOpen] = React.useState(false)
1583
+ const [search, setSearch] = React.useState('')
1584
+
1585
+ const allModelIds = modelsList.map((m) => m.id)
1586
+ const allModelIdSet = new Set(allModelIds)
1587
+ const staleIds = Array.isArray(visibleModels) ? visibleModels.filter((id) => !allModelIdSet.has(id)) : []
1588
+ const isAllVisible = !Array.isArray(visibleModels) || visibleModels.length === 0
1589
+ const count = isAllVisible ? modelsList.length : visibleModels.length
1590
+
1591
+ function handleToggleModel(modelId) {
1592
+ if (typeof onConfigUpdate !== 'function') return
1593
+ if (isAllVisible) {
1594
+ const next = allModelIds.filter((id) => id !== modelId)
1595
+ onConfigUpdate({ visibleModels: next })
1596
+ } else {
1597
+ let next
1598
+ if (visibleModels.includes(modelId)) {
1599
+ next = visibleModels.filter((id) => id !== modelId)
1600
+ } else {
1601
+ next = [...visibleModels, modelId]
1602
+ }
1603
+ onConfigUpdate({ visibleModels: next })
1604
+ }
1605
+ }
1606
+
1607
+ function handleSelectOnlyPlan() {
1608
+ if (typeof onConfigUpdate !== 'function') return
1609
+ const planOnly = modelsList.filter((m) => m.inPlan).map((m) => m.id)
1610
+ onConfigUpdate({ visibleModels: planOnly.length > 0 ? planOnly : allModelIds })
1611
+ }
1612
+
1613
+ function handleShowAll() {
1614
+ if (typeof onConfigUpdate !== 'function') return
1615
+ onConfigUpdate({ visibleModels: [] })
1616
+ }
1617
+
1618
+ function handleCleanStale() {
1619
+ if (typeof onConfigUpdate !== 'function') return
1620
+ const cleaned = visibleModels.filter((id) => allModelIdSet.has(id))
1621
+ onConfigUpdate({ visibleModels: cleaned })
1622
+ }
1623
+
1624
+ const filtered = modelsList.filter((m) => {
1625
+ if (!search.trim()) return true
1626
+ const q = search.trim().toLowerCase()
1627
+ return (
1628
+ (m.name && m.name.toLowerCase().includes(q)) ||
1629
+ (m.id && m.id.toLowerCase().includes(q)) ||
1630
+ (m.category && m.category.toLowerCase().includes(q))
1631
+ )
1632
+ })
1633
+
1634
+ return React.createElement(
1635
+ 'div',
1636
+ {
1637
+ style: {
1638
+ display: 'flex',
1639
+ flexDirection: 'column',
1640
+ gap: '10px',
1641
+ padding: '12px 14px',
1642
+ border: '1px solid var(--dsw-alias-border-l2)',
1643
+ borderRadius: '8px',
1644
+ background: 'var(--dsw-alias-bg-layer-2)',
1645
+ marginBottom: '14px',
1646
+ },
1647
+ },
1648
+ React.createElement(
1649
+ 'div',
1650
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
1651
+ React.createElement(
1652
+ 'label',
1653
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '13px', fontWeight: '500' } },
1654
+ React.createElement('input', {
1655
+ type: 'checkbox',
1656
+ checked: !!filterModelsByPlan,
1657
+ disabled: !!busy,
1658
+ onChange: (e) => onConfigUpdate && onConfigUpdate({ filterModelsByPlan: e.target.checked }),
1659
+ }),
1660
+ React.createElement('span', null, t('models.filter_by_plan'))
1661
+ ),
1662
+ React.createElement(
1663
+ 'div',
1664
+ { style: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' } },
1665
+ !isAllVisible
1666
+ ? React.createElement(
1667
+ 'button',
1668
+ {
1669
+ type: 'button',
1670
+ className: 'cb-btn',
1671
+ style: { padding: '4px 8px', fontSize: '12px' },
1672
+ disabled: !!busy,
1673
+ onClick: handleShowAll,
1674
+ },
1675
+ t('models.show_all')
1676
+ )
1677
+ : null,
1678
+ React.createElement(
1679
+ 'button',
1680
+ {
1681
+ type: 'button',
1682
+ className: 'cb-btn',
1683
+ style: { padding: '4px 8px', fontSize: '12px' },
1684
+ disabled: !!busy,
1685
+ onClick: handleSelectOnlyPlan,
1686
+ },
1687
+ t('models.plan_only')
1688
+ ),
1689
+ staleIds.length > 0
1690
+ ? React.createElement(
1691
+ 'button',
1692
+ {
1693
+ type: 'button',
1694
+ className: 'cb-btn',
1695
+ style: { padding: '4px 8px', fontSize: '12px', color: 'var(--dsw-alias-state-warn-primary)' },
1696
+ disabled: !!busy,
1697
+ onClick: handleCleanStale,
1698
+ },
1699
+ t('models.clean_stale', { count: staleIds.length })
1700
+ )
1701
+ : null,
1702
+ React.createElement(
1703
+ 'button',
1704
+ {
1705
+ type: 'button',
1706
+ className: 'cb-btn',
1707
+ style: { padding: '4px 10px', fontSize: '12px' },
1708
+ onClick: () => setOpen(!open),
1709
+ },
1710
+ open ? '▲ ' + t('models.models_selected', { selected: count, total: modelsList.length }) : '▼ ' + t('models.models_selected', { selected: count, total: modelsList.length })
1711
+ )
1712
+ )
1713
+ ),
1714
+ React.createElement(
1715
+ 'div',
1716
+ { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', marginTop: '-4px' } },
1717
+ t('models.filter_by_plan_hint')
1718
+ ),
1719
+ open
1720
+ ? React.createElement(
1721
+ 'div',
1722
+ {
1723
+ style: {
1724
+ display: 'flex',
1725
+ flexDirection: 'column',
1726
+ gap: '8px',
1727
+ paddingTop: '8px',
1728
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
1729
+ },
1730
+ },
1731
+ React.createElement('input', {
1732
+ type: 'text',
1733
+ className: 'cb-input',
1734
+ style: { height: '30px', fontSize: '12px' },
1735
+ placeholder: t('models.search_placeholder'),
1736
+ value: search,
1737
+ onChange: (e) => setSearch(e.target.value),
1738
+ }),
1739
+ React.createElement(
1740
+ 'div',
1741
+ {
1742
+ style: {
1743
+ display: 'flex',
1744
+ flexDirection: 'column',
1745
+ gap: '4px',
1746
+ maxHeight: '240px',
1747
+ overflowY: 'auto',
1748
+ paddingRight: '4px',
1749
+ },
1750
+ },
1751
+ filtered.map((m) => {
1752
+ const isChecked = isAllVisible || visibleModels.includes(m.id)
1753
+ const ctxStr = `${formatModelLimits(m.contextLength)} / ${formatModelLimits(m.maxTokens)}`
1754
+ return React.createElement(
1755
+ 'label',
1756
+ {
1757
+ key: m.id,
1758
+ style: {
1759
+ display: 'flex',
1760
+ alignItems: 'center',
1761
+ justifyContent: 'space-between',
1762
+ padding: '5px 8px',
1763
+ borderRadius: '6px',
1764
+ background: isChecked ? 'var(--dsw-alias-bg-layer-3)' : 'transparent',
1765
+ cursor: 'pointer',
1766
+ fontSize: '12px',
1767
+ opacity: m.inPlan ? 1 : 0.65,
1768
+ },
1769
+ },
1770
+ React.createElement(
1771
+ 'div',
1772
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: 1, minWidth: 0 } },
1773
+ React.createElement('input', {
1774
+ type: 'checkbox',
1775
+ checked: isChecked,
1776
+ onChange: () => handleToggleModel(m.id),
1777
+ }),
1778
+ React.createElement('span', { style: { fontWeight: '500', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, m.name),
1779
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px' } }, `(${m.id})`)
1780
+ ),
1781
+ React.createElement(
1782
+ 'div',
1783
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
1784
+ React.createElement(
1785
+ 'span',
1786
+ {
1787
+ className: 'cb-badge',
1788
+ style: { fontSize: '11px', padding: '2px 6px', fontWeight: '600' },
1789
+ title: 'Context Window / Max Output Tokens',
1790
+ },
1791
+ ctxStr
1792
+ ),
1793
+ m.inPlan
1794
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 6px' } }, t('models.in_plan'))
1795
+ : React.createElement('span', { className: 'cb-badge', style: { fontSize: '10px', padding: '1px 6px', color: 'var(--dsw-alias-label-secondary)' } }, t('models.out_of_plan')),
1796
+ !m.isContextOverridden
1797
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 5px' } }, '原厂')
1798
+ : null
1799
+ )
1800
+ )
1801
+ })
1802
+ )
1803
+ )
1804
+ : null
1805
+ )
1806
+ }
1807
+
1540
1808
  // --- src/client/components/models-section.js ---
1541
1809
  function ModelsSection({
1542
1810
  modelsList,
@@ -1553,6 +1821,8 @@ function ModelsSection({
1553
1821
  defaultModelWarning,
1554
1822
  customModels,
1555
1823
  modelReasoningDefaults,
1824
+ visibleModels,
1825
+ filterModelsByPlan,
1556
1826
  onConfigUpdate,
1557
1827
  onUpdateReasoningEffort,
1558
1828
  t,
@@ -1604,6 +1874,14 @@ function ModelsSection({
1604
1874
  defaultModelWarning
1605
1875
  ? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, `⚠️ ${defaultModelWarning}`)
1606
1876
  : null,
1877
+ React.createElement(VisibleModelsSub, {
1878
+ modelsList,
1879
+ visibleModels,
1880
+ filterModelsByPlan,
1881
+ onConfigUpdate,
1882
+ busy,
1883
+ t,
1884
+ }),
1607
1885
  React.createElement(
1608
1886
  'div',
1609
1887
  { className: 'cb-section-title' },
@@ -2760,6 +3038,8 @@ function SettingsPage(props) {
2760
3038
  defaultModelWarning: status?.config?.defaultModelWarning || '',
2761
3039
  customModels: draft?.customModels || [],
2762
3040
  modelReasoningDefaults: draft?.modelReasoningDefaults || {},
3041
+ visibleModels: draft?.visibleModels || [],
3042
+ filterModelsByPlan: draft?.filterModelsByPlan !== false,
2763
3043
  onConfigUpdate: handleConfigPatch,
2764
3044
  onUpdateReasoningEffort: (modelId, effort) => {
2765
3045
  const next = { ...(draft?.modelReasoningDefaults || {}) }
package/lib/config.js CHANGED
@@ -37,6 +37,10 @@ export const Config = z.object({
37
37
  .description('Default model ID for chat and smoke tests.').volatile(),
38
38
  disabledModels: z.array(z.string()).default([])
39
39
  .description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
40
+ filterModelsByPlan: z.boolean().default(true)
41
+ .description('Automatically hide models not included in active subscription plan from chat picker.').volatile(),
42
+ visibleModels: z.array(z.string()).default([])
43
+ .description('User-selected visible models in chat model picker (empty means all supported models).').volatile(),
40
44
  enabledModels: z.array(z.string()).default([])
41
45
  .description('Deprecated: preserved for backwards compatibility with earlier versions.'),
42
46
  dynamicModels: z.array(z.object({
@@ -144,7 +148,34 @@ export function publicConfig(cfg) {
144
148
  disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
145
149
  }
146
150
 
151
+ const filterModelsByPlan = cfg?.filterModelsByPlan !== false
152
+ const visibleModels = Array.isArray(cfg?.visibleModels) ? cfg.visibleModels.map(migrateModelId) : []
153
+
154
+ const planModelSet = new Set(dynamic.map((m) => m.id))
155
+ for (const m of allModels) {
156
+ if (m.inPlan === undefined) {
157
+ m.inPlan = isSynced ? (planModelSet.has(m.id) || planModelSet.has(migrateModelId(m.id)) || Boolean(m.isCustom)) : true
158
+ }
159
+ }
160
+
147
161
  let activeIds = getActiveModelIds(allDefaultIds, disabledList)
162
+ if (filterModelsByPlan && isSynced && planModelSet.size > 0) {
163
+ const inPlanIds = activeIds.filter((id) => {
164
+ const m = allModels.find((mod) => mod.id === id)
165
+ return m ? m.inPlan : true
166
+ })
167
+ if (inPlanIds.length > 0) {
168
+ activeIds = inPlanIds
169
+ }
170
+ }
171
+ if (visibleModels.length > 0) {
172
+ const visibleSet = new Set(visibleModels)
173
+ const filteredByVisible = activeIds.filter((id) => visibleSet.has(id))
174
+ if (filteredByVisible.length > 0) {
175
+ activeIds = filteredByVisible
176
+ }
177
+ }
178
+
148
179
  if (!activeIds.length && allDefaultIds.length) {
149
180
  activeIds = [allDefaultIds[0]]
150
181
  }
@@ -176,6 +207,8 @@ export function publicConfig(cfg) {
176
207
  proxyMode: cfg?.proxyMode !== false,
177
208
  statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
178
209
  disabledModels: disabledList,
210
+ filterModelsByPlan,
211
+ visibleModels,
179
212
  enabledModels: activeIds,
180
213
  timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
181
214
  smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
package/lib/models.js CHANGED
@@ -85,7 +85,7 @@ export function getOriginalModelProvider(nameOrId) {
85
85
 
86
86
  function createCuratedModel({
87
87
  id, name, description,
88
- ctx = 128000, maxTokens = 8192,
88
+ ctx, maxTokens,
89
89
  origCtx, origMax, origProv,
90
90
  input = ['text'], category = 'general',
91
91
  recommended = false, reasoningEfforts,
@@ -93,11 +93,13 @@ function createCuratedModel({
93
93
  const c = origCtx || getOriginalModelContext(id)
94
94
  const p = origProv || getOriginalModelProvider(id)
95
95
  const om = origMax || getOriginalModelMaxTokens(id)
96
+ const activeCtx = ctx || c
97
+ const activeMax = maxTokens || om
96
98
  return {
97
99
  id, name, description,
98
- clineContextLength: ctx, defaultContextLength: ctx, contextLength: ctx,
100
+ clineContextLength: activeCtx, defaultContextLength: activeCtx, contextLength: activeCtx,
99
101
  originalContextLength: c, originalProvider: p, originalMaxTokens: om,
100
- maxTokens, defaultMaxTokens: maxTokens,
102
+ maxTokens: activeMax, defaultMaxTokens: activeMax,
101
103
  input, category, recommended, isCustom: false,
102
104
  ...(reasoningEfforts ? { reasoningEfforts } : {}),
103
105
  }
@@ -107,76 +109,65 @@ export const CLINE_MODELS = Object.freeze([
107
109
  createCuratedModel({
108
110
  id: 'cline-pass/deepseek-v4.1-flash',
109
111
  name: 'DeepSeek V4.1 Flash',
110
- description: 'High-speed reasoning & code completion model optimized for agentic loops.',
111
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
112
+ description: 'High-speed reasoning & code completion model optimized for agentic loops.', input: ['text', 'image'], category: 'coding', recommended: true,
112
113
  reasoningEfforts: ['low', 'medium', 'high'],
113
114
  }),
114
115
  createCuratedModel({
115
116
  id: 'cline-pass/deepseek-v4-pro',
116
117
  name: 'DeepSeek V4 Pro',
117
- description: 'Flagship reasoning and multi-turn architectural coding model.',
118
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
118
+ description: 'Flagship reasoning and multi-turn architectural coding model.', input: ['text', 'image'], category: 'coding', recommended: true,
119
119
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
120
120
  }),
121
121
  createCuratedModel({
122
122
  id: 'cline-pass/glm-5.2',
123
123
  name: 'GLM 5.2',
124
- description: 'Bilingual general & coding model with strong instruction following.',
125
- ctx: 128000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
124
+ description: 'Bilingual general & coding model with strong instruction following.', input: ['text'], category: 'general', recommended: false,
126
125
  reasoningEfforts: ['low', 'medium', 'high'],
127
126
  }),
128
127
  createCuratedModel({
129
128
  id: 'cline-pass/kimi-k3',
130
129
  name: 'Kimi K3',
131
- description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
132
- ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: true,
130
+ description: 'Long-context reasoning & document synthesis model by Moonshot AI.', input: ['text'], category: 'reasoning', recommended: true,
133
131
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
134
132
  }),
135
133
  createCuratedModel({
136
134
  id: 'cline-pass/kimi-k2.7-code',
137
135
  name: 'Kimi K2.7 Code',
138
- description: 'Specialized coding model with agentic tool calling support.',
139
- ctx: 200000, maxTokens: 8192, input: ['text'], category: 'coding', recommended: false,
136
+ description: 'Specialized coding model with agentic tool calling support.', input: ['text'], category: 'coding', recommended: false,
140
137
  }),
141
138
  createCuratedModel({
142
139
  id: 'cline-pass/kimi-k2.6',
143
140
  name: 'Kimi K2.6',
144
- description: 'Balanced long-context conversational model.',
145
- ctx: 200000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
141
+ description: 'Balanced long-context conversational model.', input: ['text'], category: 'general', recommended: false,
146
142
  }),
147
143
  createCuratedModel({
148
144
  id: 'cline-pass/qwen3.7-max',
149
145
  name: 'Qwen 3.7 Max',
150
- description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
151
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: true,
146
+ description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.', input: ['text', 'image'], category: 'multimodal', recommended: true,
152
147
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
153
148
  }),
154
149
  createCuratedModel({
155
150
  id: 'cline-pass/qwen3.7-plus',
156
151
  name: 'Qwen 3.7 Plus',
157
- description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
158
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: false,
152
+ description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.', input: ['text', 'image'], category: 'multimodal', recommended: false,
159
153
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
160
154
  }),
161
155
  createCuratedModel({
162
156
  id: 'cline-pass/minimax-m3',
163
157
  name: 'MiniMax M3',
164
- description: 'High throughput reasoning and synthesis model.',
165
- ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: false,
158
+ description: 'High throughput reasoning and synthesis model.', input: ['text'], category: 'reasoning', recommended: false,
166
159
  reasoningEfforts: ['low', 'medium', 'high'],
167
160
  }),
168
161
  createCuratedModel({
169
162
  id: 'cline-pass/mimo-v2.5',
170
163
  name: 'MiMo V2.5',
171
- description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
172
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'general', recommended: false,
164
+ description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.', input: ['text', 'image'], category: 'general', recommended: false,
173
165
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
174
166
  }),
175
167
  createCuratedModel({
176
168
  id: 'cline-pass/mimo-v2.5-pro',
177
169
  name: 'MiMo V2.5 Pro',
178
- description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
179
- ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: false,
170
+ description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.', input: ['text', 'image'], category: 'coding', recommended: false,
180
171
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
181
172
  }),
182
173
  ])
@@ -285,7 +276,7 @@ export function parsePlanIncludedModels(includedInput) {
285
276
  *
286
277
  * Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
287
278
  */
288
- export function getAllModels(dynamicModels = [], modelContextOverrides = [], customModels = []) {
279
+ export function getAllModels(dynamicModels = [], modelContextOverrides = [], customModels = [], options = {}) {
289
280
  const normalizedDynamic = (Array.isArray(dynamicModels) ? dynamicModels : []).map(migrateModelEntry)
290
281
  const overridesMap = new Map()
291
282
  if (Array.isArray(modelContextOverrides)) {
@@ -328,6 +319,7 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
328
319
  }
329
320
  }
330
321
 
322
+ const dynamicIdSet = new Set(normalizedDynamic.map((m) => m.id))
331
323
  let baseModels = []
332
324
  if (normalizedDynamic.length > 0) {
333
325
  const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
@@ -348,26 +340,41 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
348
340
  originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
349
341
  originalMaxTokens: curated.originalMaxTokens || getOriginalModelMaxTokens(curated.id),
350
342
  originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
351
- defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens || 8192,
343
+ defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens || getOriginalModelMaxTokens(curated.id),
352
344
  input: dm.input && dm.input.length ? dm.input : curated.input,
353
345
  category: dm.category || curated.category,
354
346
  reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
347
+ inPlan: true,
355
348
  unverified: false,
356
349
  })
357
350
  }
351
+ const dynOrigCtx = getOriginalModelContext(dm.id)
352
+ const dynOrigMax = getOriginalModelMaxTokens(dm.id)
358
353
  return applyOverride({
359
354
  ...dm,
360
- clineContextLength: dm.contextLength || 200000,
361
- defaultContextLength: dm.contextLength || 200000,
362
- originalContextLength: dm.originalContextLength || getOriginalModelContext(dm.id),
363
- originalMaxTokens: dm.originalMaxTokens || getOriginalModelMaxTokens(dm.id),
355
+ clineContextLength: dm.contextLength || dynOrigCtx,
356
+ defaultContextLength: dm.contextLength || dynOrigCtx,
357
+ originalContextLength: dm.originalContextLength || dynOrigCtx,
358
+ originalMaxTokens: dm.originalMaxTokens || dynOrigMax,
364
359
  originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
365
- defaultMaxTokens: dm.maxTokens || 8192,
360
+ defaultMaxTokens: dm.maxTokens || dynOrigMax,
361
+ contextLength: dm.contextLength || dynOrigCtx,
362
+ maxTokens: dm.maxTokens || dynOrigMax,
363
+ inPlan: true,
366
364
  unverified: false,
367
365
  })
368
366
  })
367
+
368
+ if (options && options.includeOutOfPlan) {
369
+ const baseModelIds = new Set(baseModels.map((m) => m.id))
370
+ for (const cm of CLINE_MODELS) {
371
+ if (!baseModelIds.has(cm.id) && !dynamicIdSet.has(cm.id)) {
372
+ baseModels.push(applyOverride({ ...cm, inPlan: false, unverified: false }))
373
+ }
374
+ }
375
+ }
369
376
  } else {
370
- baseModels = CLINE_MODELS.map((m) => applyOverride({ ...m, unverified: true }))
377
+ baseModels = CLINE_MODELS.map((m) => applyOverride({ ...m, inPlan: true, unverified: true }))
371
378
  }
372
379
 
373
380
  if (Array.isArray(customModels) && customModels.length > 0) {
@@ -383,6 +390,7 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
383
390
  input: Array.isArray(cm.input) && cm.input.length ? cm.input : ['text'],
384
391
  category: cm.category || 'general',
385
392
  isCustom: true,
393
+ inPlan: true,
386
394
  reasoningEfforts: cm.reasoningEfforts !== undefined ? cm.reasoningEfforts : (cm.isReasoning ? ['low', 'medium', 'high', 'max'] : undefined),
387
395
  unverified: false,
388
396
  }
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
14
14
  let cachedToken = null
15
15
 
16
16
  const require = createRequire(import.meta.url)
17
- let packageVersion = '0.5.26'
17
+ let packageVersion = '0.5.27'
18
18
  try {
19
- packageVersion = require('../package.json').version || '0.5.26'
19
+ packageVersion = require('../package.json').version || '0.5.27'
20
20
  } catch (err) {
21
21
  /* fallback to default version if manifest is unreadable */
22
22
  }
@@ -274,4 +274,41 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
274
274
  }
275
275
  },
276
276
  }), 'dsh-clinebot: /models/probe')
277
+ // POST /dsh-clinebot/models/visible — configure visible models and plan filtering
278
+ ctx.effect(() => ctx.webServer.register({
279
+ kind: 'exact',
280
+ path: '/dsh-clinebot/models/visible',
281
+ handler: async (req, res) => {
282
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
283
+ if (!assertTrustedSettingsRequest(req, res, ctx)) return
284
+ try {
285
+ const bodyBuf = await readBody(req)
286
+ let body = {}
287
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
288
+ const settingsApi = getSettingsApi()
289
+ if (!settingsApi || typeof settingsApi.replace !== 'function') {
290
+ return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
291
+ }
292
+ const current = plainConfig(live())
293
+ const next = { ...current }
294
+ if (Array.isArray(body.visibleModels)) {
295
+ next.visibleModels = body.visibleModels
296
+ }
297
+ if (typeof body.filterModelsByPlan === 'boolean') {
298
+ next.filterModelsByPlan = body.filterModelsByPlan
299
+ }
300
+ await settingsApi.replace(next)
301
+ await syncProviderState(next)
302
+ writeJson(res, 200, {
303
+ ok: true,
304
+ visibleModels: next.visibleModels,
305
+ filterModelsByPlan: next.filterModelsByPlan,
306
+ enabledModels: publicConfig(next).enabledModels,
307
+ })
308
+ } catch (err) {
309
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
310
+ }
311
+ },
312
+ }), 'dsh-clinebot: /models/visible')
313
+
277
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.5.26",
3
+ "version": "0.5.27",
4
4
  "description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
5
5
  "license": "MIT",
6
6
  "type": "module",