@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 +16 -0
- package/README.md +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +217 -1
- package/lib/config.js +19 -6
- package/lib/models.js +146 -19
- package/lib/provider-sync.js +2 -2
- package/lib/routes/models.js +85 -3
- package/lib/slash-command.js +1 -1
- package/package.json +1 -1
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(
|
|
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.')
|
|
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 }
|
|
77
|
-
//
|
|
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
|
-
|
|
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)
|
|
295
|
-
|
|
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
|
-
|
package/lib/provider-sync.js
CHANGED
|
@@ -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
|
|
package/lib/routes/models.js
CHANGED
|
@@ -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(
|
|
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',
|
package/lib/slash-command.js
CHANGED
|
@@ -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.
|
|
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",
|