@goodandready/dsh-clinebot 0.4.2 → 0.4.4

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.4.4] - 2026-09-25
9
+
10
+ ### Added
11
+ - **Model Context Customization & Authentic Provider Specs (#110, GitHub #8)**: User-controlled context length configuration allowing custom context windows per model, resetting to gateway defaults (128K/200K), or one-click application of authentic original model specifications (Moonshot Kimi 2M, Alibaba Qwen 1M, MiniMax 1M, Xiaomi MiMo 1M, DeepSeek 128K, Zhipu GLM 128K).
12
+ - **Batch Context Controls (#110)**: Global 1-click batch actions in the models management header ("⚡ Original Specs" and "↺ Reset Contexts") to instantly apply or reset context windows across all models simultaneously.
13
+ - **Immediate DSH Provider Synchronization (#110)**: Dynamic synchronization of overridden context windows directly to `llm-pi-ai` provider settings, preventing chat sessions from prematurely compacting or truncating context at 128k/200k when larger context is selected.
14
+ - **Context Management Endpoint (`POST /dsh-clinebot/models/context`) (#110)**: Full REST route supporting single model updates (`modelId`, `contextLength`, `maxTokens`), single model resets (`reset: true`), batch mode `all-original`, batch mode `all-default`, and array bulk overrides.
15
+ - **Interactive UI & Localization (#110)**: Inline context badges, quick edit popover, clean token input, and bilingual English / Chinese localization across all context actions.
16
+
17
+ ## [0.4.3] - 2026-09-24
18
+
19
+ ### Fixed
20
+ - **DSH Settings Reload & DataCloneError Normalization (#96)**: Extended `plainConfig` in `lib/config.js` to recursively resolve functional getters (`() => current`), nested volatile getter objects, and arrays before passing configuration to `structuredClone`, completely eliminating `DataCloneError: () => current could not be cloned` during Cordis plugin reload and settings updates.
21
+ - **Config Schema Non-Volatile Alignment (#96)**: Removed the `.volatile()` marker from `planSyncedAt`, ensuring that internal synchronization timestamps are preserved as runtime state and not serialized into user settings forms.
22
+ - **Boot and Reload Regression Test (#96)**: Added regression tests covering deep functional getter resolution in `plainConfig` and verifying host boot/reload stability with profile-shaped `dynamicModels`.
23
+
8
24
  ## [0.4.2] - 2026-09-24
9
25
 
10
26
  ### Fixed
package/README.md CHANGED
@@ -182,6 +182,7 @@ dsh-clinebot:
182
182
  | `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI-compatible base URL |
183
183
  | `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Environment variable / credentials key name |
184
184
  | `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | Default selected model ID |
185
+ | `modelContextOverrides` | `array` | `[]` | User-defined model context length and max token overrides |
185
186
  | `timeoutMs` | `number` | `15000` | HTTP request timeout in milliseconds |
186
187
  | `smokeTimeoutMs` | `number` | `25000` | Smoke test latency ping timeout |
187
188
  | `enabledModels` | `array` | `[...]` | List of models exposed in the DSH chat picker |
package/README.ru.md CHANGED
@@ -177,6 +177,7 @@ dsh-clinebot:
177
177
  | `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | Базовый URL OpenAI-совместимого API ClinePass |
178
178
  | `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Имя переменной / ключа в хранилище credentials |
179
179
  | `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | Модель, выбираемая по умолчанию |
180
+ | `modelContextOverrides` | `array` | `[]` | Пользовательские переопределения размера контекста и лимита токенов |
180
181
  | `timeoutMs` | `number` | `15000` | Таймаут HTTP-запросов (мс) |
181
182
  | `smokeTimeoutMs` | `number` | `25000` | Таймаут тестового пинга (мс) |
182
183
  | `enabledModels` | `array` | `[...]` | Список моделей, активных в селекторе чата |
package/README.zh.md CHANGED
@@ -179,6 +179,7 @@ dsh-clinebot:
179
179
  | `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI 兼容接口地址 |
180
180
  | `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | 凭据管理系统中的密钥名称 |
181
181
  | `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | 默认选中的模型 ID |
182
+ | `modelContextOverrides` | `array` | `[]` | 用户自定义模型上下文窗口与最大 Token 数量覆盖 |
182
183
  | `timeoutMs` | `number` | `15000` | HTTP 请求超时时间(毫秒) |
183
184
  | `smokeTimeoutMs` | `number` | `25000` | 探活测试超时时间(毫秒) |
184
185
  | `enabledModels` | `array` | `[...]` | 允许在聊天下拉框中显示的可用模型列表 |
package/lib/client.js CHANGED
@@ -109,6 +109,18 @@ const en = {
109
109
  'models.th_caps': 'Capabilities',
110
110
  'models.star': 'Star',
111
111
  'models.auto_new': 'New',
112
+ 'models.apply_original_all': '⚡ Original Specs',
113
+ 'models.reset_contexts_all': '↺ Reset Contexts',
114
+ 'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
115
+ 'models.ctx_default': 'Default ({ctx})',
116
+ 'models.ctx_original': 'Original ({ctx} · {provider})',
117
+ 'models.ctx_custom': 'Custom…',
118
+ 'models.ctx_edit': 'Edit Context Window',
119
+ 'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
120
+ 'models.ctx_save': 'Save',
121
+ 'models.ctx_cancel': 'Cancel',
122
+ 'models.ctx_tag_original': 'Original',
123
+ 'models.ctx_tag_custom': 'Custom',
112
124
  'stats.title': '📈 Current DSH Session Telemetry',
113
125
  'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
114
126
  'stats.requests': 'Successful / Total Requests',
@@ -219,6 +231,18 @@ const en = {
219
231
  'models.th_caps': '特性能力',
220
232
  'models.star': '推荐',
221
233
  'models.auto_new': '新上线',
234
+ 'models.apply_original_all': '⚡ 原厂规格',
235
+ 'models.reset_contexts_all': '↺ 重置上下文',
236
+ 'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
237
+ 'models.ctx_default': '默认 ({ctx})',
238
+ 'models.ctx_original': '原厂 ({ctx} · {provider})',
239
+ 'models.ctx_custom': '自定义…',
240
+ 'models.ctx_edit': '编辑上下文窗口',
241
+ 'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
242
+ 'models.ctx_save': '保存',
243
+ 'models.ctx_cancel': '取消',
244
+ 'models.ctx_tag_original': '原厂',
245
+ 'models.ctx_tag_custom': '自定义',
222
246
  'stats.title': '📈 当前 DSH 会话请求遥测',
223
247
  'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
224
248
  'stats.requests': '成功 / 总请求次数',
@@ -935,6 +959,7 @@ function ModelsSection({
935
959
  keyPresent,
936
960
  busy,
937
961
  handleSyncPlanModels,
962
+ handleUpdateModelContext,
938
963
  handleSetModelsFilter,
939
964
  handleToggleModel,
940
965
  planSynced,
@@ -945,6 +970,21 @@ function ModelsSection({
945
970
  const syncDateStr = planSyncedAt ? new Date(planSyncedAt).toLocaleDateString() : ''
946
971
  const [search, setSearch] = React.useState('')
947
972
  const [viewFilter, setViewFilter] = React.useState('all')
973
+ const [editingModelId, setEditingModelId] = React.useState(null)
974
+ const [customCtxVal, setCustomCtxVal] = React.useState('')
975
+
976
+ function formatCtx(num) {
977
+ num = Number(num) || 200000
978
+ if (num >= 1000000) {
979
+ const m = num / 1000000
980
+ return `${m % 1 === 0 ? m : m.toFixed(1)}M`
981
+ }
982
+ if (num >= 1000) {
983
+ const k = num / 1000
984
+ return `${k % 1 === 0 ? k : k.toFixed(1)}K`
985
+ }
986
+ return String(num)
987
+ }
948
988
 
949
989
  const filteredModels = modelsList.filter((m) => {
950
990
  if (viewFilter === 'vision') {
@@ -998,6 +1038,28 @@ function ModelsSection({
998
1038
  },
999
1039
  busy === 'sync-models' ? t('models.syncing') : t('models.sync')
1000
1040
  ),
1041
+ React.createElement(
1042
+ 'button',
1043
+ {
1044
+ type: 'button',
1045
+ className: 'cb-btn',
1046
+ disabled: !!busy,
1047
+ title: t('models.apply_original_all'),
1048
+ onClick: () => typeof handleUpdateModelContext === 'function' && handleUpdateModelContext({ mode: 'all-original' }),
1049
+ },
1050
+ t('models.apply_original_all')
1051
+ ),
1052
+ React.createElement(
1053
+ 'button',
1054
+ {
1055
+ type: 'button',
1056
+ className: 'cb-btn',
1057
+ disabled: !!busy,
1058
+ title: t('models.reset_contexts_all'),
1059
+ onClick: () => typeof handleUpdateModelContext === 'function' && handleUpdateModelContext({ mode: 'all-default' }),
1060
+ },
1061
+ t('models.reset_contexts_all')
1062
+ ),
1001
1063
  React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') }, t('models.all')),
1002
1064
  React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') }, t('models.vision')),
1003
1065
  React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') }, t('models.coding')),
@@ -1103,6 +1165,12 @@ function ModelsSection({
1103
1165
  null,
1104
1166
  filteredModels.map((m) => {
1105
1167
  const isEnabled = !disabledSet.has(m.id)
1168
+ const defaultCtx = m.defaultContextLength || m.clineContextLength || 200000
1169
+ const origCtx = m.originalContextLength || 200000
1170
+ const origProv = m.originalProvider || 'Original'
1171
+ const isOverridden = Boolean(m.isContextOverridden)
1172
+ const isOriginal = isOverridden && (m.contextLength === origCtx)
1173
+
1106
1174
  return React.createElement(
1107
1175
  'tr',
1108
1176
  { key: m.id },
@@ -1127,7 +1195,140 @@ function ModelsSection({
1127
1195
  : null
1128
1196
  ),
1129
1197
  React.createElement('td', null, React.createElement('code', null, m.id)),
1130
- React.createElement('td', null, `${Math.round((m.contextLength || 128000) / 1000)}k`),
1198
+ React.createElement(
1199
+ 'td',
1200
+ null,
1201
+ editingModelId === m.id
1202
+ ? React.createElement(
1203
+ 'div',
1204
+ {
1205
+ style: {
1206
+ display: 'flex',
1207
+ flexDirection: 'column',
1208
+ gap: '6px',
1209
+ background: 'var(--dsw-alias-bg-hover, rgba(0,0,0,0.04))',
1210
+ padding: '6px',
1211
+ borderRadius: '6px',
1212
+ border: '1px solid var(--dsw-alias-border-l2)',
1213
+ },
1214
+ },
1215
+ React.createElement(
1216
+ 'div',
1217
+ { style: { display: 'flex', gap: '4px', flexWrap: 'wrap' } },
1218
+ React.createElement(
1219
+ 'button',
1220
+ {
1221
+ type: 'button',
1222
+ className: `cb-btn ${!isOverridden ? 'cb-btn-active' : ''}`,
1223
+ style: { padding: '2px 6px', fontSize: '11px' },
1224
+ onClick: () => {
1225
+ if (typeof handleUpdateModelContext === 'function') {
1226
+ handleUpdateModelContext({ modelId: m.id, reset: true })
1227
+ }
1228
+ setEditingModelId(null)
1229
+ },
1230
+ },
1231
+ t('models.ctx_default', { ctx: formatCtx(defaultCtx) })
1232
+ ),
1233
+ React.createElement(
1234
+ 'button',
1235
+ {
1236
+ type: 'button',
1237
+ className: `cb-btn ${isOriginal ? 'cb-btn-active' : ''}`,
1238
+ style: { padding: '2px 6px', fontSize: '11px' },
1239
+ onClick: () => {
1240
+ if (typeof handleUpdateModelContext === 'function') {
1241
+ handleUpdateModelContext({ modelId: m.id, contextLength: origCtx })
1242
+ }
1243
+ setEditingModelId(null)
1244
+ },
1245
+ },
1246
+ t('models.ctx_original', { ctx: formatCtx(origCtx), provider: origProv })
1247
+ )
1248
+ ),
1249
+ React.createElement(
1250
+ 'div',
1251
+ { style: { display: 'flex', gap: '4px', alignItems: 'center' } },
1252
+ React.createElement('input', {
1253
+ type: 'number',
1254
+ className: 'cb-input',
1255
+ style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '120px' },
1256
+ placeholder: t('models.ctx_custom_placeholder'),
1257
+ value: customCtxVal,
1258
+ onChange: (e) => setCustomCtxVal(e.target.value),
1259
+ }),
1260
+ React.createElement(
1261
+ 'button',
1262
+ {
1263
+ type: 'button',
1264
+ className: 'cb-btn cb-btn-primary',
1265
+ style: { padding: '2px 8px', fontSize: '11px' },
1266
+ onClick: () => {
1267
+ const val = parseInt(customCtxVal, 10)
1268
+ if (val > 0 && typeof handleUpdateModelContext === 'function') {
1269
+ handleUpdateModelContext({ modelId: m.id, contextLength: val })
1270
+ }
1271
+ setEditingModelId(null)
1272
+ },
1273
+ },
1274
+ t('models.ctx_save')
1275
+ ),
1276
+ React.createElement(
1277
+ 'button',
1278
+ {
1279
+ type: 'button',
1280
+ className: 'cb-btn',
1281
+ style: { padding: '2px 6px', fontSize: '11px' },
1282
+ onClick: () => setEditingModelId(null),
1283
+ },
1284
+ t('models.ctx_cancel')
1285
+ )
1286
+ )
1287
+ )
1288
+ : React.createElement(
1289
+ 'div',
1290
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
1291
+ React.createElement(
1292
+ 'span',
1293
+ {
1294
+ style: { fontWeight: '600', cursor: 'pointer' },
1295
+ onClick: () => {
1296
+ setCustomCtxVal(String(m.contextLength || defaultCtx))
1297
+ setEditingModelId(m.id)
1298
+ },
1299
+ },
1300
+ formatCtx(m.contextLength || defaultCtx)
1301
+ ),
1302
+ isOverridden
1303
+ ? React.createElement(
1304
+ 'span',
1305
+ {
1306
+ className: isOriginal ? 'cb-badge cb-badge-ok' : 'cb-badge cb-badge-warn',
1307
+ style: { fontSize: '10px', padding: '1px 6px', cursor: 'pointer' },
1308
+ onClick: () => {
1309
+ setCustomCtxVal(String(m.contextLength || defaultCtx))
1310
+ setEditingModelId(m.id)
1311
+ },
1312
+ },
1313
+ isOriginal ? t('models.ctx_tag_original') : t('models.ctx_tag_custom')
1314
+ )
1315
+ : null,
1316
+ React.createElement(
1317
+ 'button',
1318
+ {
1319
+ type: 'button',
1320
+ className: 'cb-btn',
1321
+ style: { padding: '2px 6px', fontSize: '11px', lineHeight: '1', border: 'none', background: 'transparent' },
1322
+ title: t('models.ctx_edit'),
1323
+ onClick: () => {
1324
+ setCustomCtxVal(String(m.contextLength || defaultCtx))
1325
+ setEditingModelId(m.id)
1326
+ },
1327
+ },
1328
+ '✎'
1329
+ )
1330
+ )
1331
+ ),
1131
1332
  React.createElement(
1132
1333
  'td',
1133
1334
  null,
@@ -1393,6 +1594,20 @@ function SettingsPage(props) {
1393
1594
  })
1394
1595
  }
1395
1596
 
1597
+ async function handleUpdateModelContext(payload) {
1598
+ await performAction('update-context', async () => {
1599
+ const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
1600
+ method: 'POST',
1601
+ headers: { 'Content-Type': 'application/json' },
1602
+ body: JSON.stringify(payload),
1603
+ })
1604
+ const data = await res.json().catch(() => ({}))
1605
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1606
+ setMsg(t('models.context_updated_msg'))
1607
+ await load()
1608
+ })
1609
+ }
1610
+
1396
1611
  async function handleSyncPlanModels() {
1397
1612
  await performAction('sync-models', async () => {
1398
1613
  const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
@@ -1689,6 +1904,7 @@ function SettingsPage(props) {
1689
1904
  keyPresent,
1690
1905
  busy,
1691
1906
  handleSyncPlanModels,
1907
+ handleUpdateModelContext,
1692
1908
  handleSetModelsFilter,
1693
1909
  handleToggleModel,
1694
1910
  planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
package/lib/config.js CHANGED
@@ -52,7 +52,13 @@ export const Config = z.object({
52
52
  })).default([])
53
53
  .description('Models automatically discovered from the official ClinePass subscription plan.'),
54
54
  planSyncedAt: z.number().default(0)
55
- .description('Timestamp when models were last synchronized with active subscription plan.').volatile(),
55
+ .description('Timestamp when models were last synchronized with active subscription plan.'),
56
+ modelContextOverrides: z.array(z.object({
57
+ modelId: z.string(),
58
+ contextLength: z.number().default(200000),
59
+ maxTokens: z.number().default(8192),
60
+ })).default([])
61
+ .description('Custom user-defined model context length and max token overrides.').volatile(),
56
62
  timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
57
63
  .description('HTTP probe timeout in milliseconds.').volatile(),
58
64
  smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
@@ -73,15 +79,20 @@ function isVolatileRef(value) {
73
79
  return !!value && typeof value === 'object' && !Array.isArray(value) && typeof value.get === 'function'
74
80
  }
75
81
 
76
- // DSH stores each volatile field as { get }. structuredClone cannot copy that
77
- // getter, so callers need a plain snapshot before validation.
82
+ // DSH stores each volatile field as { get } or getter functions. structuredClone
83
+ // cannot copy functions or getters, so callers need a plain snapshot before validation.
78
84
  export function plainConfig(cfg) {
85
+ while (typeof cfg === 'function') {
86
+ cfg = cfg()
87
+ }
79
88
  if (isVolatileRef(cfg)) return plainConfig(cfg.get())
80
89
  if (!cfg || typeof cfg !== 'object') return cfg
90
+ if (Array.isArray(cfg)) {
91
+ return cfg.map((item) => plainConfig(item))
92
+ }
81
93
  const out = {}
82
94
  for (const key of Object.keys(cfg)) {
83
- const value = cfg[key]
84
- out[key] = isVolatileRef(value) ? value.get() : value
95
+ out[key] = plainConfig(cfg[key])
85
96
  }
86
97
  return out
87
98
  }
@@ -101,8 +112,9 @@ export function volatileConfig(cfg) {
101
112
  export function publicConfig(cfg) {
102
113
  cfg = plainConfig(cfg)
103
114
  const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
115
+ const modelContextOverrides = Array.isArray(cfg?.modelContextOverrides) ? cfg.modelContextOverrides : []
104
116
  const isSynced = dynamic.length > 0
105
- const allModels = getAllModels(dynamic)
117
+ const allModels = getAllModels(dynamic, modelContextOverrides)
106
118
  const allDefaultIds = allModels.map((m) => m.id)
107
119
 
108
120
  let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
@@ -136,6 +148,7 @@ export function publicConfig(cfg) {
136
148
  dynamicModels: dynamic,
137
149
  planSynced: isSynced,
138
150
  planSyncedAt,
151
+ modelContextOverrides,
139
152
  disabledModels: disabledList,
140
153
  enabledModels: activeIds,
141
154
  timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
package/lib/models.js CHANGED
@@ -6,18 +6,54 @@
6
6
  * a uniform 200,000 token context window cap across subscription completion routes,
7
7
  * and /users/me/plan returns only feature strings without per-model context specs.
8
8
  * This catalogue provides the official ClinePass models (200k gateway capacity) plus
9
- * support for user-defined custom models with arbitrary context length.
9
+ * support for user-defined custom models with arbitrary context length and authentic
10
+ * original model creator specifications (Moonshot 2M, Qwen 1M, MiniMax 1M, MiMo 1M, etc.).
10
11
  */
11
12
 
12
13
  export const PROVIDER_ID = 'clinebot'
13
14
  export const PROVIDER_DISPLAY_NAME = 'ClineBot (ClinePass)'
14
15
  export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4-flash'
15
16
 
17
+ /**
18
+ * Authentic original model specifications from original creators:
19
+ * Moonshot AI (Kimi): 2,000,000 tokens
20
+ * Alibaba Cloud (Qwen): 1,000,000 tokens
21
+ * MiniMax: 1,000,000 tokens
22
+ * Xiaomi (MiMo): 1,000,000 tokens
23
+ * DeepSeek: 128,000 tokens
24
+ * Zhipu AI (GLM): 128,000 tokens
25
+ */
26
+ export function getOriginalModelContext(nameOrId) {
27
+ const norm = String(nameOrId || '').toLowerCase()
28
+ if (norm.includes('kimi')) return 2000000
29
+ if (norm.includes('qwen')) return 1000000
30
+ if (norm.includes('minimax')) return 1000000
31
+ if (norm.includes('mimo')) return 1000000
32
+ if (norm.includes('deepseek')) return 128000
33
+ if (norm.includes('glm')) return 128000
34
+ return 200000
35
+ }
36
+
37
+ export function getOriginalModelProvider(nameOrId) {
38
+ const norm = String(nameOrId || '').toLowerCase()
39
+ if (norm.includes('kimi')) return 'Moonshot AI'
40
+ if (norm.includes('qwen')) return 'Alibaba Cloud'
41
+ if (norm.includes('minimax')) return 'MiniMax'
42
+ if (norm.includes('mimo')) return 'Xiaomi'
43
+ if (norm.includes('deepseek')) return 'DeepSeek'
44
+ if (norm.includes('glm')) return 'Zhipu AI'
45
+ return 'Original Vendor'
46
+ }
47
+
16
48
  export const CLINE_MODELS = Object.freeze([
17
49
  {
18
50
  id: 'cline-pass/deepseek-v4-flash',
19
51
  name: 'DeepSeek V4 Flash',
20
52
  description: 'High-speed reasoning & code completion model optimized for agentic loops.',
53
+ clineContextLength: 128000,
54
+ defaultContextLength: 128000,
55
+ originalContextLength: 128000,
56
+ originalProvider: 'DeepSeek',
21
57
  contextLength: 128000,
22
58
  maxTokens: 8192,
23
59
  input: ['text', 'image'],
@@ -30,6 +66,10 @@ export const CLINE_MODELS = Object.freeze([
30
66
  id: 'cline-pass/deepseek-v4-pro',
31
67
  name: 'DeepSeek V4 Pro',
32
68
  description: 'Flagship reasoning and multi-turn architectural coding model.',
69
+ clineContextLength: 128000,
70
+ defaultContextLength: 128000,
71
+ originalContextLength: 128000,
72
+ originalProvider: 'DeepSeek',
33
73
  contextLength: 128000,
34
74
  maxTokens: 8192,
35
75
  input: ['text', 'image'],
@@ -42,6 +82,10 @@ export const CLINE_MODELS = Object.freeze([
42
82
  id: 'cline-pass/glm-5.2',
43
83
  name: 'GLM 5.2',
44
84
  description: 'Bilingual general & coding model with strong instruction following.',
85
+ clineContextLength: 128000,
86
+ defaultContextLength: 128000,
87
+ originalContextLength: 128000,
88
+ originalProvider: 'Zhipu AI',
45
89
  contextLength: 128000,
46
90
  maxTokens: 8192,
47
91
  input: ['text'],
@@ -54,6 +98,10 @@ export const CLINE_MODELS = Object.freeze([
54
98
  id: 'cline-pass/kimi-k3',
55
99
  name: 'Kimi K3',
56
100
  description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
101
+ clineContextLength: 200000,
102
+ defaultContextLength: 200000,
103
+ originalContextLength: 2000000,
104
+ originalProvider: 'Moonshot AI',
57
105
  contextLength: 200000,
58
106
  maxTokens: 8192,
59
107
  input: ['text'],
@@ -66,6 +114,10 @@ export const CLINE_MODELS = Object.freeze([
66
114
  id: 'cline-pass/kimi-k2.7-code',
67
115
  name: 'Kimi K2.7 Code',
68
116
  description: 'Specialized coding model with agentic tool calling support.',
117
+ clineContextLength: 200000,
118
+ defaultContextLength: 200000,
119
+ originalContextLength: 2000000,
120
+ originalProvider: 'Moonshot AI',
69
121
  contextLength: 200000,
70
122
  maxTokens: 8192,
71
123
  input: ['text'],
@@ -77,6 +129,10 @@ export const CLINE_MODELS = Object.freeze([
77
129
  id: 'cline-pass/kimi-k2.6',
78
130
  name: 'Kimi K2.6',
79
131
  description: 'Balanced long-context conversational model.',
132
+ clineContextLength: 200000,
133
+ defaultContextLength: 200000,
134
+ originalContextLength: 2000000,
135
+ originalProvider: 'Moonshot AI',
80
136
  contextLength: 200000,
81
137
  maxTokens: 8192,
82
138
  input: ['text'],
@@ -88,6 +144,10 @@ export const CLINE_MODELS = Object.freeze([
88
144
  id: 'cline-pass/qwen3.7-max',
89
145
  name: 'Qwen 3.7 Max',
90
146
  description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
147
+ clineContextLength: 128000,
148
+ defaultContextLength: 128000,
149
+ originalContextLength: 1000000,
150
+ originalProvider: 'Alibaba Cloud',
91
151
  contextLength: 128000,
92
152
  maxTokens: 8192,
93
153
  input: ['text', 'image'],
@@ -100,6 +160,10 @@ export const CLINE_MODELS = Object.freeze([
100
160
  id: 'cline-pass/qwen3.7-plus',
101
161
  name: 'Qwen 3.7 Plus',
102
162
  description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
163
+ clineContextLength: 128000,
164
+ defaultContextLength: 128000,
165
+ originalContextLength: 1000000,
166
+ originalProvider: 'Alibaba Cloud',
103
167
  contextLength: 128000,
104
168
  maxTokens: 8192,
105
169
  input: ['text', 'image'],
@@ -112,6 +176,10 @@ export const CLINE_MODELS = Object.freeze([
112
176
  id: 'cline-pass/minimax-m3',
113
177
  name: 'MiniMax M3',
114
178
  description: 'High throughput reasoning and synthesis model.',
179
+ clineContextLength: 200000,
180
+ defaultContextLength: 200000,
181
+ originalContextLength: 1000000,
182
+ originalProvider: 'MiniMax',
115
183
  contextLength: 200000,
116
184
  maxTokens: 8192,
117
185
  input: ['text'],
@@ -124,6 +192,10 @@ export const CLINE_MODELS = Object.freeze([
124
192
  id: 'cline-pass/mimo-v2.5',
125
193
  name: 'MiMo V2.5',
126
194
  description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
195
+ clineContextLength: 128000,
196
+ defaultContextLength: 128000,
197
+ originalContextLength: 1000000,
198
+ originalProvider: 'Xiaomi',
127
199
  contextLength: 128000,
128
200
  maxTokens: 8192,
129
201
  input: ['text', 'image'],
@@ -136,6 +208,10 @@ export const CLINE_MODELS = Object.freeze([
136
208
  id: 'cline-pass/mimo-v2.5-pro',
137
209
  name: 'MiMo V2.5 Pro',
138
210
  description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
211
+ clineContextLength: 128000,
212
+ defaultContextLength: 128000,
213
+ originalContextLength: 1000000,
214
+ originalProvider: 'Xiaomi',
139
215
  contextLength: 128000,
140
216
  maxTokens: 8192,
141
217
  input: ['text', 'image'],
@@ -203,10 +279,16 @@ export function parsePlanIncludedModels(includedInput) {
203
279
  .replace(/-+/g, '-')
204
280
  const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
205
281
  const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
282
+ const origCtx = getOriginalModelContext(name)
283
+ const origProv = getOriginalModelProvider(name)
206
284
  matched.push({
207
285
  id: `cline-pass/${idPart}`,
208
286
  name,
209
287
  description: `Official ClinePass subscription model: ${name}`,
288
+ clineContextLength: 200000,
289
+ defaultContextLength: 200000,
290
+ originalContextLength: origCtx,
291
+ originalProvider: origProv,
210
292
  contextLength: 200000,
211
293
  maxTokens: 8192,
212
294
  input: isVision ? ['text', 'image'] : ['text'],
@@ -228,8 +310,40 @@ export function parsePlanIncludedModels(includedInput) {
228
310
  * with curated capabilities, reasoning efforts, and descriptions from CLINE_MODELS.
229
311
  * 2. When plan is unsynced (dynamicModels is empty):
230
312
  * Falls back to the full curated CLINE_MODELS catalog marked with unverified: true.
313
+ *
314
+ * Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
231
315
  */
232
- export function getAllModels(dynamicModels = []) {
316
+ export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
317
+ const overridesMap = new Map()
318
+ if (Array.isArray(modelContextOverrides)) {
319
+ for (const ov of modelContextOverrides) {
320
+ if (ov && ov.modelId) overridesMap.set(ov.modelId, ov)
321
+ }
322
+ }
323
+
324
+ function applyOverride(m) {
325
+ const defaultCtx = m.clineContextLength || m.defaultContextLength || m.contextLength || 200000
326
+ const origCtx = m.originalContextLength || getOriginalModelContext(m.id)
327
+ const origProv = m.originalProvider || getOriginalModelProvider(m.id)
328
+ const ov = overridesMap.get(m.id)
329
+ const activeCtx = ov && typeof ov.contextLength === 'number' && ov.contextLength > 0
330
+ ? ov.contextLength
331
+ : defaultCtx
332
+ const activeMaxTokens = ov && typeof ov.maxTokens === 'number' && ov.maxTokens > 0
333
+ ? ov.maxTokens
334
+ : m.maxTokens || 8192
335
+ return {
336
+ ...m,
337
+ contextLength: activeCtx,
338
+ maxTokens: activeMaxTokens,
339
+ defaultContextLength: defaultCtx,
340
+ clineContextLength: defaultCtx,
341
+ originalContextLength: origCtx,
342
+ originalProvider: origProv,
343
+ isContextOverridden: activeCtx !== defaultCtx,
344
+ }
345
+ }
346
+
233
347
  if (Array.isArray(dynamicModels) && dynamicModels.length > 0) {
234
348
  const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
235
349
  return dynamicModels.map((dm) => {
@@ -238,41 +352,49 @@ export function getAllModels(dynamicModels = []) {
238
352
  (cm) => cm.id === dm.id || normalize(cm.id.replace(/^cline-pass\//, '')) === targetNorm || normalize(cm.name) === targetNorm
239
353
  )
240
354
  if (curated) {
241
- return {
355
+ return applyOverride({
242
356
  ...curated,
243
357
  ...dm,
244
358
  description: dm.description || curated.description,
245
359
  contextLength: dm.contextLength || curated.contextLength,
246
360
  maxTokens: dm.maxTokens || curated.maxTokens,
361
+ clineContextLength: curated.clineContextLength || curated.contextLength,
362
+ defaultContextLength: curated.defaultContextLength || curated.contextLength,
363
+ originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
364
+ originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
247
365
  input: dm.input && dm.input.length ? dm.input : curated.input,
248
366
  category: dm.category || curated.category,
249
367
  reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
250
368
  unverified: false,
251
- }
369
+ })
252
370
  }
253
- return {
371
+ return applyOverride({
254
372
  ...dm,
373
+ clineContextLength: dm.contextLength || 200000,
374
+ defaultContextLength: dm.contextLength || 200000,
375
+ originalContextLength: dm.originalContextLength || getOriginalModelContext(dm.id),
376
+ originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
255
377
  unverified: false,
256
- }
378
+ })
257
379
  })
258
380
  }
259
381
 
260
- return CLINE_MODELS.map((m) => ({ ...m, unverified: true }))
382
+ return CLINE_MODELS.map((m) => applyOverride({ ...m, unverified: true }))
261
383
  }
262
384
 
263
- export function findModel(id, dynamicModels = []) {
264
- const all = getAllModels(dynamicModels)
385
+ export function findModel(id, dynamicModels = [], modelContextOverrides = []) {
386
+ const all = getAllModels(dynamicModels, modelContextOverrides)
265
387
  const found = all.find((m) => m.id === id)
266
388
  if (found) return found
267
389
  return CLINE_MODELS.find((m) => m.id === id) || null
268
390
  }
269
391
 
270
- export function isSupportedModel(id, dynamicModels = []) {
271
- return findModel(id, dynamicModels) !== null
392
+ export function isSupportedModel(id, dynamicModels = [], modelContextOverrides = []) {
393
+ return findModel(id, dynamicModels, modelContextOverrides) !== null
272
394
  }
273
395
 
274
- export function getDefaultModelIds(dynamicModels = []) {
275
- return getAllModels(dynamicModels).map((m) => m.id)
396
+ export function getDefaultModelIds(dynamicModels = [], modelContextOverrides = []) {
397
+ return getAllModels(dynamicModels, modelContextOverrides).map((m) => m.id)
276
398
  }
277
399
 
278
400
  /**
@@ -287,12 +409,18 @@ export function getActiveModelIds(allModels = [], disabledModelIds = []) {
287
409
  }
288
410
 
289
411
  /**
290
- * Format context length cleanly (e.g. 200000 -> '200K', 128000 -> '128K').
412
+ * Format context length cleanly (e.g. 2000000 -> '2M', 1000000 -> '1M', 200000 -> '200K', 128000 -> '128K').
291
413
  */
292
414
  export function formatModelContext(contextLength) {
293
415
  const num = Number(contextLength) || 200000
294
- if (num >= 1000000) return `${Math.round(num / 1000000)}M`
295
- if (num >= 1000) return `${Math.round(num / 1000)}K`
416
+ if (num >= 1000000) {
417
+ const m = num / 1000000
418
+ return `${m % 1 === 0 ? m : m.toFixed(1)}M`
419
+ }
420
+ if (num >= 1000) {
421
+ const k = num / 1000
422
+ return `${k % 1 === 0 ? k : k.toFixed(1)}K`
423
+ }
296
424
  return String(num)
297
425
  }
298
426
 
@@ -318,8 +446,8 @@ export function formatModelDescription(model) {
318
446
  /**
319
447
  * Check if a model supports vision input.
320
448
  */
321
- export function isVisionModel(id, dynamicModels = []) {
322
- const m = findModel(id, dynamicModels)
449
+ export function isVisionModel(id, dynamicModels = [], modelContextOverrides = []) {
450
+ const m = findModel(id, dynamicModels, modelContextOverrides)
323
451
  if (!m) return false
324
452
  return Boolean(m.input?.includes('image') || m.input?.includes('vision'))
325
453
  }
@@ -356,4 +484,3 @@ export async function loadModelsDiskCache(cachePath) {
356
484
  return null
357
485
  }
358
486
  }
359
-
@@ -88,7 +88,7 @@ export async function buildStatus(ctx, cfg) {
88
88
  ])
89
89
 
90
90
  const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
91
- const allModels = getAllModels(pub.dynamicModels)
91
+ const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
92
92
 
93
93
  let usage = null
94
94
  const keyToUse = activeAcc.value || key.value
@@ -156,7 +156,7 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
156
156
  }
157
157
 
158
158
  const pub = publicConfig(cfg)
159
- const allModels = getAllModels(pub.dynamicModels)
159
+ const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
160
160
  const allowedSet = new Set(activeModelIds || pub.enabledModels)
161
161
  const modelsToRegister = allModels.filter((m) => allowedSet.has(m.id))
162
162
 
@@ -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 } from '../models.js'
5
+ import { getAllModels, saveModelsDiskCache, getOriginalModelContext } from '../models.js'
6
6
  import { fetchUsageLimits } from '../cline-client.js'
7
7
  import { publicUsage } from '../http.js'
8
8
 
@@ -33,7 +33,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
33
33
  }
34
34
 
35
35
  const dynamicModels = Array.isArray(usageData.dynamicModels) ? usageData.dynamicModels : []
36
- const allModels = getAllModels(dynamicModels)
36
+ const allModels = getAllModels(dynamicModels, pub.modelContextOverrides)
37
37
  const settingsApi = getSettingsApi()
38
38
  if (!settingsApi || typeof settingsApi.replace !== 'function') {
39
39
  return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
@@ -82,11 +82,12 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
82
82
  return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
83
83
  }
84
84
 
85
+ const pub = publicConfig(live())
85
86
  const patch = {}
86
87
  if (Array.isArray(body.disabledModels)) {
87
88
  patch.disabledModels = body.disabledModels
88
89
  } else if (Array.isArray(body.enabledModels)) {
89
- const allModels = getAllModels(publicConfig(live()).dynamicModels)
90
+ const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
90
91
  const enabledSet = new Set(body.enabledModels)
91
92
  patch.disabledModels = allModels.map((m) => m.id).filter((id) => !enabledSet.has(id))
92
93
  }
@@ -101,6 +102,87 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
101
102
  },
102
103
  }), 'dsh-clinebot: /models/toggle')
103
104
 
105
+ // POST /dsh-clinebot/models/context — configure custom context length and token limits
106
+ ctx.effect(() => ctx.webServer.register({
107
+ kind: 'exact',
108
+ path: '/dsh-clinebot/models/context',
109
+ handler: async (req, res) => {
110
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
111
+ if (!assertTrustedSettingsRequest(req, res)) return
112
+ try {
113
+ const bodyBuf = await readBody(req)
114
+ let body = {}
115
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
116
+
117
+ const settingsApi = getSettingsApi()
118
+ if (!settingsApi || typeof settingsApi.replace !== 'function') {
119
+ return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
120
+ }
121
+
122
+ const pub = publicConfig(live())
123
+ const allModels = getAllModels(pub.dynamicModels, [])
124
+ let currentOverrides = Array.isArray(pub.modelContextOverrides) ? [...pub.modelContextOverrides] : []
125
+
126
+ if (body.mode === 'all-original') {
127
+ // Set all models to their authentic original creator context limits
128
+ currentOverrides = allModels.map((m) => ({
129
+ modelId: m.id,
130
+ contextLength: m.originalContextLength || getOriginalModelContext(m.id),
131
+ maxTokens: m.maxTokens || 8192,
132
+ }))
133
+ } else if (body.mode === 'all-default' || body.mode === 'reset-all') {
134
+ // Reset all overrides back to provider defaults
135
+ currentOverrides = []
136
+ } else if (Array.isArray(body.overrides)) {
137
+ // Bulk array override
138
+ currentOverrides = body.overrides
139
+ .filter((o) => o && typeof o.modelId === 'string')
140
+ .map((o) => ({
141
+ modelId: o.modelId,
142
+ contextLength: Math.max(1000, Number(o.contextLength) || 200000),
143
+ maxTokens: Math.max(256, Number(o.maxTokens) || 8192),
144
+ }))
145
+ } else if (body.modelId) {
146
+ const modelId = String(body.modelId)
147
+ if (body.reset) {
148
+ currentOverrides = currentOverrides.filter((o) => o.modelId !== modelId)
149
+ } else {
150
+ const rawCtx = Number(body.contextLength)
151
+ const contextLength = Number.isFinite(rawCtx) && rawCtx > 0 ? Math.max(1000, rawCtx) : 200000
152
+ const rawMax = Number(body.maxTokens)
153
+ const maxTokens = Number.isFinite(rawMax) && rawMax > 0 ? Math.max(256, rawMax) : 8192
154
+
155
+ const existingIdx = currentOverrides.findIndex((o) => o.modelId === modelId)
156
+ const newEntry = { modelId, contextLength, maxTokens }
157
+ if (existingIdx >= 0) {
158
+ currentOverrides[existingIdx] = newEntry
159
+ } else {
160
+ currentOverrides.push(newEntry)
161
+ }
162
+ }
163
+ } else {
164
+ return writeJson(res, 400, { ok: false, error: 'Missing modelId, mode, or overrides array' })
165
+ }
166
+
167
+ const next = Config({
168
+ ...plainConfig(live()),
169
+ modelContextOverrides: currentOverrides,
170
+ })
171
+ await settingsApi.replace(next)
172
+ await syncProviderState(next)
173
+
174
+ const updatedModels = getAllModels(pub.dynamicModels, currentOverrides)
175
+ return writeJson(res, 200, {
176
+ ok: true,
177
+ modelContextOverrides: next.modelContextOverrides,
178
+ models: updatedModels,
179
+ })
180
+ } catch (err) {
181
+ return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
182
+ }
183
+ },
184
+ }), 'dsh-clinebot: /models/context')
185
+
104
186
  // GET /dsh-clinebot/usage — direct fresh usage limit query
105
187
  ctx.effect(() => ctx.webServer.register({
106
188
  kind: 'exact',
@@ -34,7 +34,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
34
34
 
35
35
  // 1. Subcommand /cline models
36
36
  if (subcmd === 'models') {
37
- const allModels = getAllModels(pub.dynamicModels)
37
+ const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides)
38
38
  const disabledSet = new Set(pub.disabledModels || [])
39
39
  const activeCount = allModels.filter((m) => !disabledSet.has(m.id)).length
40
40
  const syncStatus = pub.planSynced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",