@goodandready/dsh-clinebot 0.5.19 → 0.5.21

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,22 @@ 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.21] - 2026-10-12
9
+
10
+ ### Added
11
+ - **Configurable Model Max Output Tokens (maxTokens) & Form Controls (Issue #216, GH #13)**: Exposed editable `maxTokens` (Max Output Tokens) in the Models settings UI alongside context window, complete with preset Default/Original buttons, input controls, and badge status displays. Added context length and max output token configuration fields to the Custom Models form.
12
+ - **Wildcard Override Defaults and Apply-to-All Action (Issue #216, GH #13)**: Implemented wildcard model context override (`modelId: '*'`) in `getAllModels` and an "All" action in the settings UI calling `/dsh-clinebot/models/context` (`mode: 'apply-all'`) to batch-apply preferred context window and max output tokens across all models.
13
+ - **Authentic Vendor Model Limits (Issue #216, GH #13)**: Added accurate `originalMaxTokens` specifications across models (Qwen 131072, GLM 128000, MiniMax 128000, Kimi 65536, DeepSeek-V4.1 65536, MiMo 16384) and updated original context windows for DeepSeek-V4.1 and GLM-5.3.
14
+
15
+ ### Fixed
16
+ - **Preservation of Manual Provider Token Limits Across Sync (Issue #216, GH #13)**: `buildPiAiProvider` and `upsertPiAiProvider` now query existing provider models from `llm-pi-ai.providers.clinebot.models` and preserve user edits to `contextWindow` and `maxTokens` during dynamic catalog synchronization when not explicitly overridden by plugin configuration.
17
+ - **Context Override Detection for Output Limits (Issue #216, GH #13)**: Updated `isContextOverridden` calculation to consider custom `maxTokens` deviations (`activeMaxTokens !== defaultMaxTokens`), properly reflecting override badges in the UI.
18
+
19
+ ## [0.5.20] - 2026-10-11
20
+
21
+ ### Fixed
22
+ - **Local ESLint Binary Execution and devDependencies Enforcement (Issue #214)**: Added `eslint` (`^9.39.5`) to `devDependencies` in `package.json` and committed `package-lock.json`. Updated `scripts/build-client.js` to strictly enforce local binary execution (`node_modules/.bin/eslint`) and immediately abort with exit code 1 if `eslint` is missing, preventing silent bypass of `no-undef` validation on fresh clones and CI environments. Added automated decomposition test in `test/decomposition.test.js` validating local `eslint` presence and zero `no-undef` violations.
23
+
8
24
  ## [0.5.19] - 2026-10-07
9
25
 
10
26
  ### Added
package/lib/client.js CHANGED
@@ -147,12 +147,18 @@ const en = {
147
147
  'models.apply_original_all': '⚡ Original Specs',
148
148
  'models.reset_contexts_all': '↺ Reset Contexts',
149
149
  'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
150
- 'models.ctx_default': 'Default ({ctx})',
151
- 'models.ctx_original': 'Original ({ctx} · {provider})',
152
- 'models.ctx_edit': 'Edit Context Window',
150
+ 'models.ctx_default': 'Default ({ctx} / {max})',
151
+ 'models.ctx_original': 'Original ({ctx} / {max} · {provider})',
152
+ 'models.ctx_edit': 'Edit Limits',
153
+ 'models.ctx_window_label': 'Context (e.g. 200000)',
154
+ 'models.ctx_max_tokens_label': 'Max Out (e.g. 16384)',
155
+ 'models.ctx_apply_all': 'All',
156
+ 'models.ctx_apply_all_title': 'Apply these limits across all models',
153
157
  'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
154
158
  'models.ctx_save': 'Save',
155
159
  'models.ctx_cancel': 'Cancel',
160
+ 'models.custom_ctx_label': 'Context',
161
+ 'models.custom_max_label': 'Max Out',
156
162
  'models.ctx_tag_original': 'Original',
157
163
  'models.ctx_tag_custom': 'Custom',
158
164
  'stats.title': '📈 Current DSH Session Telemetry',
@@ -304,12 +310,18 @@ const en = {
304
310
  'models.apply_original_all': '⚡ 原厂规格',
305
311
  'models.reset_contexts_all': '↺ 重置上下文',
306
312
  'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
307
- 'models.ctx_default': '默认 ({ctx})',
308
- 'models.ctx_original': '原厂 ({ctx} · {provider})',
309
- 'models.ctx_edit': '编辑上下文窗口',
313
+ 'models.ctx_default': '默认 ({ctx} / {max})',
314
+ 'models.ctx_original': '原厂 ({ctx} / {max} · {provider})',
315
+ 'models.ctx_edit': '编辑限制',
316
+ 'models.ctx_window_label': '上下文 (如 200000)',
317
+ 'models.ctx_max_tokens_label': '最大输出 (如 16384)',
318
+ 'models.ctx_apply_all': '全部',
319
+ 'models.ctx_apply_all_title': '将此限制批量应用到所有模型',
310
320
  'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
311
321
  'models.ctx_save': '保存',
312
322
  'models.ctx_cancel': '取消',
323
+ 'models.custom_ctx_label': '上下文',
324
+ 'models.custom_max_label': '最大输出',
313
325
  'models.ctx_tag_original': '原厂',
314
326
  'models.ctx_tag_custom': '自定义',
315
327
  'stats.title': '📈 当前 DSH 会话请求遥测',
@@ -1219,17 +1231,21 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
1219
1231
  function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1220
1232
  const [newId, setNewId] = React.useState('')
1221
1233
  const [newName, setNewName] = React.useState('')
1234
+ const [newCtx, setNewCtx] = React.useState('')
1235
+ const [newMax, setNewMax] = React.useState('')
1222
1236
  const [isExpanded, setIsExpanded] = React.useState(false)
1223
1237
 
1224
1238
  function handleAdd() {
1225
1239
  const trimmedId = newId.trim()
1226
1240
  const trimmedName = newName.trim() || trimmedId
1227
1241
  if (!trimmedId) return
1242
+ const parsedCtx = parseInt(newCtx, 10)
1243
+ const parsedMax = parseInt(newMax, 10)
1228
1244
  const nextList = [...customModels.filter((m) => m.id !== trimmedId), {
1229
1245
  id: trimmedId,
1230
1246
  name: trimmedName,
1231
- contextLength: 128000,
1232
- maxTokens: 8192,
1247
+ contextLength: parsedCtx > 0 ? parsedCtx : 128000,
1248
+ maxTokens: parsedMax > 0 ? parsedMax : 8192,
1233
1249
  category: 'general',
1234
1250
  isCustom: true,
1235
1251
  }]
@@ -1238,6 +1254,8 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1238
1254
  }
1239
1255
  setNewId('')
1240
1256
  setNewName('')
1257
+ setNewCtx('')
1258
+ setNewMax('')
1241
1259
  }
1242
1260
 
1243
1261
  function handleDelete(id) {
@@ -1288,7 +1306,7 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1288
1306
  React.createElement('input', {
1289
1307
  type: 'text',
1290
1308
  className: 'cb-input',
1291
- style: { flex: '1 1 180px' },
1309
+ style: { flex: '2 1 140px' },
1292
1310
  placeholder: t('models.custom_id'),
1293
1311
  value: newId,
1294
1312
  disabled: !!busy,
@@ -1297,12 +1315,32 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1297
1315
  React.createElement('input', {
1298
1316
  type: 'text',
1299
1317
  className: 'cb-input',
1300
- style: { flex: '1 1 180px' },
1318
+ style: { flex: '2 1 140px' },
1301
1319
  placeholder: t('models.custom_name'),
1302
1320
  value: newName,
1303
1321
  disabled: !!busy,
1304
1322
  onChange: (e) => setNewName(e.target.value),
1305
1323
  }),
1324
+ React.createElement('input', {
1325
+ type: 'number',
1326
+ className: 'cb-input',
1327
+ style: { flex: '1 1 90px' },
1328
+ placeholder: t('models.custom_ctx_label'),
1329
+ title: t('models.custom_ctx_label'),
1330
+ value: newCtx,
1331
+ disabled: !!busy,
1332
+ onChange: (e) => setNewCtx(e.target.value),
1333
+ }),
1334
+ React.createElement('input', {
1335
+ type: 'number',
1336
+ className: 'cb-input',
1337
+ style: { flex: '1 1 80px' },
1338
+ placeholder: t('models.custom_max_label'),
1339
+ title: t('models.custom_max_label'),
1340
+ value: newMax,
1341
+ disabled: !!busy,
1342
+ onChange: (e) => setNewMax(e.target.value),
1343
+ }),
1306
1344
  React.createElement(
1307
1345
  'button',
1308
1346
  {
@@ -1338,7 +1376,8 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
1338
1376
  'div',
1339
1377
  null,
1340
1378
  React.createElement('span', { style: { fontWeight: 500 } }, cm.name || cm.id),
1341
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px' } }, `(${cm.id})`)
1379
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px' } }, `(${cm.id})`),
1380
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', marginLeft: '8px', fontSize: '11px' } }, `[${cm.contextLength || 128000} / ${cm.maxTokens || 8192}]`)
1342
1381
  ),
1343
1382
  React.createElement(
1344
1383
  'button',
@@ -1385,6 +1424,7 @@ function ModelsSection({
1385
1424
  const [viewFilter, setViewFilter] = React.useState('all')
1386
1425
  const [editingModelId, setEditingModelId] = React.useState(null)
1387
1426
  const [customCtxVal, setCustomCtxVal] = React.useState('')
1427
+ const [customMaxVal, setCustomMaxVal] = React.useState('')
1388
1428
 
1389
1429
  function formatCtx(num) {
1390
1430
  num = Number(num) || 200000
@@ -1580,9 +1620,11 @@ function ModelsSection({
1580
1620
  const isEnabled = !disabledSet.has(m.id)
1581
1621
  const defaultCtx = m.defaultContextLength || m.clineContextLength || 200000
1582
1622
  const origCtx = m.originalContextLength || 200000
1623
+ const defaultMax = m.defaultMaxTokens || 8192
1624
+ const origMax = m.originalMaxTokens || defaultMax
1583
1625
  const origProv = m.originalProvider || 'Original'
1584
1626
  const isOverridden = Boolean(m.isContextOverridden)
1585
- const isOriginal = isOverridden && (m.contextLength === origCtx)
1627
+ const isOriginal = isOverridden && (m.contextLength === origCtx && (m.maxTokens || defaultMax) === origMax)
1586
1628
 
1587
1629
  return React.createElement(
1588
1630
  'tr',
@@ -1641,7 +1683,7 @@ function ModelsSection({
1641
1683
  setEditingModelId(null)
1642
1684
  },
1643
1685
  },
1644
- t('models.ctx_default', { ctx: formatCtx(defaultCtx) })
1686
+ t('models.ctx_default', { ctx: formatCtx(defaultCtx), max: formatCtx(defaultMax) })
1645
1687
  ),
1646
1688
  React.createElement(
1647
1689
  'button',
@@ -1651,41 +1693,76 @@ function ModelsSection({
1651
1693
  style: { padding: '2px 6px', fontSize: '11px' },
1652
1694
  onClick: () => {
1653
1695
  if (typeof handleUpdateModelContext === 'function') {
1654
- handleUpdateModelContext({ modelId: m.id, contextLength: origCtx })
1696
+ handleUpdateModelContext({ modelId: m.id, contextLength: origCtx, maxTokens: origMax })
1655
1697
  }
1656
1698
  setEditingModelId(null)
1657
1699
  },
1658
1700
  },
1659
- t('models.ctx_original', { ctx: formatCtx(origCtx), provider: origProv })
1701
+ t('models.ctx_original', { ctx: formatCtx(origCtx), max: formatCtx(origMax), provider: origProv })
1660
1702
  )
1661
1703
  ),
1662
1704
  React.createElement(
1663
1705
  'div',
1664
- { style: { display: 'flex', gap: '4px', alignItems: 'center' } },
1706
+ { style: { display: 'flex', gap: '4px', alignItems: 'center', flexWrap: 'wrap' } },
1665
1707
  React.createElement('input', {
1666
1708
  type: 'number',
1667
1709
  className: 'cb-input',
1668
- style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '120px' },
1669
- placeholder: t('models.ctx_custom_placeholder'),
1710
+ style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '90px' },
1711
+ placeholder: t('models.ctx_window_label'),
1712
+ title: t('models.ctx_window_label'),
1670
1713
  value: customCtxVal,
1671
1714
  onChange: (e) => setCustomCtxVal(e.target.value),
1672
1715
  }),
1716
+ React.createElement('input', {
1717
+ type: 'number',
1718
+ className: 'cb-input',
1719
+ style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '75px' },
1720
+ placeholder: t('models.ctx_max_tokens_label'),
1721
+ title: t('models.ctx_max_tokens_label'),
1722
+ value: customMaxVal,
1723
+ onChange: (e) => setCustomMaxVal(e.target.value),
1724
+ }),
1673
1725
  React.createElement(
1674
1726
  'button',
1675
1727
  {
1676
1728
  type: 'button',
1677
1729
  className: 'cb-btn cb-btn-primary',
1678
- style: { padding: '2px 8px', fontSize: '11px' },
1730
+ style: { padding: '2px 6px', fontSize: '11px' },
1679
1731
  onClick: () => {
1680
- const val = parseInt(customCtxVal, 10)
1681
- if (val > 0 && typeof handleUpdateModelContext === 'function') {
1682
- handleUpdateModelContext({ modelId: m.id, contextLength: val })
1732
+ const cVal = parseInt(customCtxVal, 10)
1733
+ const mVal = parseInt(customMaxVal, 10)
1734
+ if (typeof handleUpdateModelContext === 'function') {
1735
+ const payload = { modelId: m.id }
1736
+ if (cVal > 0) payload.contextLength = cVal
1737
+ if (mVal > 0) payload.maxTokens = mVal
1738
+ handleUpdateModelContext(payload)
1683
1739
  }
1684
1740
  setEditingModelId(null)
1685
1741
  },
1686
1742
  },
1687
1743
  t('models.ctx_save')
1688
1744
  ),
1745
+ React.createElement(
1746
+ 'button',
1747
+ {
1748
+ type: 'button',
1749
+ className: 'cb-btn',
1750
+ title: t('models.ctx_apply_all_title'),
1751
+ style: { padding: '2px 5px', fontSize: '11px' },
1752
+ onClick: () => {
1753
+ const cVal = parseInt(customCtxVal, 10)
1754
+ const mVal = parseInt(customMaxVal, 10)
1755
+ if (typeof handleUpdateModelContext === 'function') {
1756
+ const payload = { mode: 'apply-all' }
1757
+ if (cVal > 0) payload.contextLength = cVal
1758
+ if (mVal > 0) payload.maxTokens = mVal
1759
+ handleUpdateModelContext(payload)
1760
+ }
1761
+ setEditingModelId(null)
1762
+ },
1763
+ },
1764
+ t('models.ctx_apply_all')
1765
+ ),
1689
1766
  React.createElement(
1690
1767
  'button',
1691
1768
  {
@@ -1707,10 +1784,11 @@ function ModelsSection({
1707
1784
  style: { fontWeight: '600', cursor: 'pointer' },
1708
1785
  onClick: () => {
1709
1786
  setCustomCtxVal(String(m.contextLength || defaultCtx))
1787
+ setCustomMaxVal(String(m.maxTokens || defaultMax))
1710
1788
  setEditingModelId(m.id)
1711
1789
  },
1712
1790
  },
1713
- formatCtx(m.contextLength || defaultCtx)
1791
+ `${formatCtx(m.contextLength || defaultCtx)} / ${formatCtx(m.maxTokens || defaultMax)}`
1714
1792
  ),
1715
1793
  isOverridden
1716
1794
  ? React.createElement(
@@ -1720,6 +1798,7 @@ function ModelsSection({
1720
1798
  style: { fontSize: '10px', padding: '1px 6px', cursor: 'pointer' },
1721
1799
  onClick: () => {
1722
1800
  setCustomCtxVal(String(m.contextLength || defaultCtx))
1801
+ setCustomMaxVal(String(m.maxTokens || defaultMax))
1723
1802
  setEditingModelId(m.id)
1724
1803
  },
1725
1804
  },
@@ -1735,6 +1814,7 @@ function ModelsSection({
1735
1814
  title: t('models.ctx_edit'),
1736
1815
  onClick: () => {
1737
1816
  setCustomCtxVal(String(m.contextLength || defaultCtx))
1817
+ setCustomMaxVal(String(m.maxTokens || defaultMax))
1738
1818
  setEditingModelId(m.id)
1739
1819
  },
1740
1820
  },
@@ -490,10 +490,15 @@ export function buildPiAiProvider({
490
490
  modelContextOverrides = [],
491
491
  models = [],
492
492
  customModels = [],
493
+ existingModels = [],
493
494
  displayName = PROVIDER_DISPLAY_NAME,
494
495
  } = {}) {
495
496
  const allAvailable = getAllModels(dynamicModels, modelContextOverrides, customModels)
496
497
  const customModelsMap = new Map((Array.isArray(customModels) ? customModels : []).filter(Boolean).map(cm => [cm.id, cm]))
498
+ const existingMap = new Map((Array.isArray(existingModels) ? existingModels : []).filter(Boolean).map(m => [m.id, m]))
499
+ const explicitOverridesSet = new Set((Array.isArray(modelContextOverrides) ? modelContextOverrides : []).map(o => o?.modelId))
500
+ const hasWildcardOverride = explicitOverridesSet.has('*')
501
+
497
502
  const modelList = (Array.isArray(models) && models.length ? models : allAvailable).map((m) => {
498
503
  let item
499
504
  if (typeof m === 'string') {
@@ -504,12 +509,26 @@ export function buildPiAiProvider({
504
509
  item = m
505
510
  }
506
511
  const hasImage = item.input?.includes('image') || item.input?.includes('vision')
512
+
513
+ // Preserve manual provider edits if user configured contextWindow or maxTokens in llm-pi-ai without explicit override in clinebot
514
+ const existingEntry = existingMap.get(item.id)
515
+ let ctxWin = Number(item.contextLength || item.contextWindow) || 200000
516
+ let maxTok = Number(item.maxTokens) || 8192
517
+ if (existingEntry && !explicitOverridesSet.has(item.id) && !hasWildcardOverride) {
518
+ if (typeof existingEntry.contextWindow === 'number' && existingEntry.contextWindow > 0) {
519
+ ctxWin = existingEntry.contextWindow
520
+ }
521
+ if (typeof existingEntry.maxTokens === 'number' && existingEntry.maxTokens > 0) {
522
+ maxTok = existingEntry.maxTokens
523
+ }
524
+ }
525
+
507
526
  const res = {
508
527
  id: item.id,
509
528
  name: item.name || item.id,
510
529
  description: formatModelDescription(item),
511
- contextWindow: Number(item.contextLength || item.contextWindow) || 200000,
512
- maxTokens: Number(item.maxTokens) || 8192,
530
+ contextWindow: ctxWin,
531
+ maxTokens: maxTok,
513
532
  input: hasImage ? ['text', 'image'] : ['text'],
514
533
  provider: PROVIDER_ID,
515
534
  }
package/lib/models.js CHANGED
@@ -52,12 +52,26 @@ export function getOriginalModelContext(nameOrId) {
52
52
  if (norm.includes('qwen')) return 1000000
53
53
  if (norm.includes('minimax')) return 1000000
54
54
  if (norm.includes('mimo')) return 1000000
55
+ if (norm.includes('glm-5.3') || norm.includes('glm-53')) return 1000000
56
+ if (norm.includes('deepseek-v4.1') || norm.includes('deepseek-v41')) return 1000000
55
57
  if (norm.includes('deepseek')) return 128000
56
58
  if (norm.includes('glm')) return 128000
57
59
  return 200000
58
60
  }
59
61
 
60
- function getOriginalModelProvider(nameOrId) {
62
+ export function getOriginalModelMaxTokens(nameOrId) {
63
+ const norm = String(nameOrId || '').toLowerCase()
64
+ if (norm.includes('qwen')) return 131072
65
+ if (norm.includes('glm')) return 128000
66
+ if (norm.includes('minimax')) return 128000
67
+ if (norm.includes('kimi')) return 65536
68
+ if (norm.includes('deepseek-v4.1') || norm.includes('deepseek-v41')) return 65536
69
+ if (norm.includes('deepseek')) return 16384
70
+ if (norm.includes('mimo')) return 16384
71
+ return 16384
72
+ }
73
+
74
+ export function getOriginalModelProvider(nameOrId) {
61
75
  const norm = String(nameOrId || '').toLowerCase()
62
76
  if (norm.includes('kimi')) return 'Moonshot AI'
63
77
  if (norm.includes('qwen')) return 'Alibaba Cloud'
@@ -68,181 +82,102 @@ function getOriginalModelProvider(nameOrId) {
68
82
  return 'Original Vendor'
69
83
  }
70
84
 
85
+ function createCuratedModel({
86
+ id, name, description,
87
+ ctx = 128000, maxTokens = 8192,
88
+ origCtx, origMax, origProv,
89
+ input = ['text'], category = 'general',
90
+ recommended = false, reasoningEfforts,
91
+ }) {
92
+ const c = origCtx || getOriginalModelContext(id)
93
+ const p = origProv || getOriginalModelProvider(id)
94
+ const om = origMax || getOriginalModelMaxTokens(id)
95
+ return {
96
+ id, name, description,
97
+ clineContextLength: ctx, defaultContextLength: ctx, contextLength: ctx,
98
+ originalContextLength: c, originalProvider: p, originalMaxTokens: om,
99
+ maxTokens, defaultMaxTokens: maxTokens,
100
+ input, category, recommended, isCustom: false,
101
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
102
+ }
103
+ }
104
+
71
105
  export const CLINE_MODELS = Object.freeze([
72
- {
106
+ createCuratedModel({
73
107
  id: 'cline-pass/deepseek-v4-flash',
74
108
  name: 'DeepSeek V4 Flash',
75
109
  description: 'High-speed reasoning & code completion model optimized for agentic loops.',
76
- clineContextLength: 128000,
77
- defaultContextLength: 128000,
78
- originalContextLength: 128000,
79
- originalProvider: 'DeepSeek',
80
- contextLength: 128000,
81
- maxTokens: 8192,
82
- input: ['text', 'image'],
83
- category: 'coding',
84
- recommended: true,
85
- isCustom: false,
110
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
86
111
  reasoningEfforts: ['low', 'medium', 'high'],
87
- },
88
- {
112
+ }),
113
+ createCuratedModel({
89
114
  id: 'cline-pass/deepseek-v4-pro',
90
115
  name: 'DeepSeek V4 Pro',
91
116
  description: 'Flagship reasoning and multi-turn architectural coding model.',
92
- clineContextLength: 128000,
93
- defaultContextLength: 128000,
94
- originalContextLength: 128000,
95
- originalProvider: 'DeepSeek',
96
- contextLength: 128000,
97
- maxTokens: 8192,
98
- input: ['text', 'image'],
99
- category: 'coding',
100
- recommended: true,
101
- isCustom: false,
117
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
102
118
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
103
- },
104
- {
119
+ }),
120
+ createCuratedModel({
105
121
  id: 'cline-pass/glm-5.2',
106
122
  name: 'GLM 5.2',
107
123
  description: 'Bilingual general & coding model with strong instruction following.',
108
- clineContextLength: 128000,
109
- defaultContextLength: 128000,
110
- originalContextLength: 128000,
111
- originalProvider: 'Zhipu AI',
112
- contextLength: 128000,
113
- maxTokens: 8192,
114
- input: ['text'],
115
- category: 'general',
116
- recommended: false,
117
- isCustom: false,
124
+ ctx: 128000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
118
125
  reasoningEfforts: ['low', 'medium', 'high'],
119
- },
120
- {
126
+ }),
127
+ createCuratedModel({
121
128
  id: 'cline-pass/kimi-k3',
122
129
  name: 'Kimi K3',
123
130
  description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
124
- clineContextLength: 200000,
125
- defaultContextLength: 200000,
126
- originalContextLength: 2000000,
127
- originalProvider: 'Moonshot AI',
128
- contextLength: 200000,
129
- maxTokens: 8192,
130
- input: ['text'],
131
- category: 'reasoning',
132
- recommended: true,
133
- isCustom: false,
131
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: true,
134
132
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
135
- },
136
- {
133
+ }),
134
+ createCuratedModel({
137
135
  id: 'cline-pass/kimi-k2.7-code',
138
136
  name: 'Kimi K2.7 Code',
139
137
  description: 'Specialized coding model with agentic tool calling support.',
140
- clineContextLength: 200000,
141
- defaultContextLength: 200000,
142
- originalContextLength: 2000000,
143
- originalProvider: 'Moonshot AI',
144
- contextLength: 200000,
145
- maxTokens: 8192,
146
- input: ['text'],
147
- category: 'coding',
148
- recommended: false,
149
- isCustom: false,
150
- },
151
- {
138
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'coding', recommended: false,
139
+ }),
140
+ createCuratedModel({
152
141
  id: 'cline-pass/kimi-k2.6',
153
142
  name: 'Kimi K2.6',
154
143
  description: 'Balanced long-context conversational model.',
155
- clineContextLength: 200000,
156
- defaultContextLength: 200000,
157
- originalContextLength: 2000000,
158
- originalProvider: 'Moonshot AI',
159
- contextLength: 200000,
160
- maxTokens: 8192,
161
- input: ['text'],
162
- category: 'general',
163
- recommended: false,
164
- isCustom: false,
165
- },
166
- {
144
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
145
+ }),
146
+ createCuratedModel({
167
147
  id: 'cline-pass/qwen3.7-max',
168
148
  name: 'Qwen 3.7 Max',
169
149
  description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
170
- clineContextLength: 128000,
171
- defaultContextLength: 128000,
172
- originalContextLength: 1000000,
173
- originalProvider: 'Alibaba Cloud',
174
- contextLength: 128000,
175
- maxTokens: 8192,
176
- input: ['text', 'image'],
177
- category: 'multimodal',
178
- recommended: true,
179
- isCustom: false,
150
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: true,
180
151
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
181
- },
182
- {
152
+ }),
153
+ createCuratedModel({
183
154
  id: 'cline-pass/qwen3.7-plus',
184
155
  name: 'Qwen 3.7 Plus',
185
156
  description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
186
- clineContextLength: 128000,
187
- defaultContextLength: 128000,
188
- originalContextLength: 1000000,
189
- originalProvider: 'Alibaba Cloud',
190
- contextLength: 128000,
191
- maxTokens: 8192,
192
- input: ['text', 'image'],
193
- category: 'multimodal',
194
- recommended: false,
195
- isCustom: false,
157
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: false,
196
158
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
197
- },
198
- {
159
+ }),
160
+ createCuratedModel({
199
161
  id: 'cline-pass/minimax-m3',
200
162
  name: 'MiniMax M3',
201
163
  description: 'High throughput reasoning and synthesis model.',
202
- clineContextLength: 200000,
203
- defaultContextLength: 200000,
204
- originalContextLength: 1000000,
205
- originalProvider: 'MiniMax',
206
- contextLength: 200000,
207
- maxTokens: 8192,
208
- input: ['text'],
209
- category: 'reasoning',
210
- recommended: false,
211
- isCustom: false,
164
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: false,
212
165
  reasoningEfforts: ['low', 'medium', 'high'],
213
- },
214
- {
166
+ }),
167
+ createCuratedModel({
215
168
  id: 'cline-pass/mimo-v2.5',
216
169
  name: 'MiMo V2.5',
217
170
  description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
218
- clineContextLength: 128000,
219
- defaultContextLength: 128000,
220
- originalContextLength: 1000000,
221
- originalProvider: 'Xiaomi',
222
- contextLength: 128000,
223
- maxTokens: 8192,
224
- input: ['text', 'image'],
225
- category: 'general',
226
- recommended: false,
227
- isCustom: false,
171
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'general', recommended: false,
228
172
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
229
- },
230
- {
173
+ }),
174
+ createCuratedModel({
231
175
  id: 'cline-pass/mimo-v2.5-pro',
232
176
  name: 'MiMo V2.5 Pro',
233
177
  description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
234
- clineContextLength: 128000,
235
- defaultContextLength: 128000,
236
- originalContextLength: 1000000,
237
- originalProvider: 'Xiaomi',
238
- contextLength: 128000,
239
- maxTokens: 8192,
240
- input: ['text', 'image'],
241
- category: 'coding',
242
- recommended: false,
243
- isCustom: false,
178
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: false,
244
179
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
245
- },
180
+ }),
246
181
  ])
247
182
 
248
183
  /**
@@ -303,6 +238,7 @@ export function parsePlanIncludedModels(includedInput) {
303
238
  const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
304
239
  const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
305
240
  const origCtx = getOriginalModelContext(name)
241
+ const origMax = getOriginalModelMaxTokens(name)
306
242
  const origProv = getOriginalModelProvider(name)
307
243
  matched.push({
308
244
  id: `cline-pass/${idPart}`,
@@ -311,9 +247,11 @@ export function parsePlanIncludedModels(includedInput) {
311
247
  clineContextLength: 200000,
312
248
  defaultContextLength: 200000,
313
249
  originalContextLength: origCtx,
250
+ originalMaxTokens: origMax,
314
251
  originalProvider: origProv,
315
252
  contextLength: 200000,
316
253
  maxTokens: 8192,
254
+ defaultMaxTokens: 8192,
317
255
  input: isVision ? ['text', 'image'] : ['text'],
318
256
  category: hasReasoning ? 'reasoning' : (isVision ? 'multimodal' : 'general'),
319
257
  recommended: false,
@@ -351,26 +289,31 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
351
289
  }
352
290
  }
353
291
 
292
+ const wildcardOv = overridesMap.get('*')
354
293
  function applyOverride(m) {
355
294
  const defaultCtx = m.clineContextLength || m.defaultContextLength || m.contextLength || 200000
295
+ const defaultMax = m.defaultMaxTokens || m.maxTokens || 8192
356
296
  const origCtx = m.originalContextLength || getOriginalModelContext(m.id)
297
+ const origMax = m.originalMaxTokens || getOriginalModelMaxTokens(m.id)
357
298
  const origProv = m.originalProvider || getOriginalModelProvider(m.id)
358
- const ov = overridesMap.get(m.id)
299
+ const ov = overridesMap.get(m.id) || wildcardOv
359
300
  const activeCtx = ov && typeof ov.contextLength === 'number' && ov.contextLength > 0
360
301
  ? ov.contextLength
361
302
  : defaultCtx
362
303
  const activeMaxTokens = ov && typeof ov.maxTokens === 'number' && ov.maxTokens > 0
363
304
  ? ov.maxTokens
364
- : m.maxTokens || 8192
305
+ : defaultMax
365
306
  return {
366
307
  ...m,
367
308
  contextLength: activeCtx,
368
309
  maxTokens: activeMaxTokens,
369
310
  defaultContextLength: defaultCtx,
370
311
  clineContextLength: defaultCtx,
312
+ defaultMaxTokens: defaultMax,
371
313
  originalContextLength: origCtx,
314
+ originalMaxTokens: origMax,
372
315
  originalProvider: origProv,
373
- isContextOverridden: activeCtx !== defaultCtx,
316
+ isContextOverridden: activeCtx !== defaultCtx || activeMaxTokens !== defaultMax,
374
317
  }
375
318
  }
376
319
 
@@ -392,7 +335,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
392
335
  clineContextLength: curated.clineContextLength || curated.contextLength,
393
336
  defaultContextLength: curated.defaultContextLength || curated.contextLength,
394
337
  originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
338
+ originalMaxTokens: curated.originalMaxTokens || getOriginalModelMaxTokens(curated.id),
395
339
  originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
340
+ defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens || 8192,
396
341
  input: dm.input && dm.input.length ? dm.input : curated.input,
397
342
  category: dm.category || curated.category,
398
343
  reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
@@ -404,7 +349,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
404
349
  clineContextLength: dm.contextLength || 200000,
405
350
  defaultContextLength: dm.contextLength || 200000,
406
351
  originalContextLength: dm.originalContextLength || getOriginalModelContext(dm.id),
352
+ originalMaxTokens: dm.originalMaxTokens || getOriginalModelMaxTokens(dm.id),
407
353
  originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
354
+ defaultMaxTokens: dm.maxTokens || 8192,
408
355
  unverified: false,
409
356
  })
410
357
  })
@@ -239,6 +239,22 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
239
239
  }
240
240
  }
241
241
 
242
+ let existingModels = []
243
+ try {
244
+ if (typeof settings.describe === 'function') {
245
+ const descriptors = settings.describe()
246
+ const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
247
+ const existingProv = piAiDesc?.value?.providers?.[PROVIDER_ID]
248
+ if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
249
+ } else if (typeof settings.get === 'function') {
250
+ const piAiVal = settings.get(LLM_PI_AI_NS)
251
+ const existingProv = piAiVal?.providers?.[PROVIDER_ID]
252
+ if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
253
+ }
254
+ } catch {
255
+ /* ignore read error */
256
+ }
257
+
242
258
  const providerObj = buildPiAiProvider({
243
259
  baseUrl: effectiveBaseUrl,
244
260
  apiKeyEnv: isProxy ? LOCAL_PROXY_KEY_ENV : (activeAcc?.apiKeyEnv || pub.apiKeyEnv),
@@ -246,6 +262,7 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
246
262
  dynamicModels: pub.dynamicModels,
247
263
  modelContextOverrides: pub.modelContextOverrides,
248
264
  customModels: pub.customModels,
265
+ existingModels,
249
266
  displayName: PROVIDER_DISPLAY_NAME,
250
267
  })
251
268
 
@@ -257,6 +274,10 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
257
274
  },
258
275
  ])
259
276
 
277
+ ctx?.logger?.info?.(
278
+ `[dsh-clinebot] Synchronized llm-pi-ai provider "${PROVIDER_ID}" with ${providerObj.models.length} models.`
279
+ )
280
+
260
281
  return providerObj
261
282
  }
262
283
 
@@ -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.19'
17
+ let packageVersion = '0.5.21'
18
18
  try {
19
- packageVersion = require('../package.json').version || '0.5.19'
19
+ packageVersion = require('../package.json').version || '0.5.21'
20
20
  } catch (err) {
21
21
  /* fallback to default version if manifest is unreadable */
22
22
  }
@@ -2,7 +2,7 @@ import { writeJson, readBody } from '../http.js'
2
2
  import { assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, plainConfig, Config } from '../config.js'
4
4
  import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
5
- import { getAllModels, saveModelsDiskCache, getOriginalModelContext } from '../models.js'
5
+ import { getAllModels, saveModelsDiskCache, getOriginalModelContext, getOriginalModelMaxTokens } from '../models.js'
6
6
  import { fetchUsageLimits } from '../cline-client.js'
7
7
  import { publicUsage } from '../http.js'
8
8
 
@@ -129,15 +129,36 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
129
129
  let currentOverrides = Array.isArray(pub.modelContextOverrides) ? [...pub.modelContextOverrides] : []
130
130
 
131
131
  if (body.mode === 'all-original') {
132
- // Set all models to their authentic original creator context limits
132
+ // Set all models to their authentic original creator context & token limits
133
133
  currentOverrides = allModels.map((m) => ({
134
134
  modelId: m.id,
135
135
  contextLength: m.originalContextLength || getOriginalModelContext(m.id),
136
- maxTokens: m.maxTokens || 8192,
136
+ maxTokens: m.originalMaxTokens || getOriginalModelMaxTokens(m.id),
137
137
  }))
138
138
  } else if (body.mode === 'all-default' || body.mode === 'reset-all') {
139
139
  // Reset all overrides back to provider defaults
140
140
  currentOverrides = []
141
+ } else if (body.mode === 'apply-all') {
142
+ // Bulk apply limits across all models and set wildcard default
143
+ const rawCtx = Number(body.contextLength)
144
+ const contextLength = Number.isFinite(rawCtx) && rawCtx > 0 ? Math.max(1000, rawCtx) : undefined
145
+ const rawMax = Number(body.maxTokens)
146
+ const maxTokens = Number.isFinite(rawMax) && rawMax > 0 ? Math.max(256, rawMax) : undefined
147
+
148
+ currentOverrides = allModels.map((m) => {
149
+ const existing = currentOverrides.find((o) => o.modelId === m.id)
150
+ return {
151
+ modelId: m.id,
152
+ contextLength: contextLength !== undefined ? contextLength : (existing?.contextLength || m.contextLength),
153
+ maxTokens: maxTokens !== undefined ? maxTokens : (existing?.maxTokens || m.maxTokens),
154
+ }
155
+ })
156
+ currentOverrides = currentOverrides.filter((o) => o.modelId !== '*')
157
+ currentOverrides.push({
158
+ modelId: '*',
159
+ contextLength: contextLength || 200000,
160
+ maxTokens: maxTokens || 16384,
161
+ })
141
162
  } else if (Array.isArray(body.overrides)) {
142
163
  // Bulk array override
143
164
  currentOverrides = body.overrides
@@ -153,11 +174,19 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
153
174
  currentOverrides = currentOverrides.filter((o) => o.modelId !== modelId)
154
175
  } else {
155
176
  const rawCtx = Number(body.contextLength)
156
- const contextLength = Number.isFinite(rawCtx) && rawCtx > 0 ? Math.max(1000, rawCtx) : 200000
157
177
  const rawMax = Number(body.maxTokens)
158
- const maxTokens = Number.isFinite(rawMax) && rawMax > 0 ? Math.max(256, rawMax) : 8192
159
-
160
178
  const existingIdx = currentOverrides.findIndex((o) => o.modelId === modelId)
179
+ const existingEntry = existingIdx >= 0 ? currentOverrides[existingIdx] : null
180
+ const targetModel = allModels.find((m) => m.id === modelId)
181
+
182
+ const contextLength = Number.isFinite(rawCtx) && rawCtx > 0
183
+ ? Math.max(1000, rawCtx)
184
+ : (existingEntry?.contextLength || targetModel?.contextLength || 200000)
185
+
186
+ const maxTokens = Number.isFinite(rawMax) && rawMax > 0
187
+ ? Math.max(256, rawMax)
188
+ : (existingEntry?.maxTokens || targetModel?.maxTokens || 8192)
189
+
161
190
  const newEntry = { modelId, contextLength, maxTokens }
162
191
  if (existingIdx >= 0) {
163
192
  currentOverrides[existingIdx] = newEntry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
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",
@@ -64,7 +64,8 @@
64
64
  "@deepseek-ai/schemastery": "^3.18.1"
65
65
  },
66
66
  "devDependencies": {
67
- "@deepseek-ai/schemastery": "^3.18.1"
67
+ "@deepseek-ai/schemastery": "^3.18.1",
68
+ "eslint": "^9.39.5"
68
69
  },
69
70
  "homepage": "https://github.com/GooDAnDReaDY/dsh-clinebot#readme",
70
71
  "bugs": {