@goodandready/dsh-clinebot 0.4.3 → 0.4.5
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 +20 -0
- package/README.md +9 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +217 -13
- package/lib/cline-client.js +9 -88
- package/lib/config.js +9 -1
- package/lib/credential-refs.js +84 -0
- package/lib/index.js +5 -1
- package/lib/models.js +189 -23
- package/lib/provider-sync.js +17 -3
- package/lib/routes/models.js +85 -3
- package/lib/slash-command.js +1 -1
- package/media/visual-verification.png +0 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.5] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Obsolete Model ID Migration & Canonical Normalization (#103)**: Automatically migrate historical model identifiers lacking dots (e.g., `deepseek-v41-flash` -> `deepseek-v4.1-flash`, `qwen38-max` -> `qwen3.8-max`, `glm-53` -> `glm-5.3`, `glm-53-flash` -> `glm-5.3-flash`, `muse-spark-13-contributor` -> `muse-spark-1.3-contributor`) across disk cache, DSH settings, and auto-discovery, eliminating stuck legacy IDs.
|
|
12
|
+
- **Config Schema Test Assertion for Schemastery 3.18.4 (#112)**: Aligned `test/config-schema.test.js` with Schemastery 3.18.4 volatile getter behavior using `plainConfig(validated).enabled`.
|
|
13
|
+
- **UI Color Compliance & Token Hardening (#116)**: Replaced hardcoded `rgba()` fallbacks in `accounts-section.js` and `models-section.js` with canonical DSH `color-mix()` and design tokens.
|
|
14
|
+
- **Decomposition Compliance (#117)**: Extracted credentials helpers into `lib/credential-refs.js`, reducing `lib/cline-client.js` from 590 to 511 lines (well below the 600-line ceiling).
|
|
15
|
+
- **Client Localization Pruning (#115)**: Removed 6 obsolete dead locale keys from `src/client/locales.js` in both `en` and `zh`.
|
|
16
|
+
- **Route Policy & Packaging Contract (#113)**: Documented mandatory «Политика маршрутов» (16 registered endpoints) and «Что публикуется» sections in `docs/design/DESIGN.md`.
|
|
17
|
+
- **Visual Verification Evidence (#114)**: Added dual-theme production visual verification artifact `media/visual-verification.png` and embedded under `## Visual verification` in `README.md`.
|
|
18
|
+
|
|
19
|
+
## [0.4.4] - 2026-09-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **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).
|
|
23
|
+
- **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.
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **Interactive UI & Localization (#110)**: Inline context badges, quick edit popover, clean token input, and bilingual English / Chinese localization across all context actions.
|
|
27
|
+
|
|
8
28
|
## [0.4.3] - 2026-09-24
|
|
9
29
|
|
|
10
30
|
### 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 |
|
|
@@ -205,6 +206,14 @@ All endpoints are registered under `/dsh-clinebot/*` and protected against untru
|
|
|
205
206
|
|
|
206
207
|
---
|
|
207
208
|
|
|
209
|
+
## Visual verification
|
|
210
|
+
|
|
211
|
+
Settings card in DeepSeek Harness, Dark and Light themes side by side:
|
|
212
|
+
|
|
213
|
+

|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
208
217
|
## 🧪 Testing
|
|
209
218
|
|
|
210
219
|
Run the automated test suite:
|
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
|
@@ -48,7 +48,6 @@ const en = {
|
|
|
48
48
|
'key.save': 'Save Key',
|
|
49
49
|
'key.saving': 'Saving…',
|
|
50
50
|
'key.login_fast': '🔑 Open API Keys ↗',
|
|
51
|
-
'key.login_waiting': 'Paste key below for instant verification.',
|
|
52
51
|
'key.verifying': 'Validating key with Cline…',
|
|
53
52
|
'key.verified': '✓ Valid key for {email} ({plan})',
|
|
54
53
|
'key.invalid': '✗ Key validation failed: {error}',
|
|
@@ -66,10 +65,8 @@ const en = {
|
|
|
66
65
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
67
66
|
'accounts.delete_btn': 'Delete',
|
|
68
67
|
'accounts.delete_confirm': 'Are you sure you want to delete account "{label}"?',
|
|
69
|
-
'accounts.delete_secret': 'Delete secret from credentials',
|
|
70
68
|
'accounts.save_btn': 'Save Account',
|
|
71
69
|
'accounts.cancel_btn': 'Cancel',
|
|
72
|
-
'accounts.key_label': 'API Key',
|
|
73
70
|
'accounts.quota_used': '{pct}% used',
|
|
74
71
|
'accounts.last_failover': 'Last failover: {from} → {to} ({reason})',
|
|
75
72
|
'quota.title': '📊 ClinePass Quota & Rate Limits',
|
|
@@ -81,7 +78,6 @@ const en = {
|
|
|
81
78
|
'quota.window_5h': '⏱ 5-Hour Rolling Limit',
|
|
82
79
|
'quota.window_weekly': '📅 Weekly Window',
|
|
83
80
|
'quota.used': '{pct}% used',
|
|
84
|
-
'quota.remaining': 'Remaining: {pct}%',
|
|
85
81
|
'quota.reset_at': 'Reset: {time}',
|
|
86
82
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
87
83
|
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
|
|
@@ -109,6 +105,17 @@ const en = {
|
|
|
109
105
|
'models.th_caps': 'Capabilities',
|
|
110
106
|
'models.star': 'Star',
|
|
111
107
|
'models.auto_new': 'New',
|
|
108
|
+
'models.apply_original_all': '⚡ Original Specs',
|
|
109
|
+
'models.reset_contexts_all': '↺ Reset Contexts',
|
|
110
|
+
'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
|
|
111
|
+
'models.ctx_default': 'Default ({ctx})',
|
|
112
|
+
'models.ctx_original': 'Original ({ctx} · {provider})',
|
|
113
|
+
'models.ctx_edit': 'Edit Context Window',
|
|
114
|
+
'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
|
|
115
|
+
'models.ctx_save': 'Save',
|
|
116
|
+
'models.ctx_cancel': 'Cancel',
|
|
117
|
+
'models.ctx_tag_original': 'Original',
|
|
118
|
+
'models.ctx_tag_custom': 'Custom',
|
|
112
119
|
'stats.title': '📈 Current DSH Session Telemetry',
|
|
113
120
|
'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
|
|
114
121
|
'stats.requests': 'Successful / Total Requests',
|
|
@@ -119,7 +126,6 @@ const en = {
|
|
|
119
126
|
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
|
|
120
127
|
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
121
128
|
'diag.smoke_testing': 'Testing…',
|
|
122
|
-
'diag.smoke_ok': 'Smoke test passed! Latency: {latency} ms',
|
|
123
129
|
'diag.resync_btn': 'Force Re-sync to DSH',
|
|
124
130
|
'diag.resyncing': 'Syncing…',
|
|
125
131
|
'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
|
|
@@ -158,7 +164,6 @@ const en = {
|
|
|
158
164
|
'key.save': '保存密钥',
|
|
159
165
|
'key.saving': '正在保存…',
|
|
160
166
|
'key.login_fast': '🔑 打开 API 密钥页 ↗',
|
|
161
|
-
'key.login_waiting': '请在下方粘贴密钥,系统将自动进行有效性校验。',
|
|
162
167
|
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
163
168
|
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
164
169
|
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
@@ -176,10 +181,8 @@ const en = {
|
|
|
176
181
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
177
182
|
'accounts.delete_btn': '删除',
|
|
178
183
|
'accounts.delete_confirm': '确定要删除账号“{label}”吗?',
|
|
179
|
-
'accounts.delete_secret': '同时删除凭据密钥',
|
|
180
184
|
'accounts.save_btn': '保存账号',
|
|
181
185
|
'accounts.cancel_btn': '取消',
|
|
182
|
-
'accounts.key_label': 'API 密钥',
|
|
183
186
|
'accounts.quota_used': '已用 {pct}%',
|
|
184
187
|
'accounts.last_failover': '最近故障转移:{from} → {to} ({reason})',
|
|
185
188
|
'quota.title': '📊 ClinePass 用量配额与限额监控',
|
|
@@ -191,7 +194,6 @@ const en = {
|
|
|
191
194
|
'quota.window_5h': '⏱ 5 小时滚动限额',
|
|
192
195
|
'quota.window_weekly': '📅 每周滚动窗口',
|
|
193
196
|
'quota.used': '已使用 {pct}%',
|
|
194
|
-
'quota.remaining': '剩余可用:{pct}%',
|
|
195
197
|
'quota.reset_at': '重置时间:{time}',
|
|
196
198
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
197
199
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
@@ -219,6 +221,17 @@ const en = {
|
|
|
219
221
|
'models.th_caps': '特性能力',
|
|
220
222
|
'models.star': '推荐',
|
|
221
223
|
'models.auto_new': '新上线',
|
|
224
|
+
'models.apply_original_all': '⚡ 原厂规格',
|
|
225
|
+
'models.reset_contexts_all': '↺ 重置上下文',
|
|
226
|
+
'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
|
|
227
|
+
'models.ctx_default': '默认 ({ctx})',
|
|
228
|
+
'models.ctx_original': '原厂 ({ctx} · {provider})',
|
|
229
|
+
'models.ctx_edit': '编辑上下文窗口',
|
|
230
|
+
'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
|
|
231
|
+
'models.ctx_save': '保存',
|
|
232
|
+
'models.ctx_cancel': '取消',
|
|
233
|
+
'models.ctx_tag_original': '原厂',
|
|
234
|
+
'models.ctx_tag_custom': '自定义',
|
|
222
235
|
'stats.title': '📈 当前 DSH 会话请求遥测',
|
|
223
236
|
'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
|
|
224
237
|
'stats.requests': '成功 / 总请求次数',
|
|
@@ -229,7 +242,6 @@ const en = {
|
|
|
229
242
|
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试或强制重新同步。',
|
|
230
243
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
231
244
|
'diag.smoke_testing': '正在测试…',
|
|
232
|
-
'diag.smoke_ok': '冒烟测试通过!网络延迟:{latency} ms',
|
|
233
245
|
'diag.resync_btn': '强制重新同步到 DSH',
|
|
234
246
|
'diag.resyncing': '正在同步…',
|
|
235
247
|
'diag.resynced_msg': '模型服务已重新同步到 DSH ({count} 个模型)',
|
|
@@ -683,8 +695,8 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
683
695
|
{
|
|
684
696
|
onSubmit: onSubmitAdd,
|
|
685
697
|
style: {
|
|
686
|
-
background: 'var(--dsw-alias-bg-hover,
|
|
687
|
-
border: '1px solid var(--dsw-alias-border,
|
|
698
|
+
background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
|
|
699
|
+
border: '1px solid var(--dsw-alias-border, color-mix(in srgb, currentColor 12%, transparent))',
|
|
688
700
|
borderRadius: '8px',
|
|
689
701
|
padding: '12px',
|
|
690
702
|
marginBottom: '14px',
|
|
@@ -935,6 +947,7 @@ function ModelsSection({
|
|
|
935
947
|
keyPresent,
|
|
936
948
|
busy,
|
|
937
949
|
handleSyncPlanModels,
|
|
950
|
+
handleUpdateModelContext,
|
|
938
951
|
handleSetModelsFilter,
|
|
939
952
|
handleToggleModel,
|
|
940
953
|
planSynced,
|
|
@@ -945,6 +958,21 @@ function ModelsSection({
|
|
|
945
958
|
const syncDateStr = planSyncedAt ? new Date(planSyncedAt).toLocaleDateString() : ''
|
|
946
959
|
const [search, setSearch] = React.useState('')
|
|
947
960
|
const [viewFilter, setViewFilter] = React.useState('all')
|
|
961
|
+
const [editingModelId, setEditingModelId] = React.useState(null)
|
|
962
|
+
const [customCtxVal, setCustomCtxVal] = React.useState('')
|
|
963
|
+
|
|
964
|
+
function formatCtx(num) {
|
|
965
|
+
num = Number(num) || 200000
|
|
966
|
+
if (num >= 1000000) {
|
|
967
|
+
const m = num / 1000000
|
|
968
|
+
return `${m % 1 === 0 ? m : m.toFixed(1)}M`
|
|
969
|
+
}
|
|
970
|
+
if (num >= 1000) {
|
|
971
|
+
const k = num / 1000
|
|
972
|
+
return `${k % 1 === 0 ? k : k.toFixed(1)}K`
|
|
973
|
+
}
|
|
974
|
+
return String(num)
|
|
975
|
+
}
|
|
948
976
|
|
|
949
977
|
const filteredModels = modelsList.filter((m) => {
|
|
950
978
|
if (viewFilter === 'vision') {
|
|
@@ -998,6 +1026,28 @@ function ModelsSection({
|
|
|
998
1026
|
},
|
|
999
1027
|
busy === 'sync-models' ? t('models.syncing') : t('models.sync')
|
|
1000
1028
|
),
|
|
1029
|
+
React.createElement(
|
|
1030
|
+
'button',
|
|
1031
|
+
{
|
|
1032
|
+
type: 'button',
|
|
1033
|
+
className: 'cb-btn',
|
|
1034
|
+
disabled: !!busy,
|
|
1035
|
+
title: t('models.apply_original_all'),
|
|
1036
|
+
onClick: () => typeof handleUpdateModelContext === 'function' && handleUpdateModelContext({ mode: 'all-original' }),
|
|
1037
|
+
},
|
|
1038
|
+
t('models.apply_original_all')
|
|
1039
|
+
),
|
|
1040
|
+
React.createElement(
|
|
1041
|
+
'button',
|
|
1042
|
+
{
|
|
1043
|
+
type: 'button',
|
|
1044
|
+
className: 'cb-btn',
|
|
1045
|
+
disabled: !!busy,
|
|
1046
|
+
title: t('models.reset_contexts_all'),
|
|
1047
|
+
onClick: () => typeof handleUpdateModelContext === 'function' && handleUpdateModelContext({ mode: 'all-default' }),
|
|
1048
|
+
},
|
|
1049
|
+
t('models.reset_contexts_all')
|
|
1050
|
+
),
|
|
1001
1051
|
React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') }, t('models.all')),
|
|
1002
1052
|
React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') }, t('models.vision')),
|
|
1003
1053
|
React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') }, t('models.coding')),
|
|
@@ -1103,6 +1153,12 @@ function ModelsSection({
|
|
|
1103
1153
|
null,
|
|
1104
1154
|
filteredModels.map((m) => {
|
|
1105
1155
|
const isEnabled = !disabledSet.has(m.id)
|
|
1156
|
+
const defaultCtx = m.defaultContextLength || m.clineContextLength || 200000
|
|
1157
|
+
const origCtx = m.originalContextLength || 200000
|
|
1158
|
+
const origProv = m.originalProvider || 'Original'
|
|
1159
|
+
const isOverridden = Boolean(m.isContextOverridden)
|
|
1160
|
+
const isOriginal = isOverridden && (m.contextLength === origCtx)
|
|
1161
|
+
|
|
1106
1162
|
return React.createElement(
|
|
1107
1163
|
'tr',
|
|
1108
1164
|
{ key: m.id },
|
|
@@ -1127,7 +1183,140 @@ function ModelsSection({
|
|
|
1127
1183
|
: null
|
|
1128
1184
|
),
|
|
1129
1185
|
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
1130
|
-
React.createElement(
|
|
1186
|
+
React.createElement(
|
|
1187
|
+
'td',
|
|
1188
|
+
null,
|
|
1189
|
+
editingModelId === m.id
|
|
1190
|
+
? React.createElement(
|
|
1191
|
+
'div',
|
|
1192
|
+
{
|
|
1193
|
+
style: {
|
|
1194
|
+
display: 'flex',
|
|
1195
|
+
flexDirection: 'column',
|
|
1196
|
+
gap: '6px',
|
|
1197
|
+
background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
|
|
1198
|
+
padding: '6px',
|
|
1199
|
+
borderRadius: '6px',
|
|
1200
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1201
|
+
},
|
|
1202
|
+
},
|
|
1203
|
+
React.createElement(
|
|
1204
|
+
'div',
|
|
1205
|
+
{ style: { display: 'flex', gap: '4px', flexWrap: 'wrap' } },
|
|
1206
|
+
React.createElement(
|
|
1207
|
+
'button',
|
|
1208
|
+
{
|
|
1209
|
+
type: 'button',
|
|
1210
|
+
className: `cb-btn ${!isOverridden ? 'cb-btn-active' : ''}`,
|
|
1211
|
+
style: { padding: '2px 6px', fontSize: '11px' },
|
|
1212
|
+
onClick: () => {
|
|
1213
|
+
if (typeof handleUpdateModelContext === 'function') {
|
|
1214
|
+
handleUpdateModelContext({ modelId: m.id, reset: true })
|
|
1215
|
+
}
|
|
1216
|
+
setEditingModelId(null)
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
t('models.ctx_default', { ctx: formatCtx(defaultCtx) })
|
|
1220
|
+
),
|
|
1221
|
+
React.createElement(
|
|
1222
|
+
'button',
|
|
1223
|
+
{
|
|
1224
|
+
type: 'button',
|
|
1225
|
+
className: `cb-btn ${isOriginal ? 'cb-btn-active' : ''}`,
|
|
1226
|
+
style: { padding: '2px 6px', fontSize: '11px' },
|
|
1227
|
+
onClick: () => {
|
|
1228
|
+
if (typeof handleUpdateModelContext === 'function') {
|
|
1229
|
+
handleUpdateModelContext({ modelId: m.id, contextLength: origCtx })
|
|
1230
|
+
}
|
|
1231
|
+
setEditingModelId(null)
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
t('models.ctx_original', { ctx: formatCtx(origCtx), provider: origProv })
|
|
1235
|
+
)
|
|
1236
|
+
),
|
|
1237
|
+
React.createElement(
|
|
1238
|
+
'div',
|
|
1239
|
+
{ style: { display: 'flex', gap: '4px', alignItems: 'center' } },
|
|
1240
|
+
React.createElement('input', {
|
|
1241
|
+
type: 'number',
|
|
1242
|
+
className: 'cb-input',
|
|
1243
|
+
style: { height: '24px', fontSize: '11px', padding: '0 6px', width: '120px' },
|
|
1244
|
+
placeholder: t('models.ctx_custom_placeholder'),
|
|
1245
|
+
value: customCtxVal,
|
|
1246
|
+
onChange: (e) => setCustomCtxVal(e.target.value),
|
|
1247
|
+
}),
|
|
1248
|
+
React.createElement(
|
|
1249
|
+
'button',
|
|
1250
|
+
{
|
|
1251
|
+
type: 'button',
|
|
1252
|
+
className: 'cb-btn cb-btn-primary',
|
|
1253
|
+
style: { padding: '2px 8px', fontSize: '11px' },
|
|
1254
|
+
onClick: () => {
|
|
1255
|
+
const val = parseInt(customCtxVal, 10)
|
|
1256
|
+
if (val > 0 && typeof handleUpdateModelContext === 'function') {
|
|
1257
|
+
handleUpdateModelContext({ modelId: m.id, contextLength: val })
|
|
1258
|
+
}
|
|
1259
|
+
setEditingModelId(null)
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
t('models.ctx_save')
|
|
1263
|
+
),
|
|
1264
|
+
React.createElement(
|
|
1265
|
+
'button',
|
|
1266
|
+
{
|
|
1267
|
+
type: 'button',
|
|
1268
|
+
className: 'cb-btn',
|
|
1269
|
+
style: { padding: '2px 6px', fontSize: '11px' },
|
|
1270
|
+
onClick: () => setEditingModelId(null),
|
|
1271
|
+
},
|
|
1272
|
+
t('models.ctx_cancel')
|
|
1273
|
+
)
|
|
1274
|
+
)
|
|
1275
|
+
)
|
|
1276
|
+
: React.createElement(
|
|
1277
|
+
'div',
|
|
1278
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
1279
|
+
React.createElement(
|
|
1280
|
+
'span',
|
|
1281
|
+
{
|
|
1282
|
+
style: { fontWeight: '600', cursor: 'pointer' },
|
|
1283
|
+
onClick: () => {
|
|
1284
|
+
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1285
|
+
setEditingModelId(m.id)
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
formatCtx(m.contextLength || defaultCtx)
|
|
1289
|
+
),
|
|
1290
|
+
isOverridden
|
|
1291
|
+
? React.createElement(
|
|
1292
|
+
'span',
|
|
1293
|
+
{
|
|
1294
|
+
className: isOriginal ? 'cb-badge cb-badge-ok' : 'cb-badge cb-badge-warn',
|
|
1295
|
+
style: { fontSize: '10px', padding: '1px 6px', cursor: 'pointer' },
|
|
1296
|
+
onClick: () => {
|
|
1297
|
+
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1298
|
+
setEditingModelId(m.id)
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
isOriginal ? t('models.ctx_tag_original') : t('models.ctx_tag_custom')
|
|
1302
|
+
)
|
|
1303
|
+
: null,
|
|
1304
|
+
React.createElement(
|
|
1305
|
+
'button',
|
|
1306
|
+
{
|
|
1307
|
+
type: 'button',
|
|
1308
|
+
className: 'cb-btn',
|
|
1309
|
+
style: { padding: '2px 6px', fontSize: '11px', lineHeight: '1', border: 'none', background: 'transparent' },
|
|
1310
|
+
title: t('models.ctx_edit'),
|
|
1311
|
+
onClick: () => {
|
|
1312
|
+
setCustomCtxVal(String(m.contextLength || defaultCtx))
|
|
1313
|
+
setEditingModelId(m.id)
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
'✎'
|
|
1317
|
+
)
|
|
1318
|
+
)
|
|
1319
|
+
),
|
|
1131
1320
|
React.createElement(
|
|
1132
1321
|
'td',
|
|
1133
1322
|
null,
|
|
@@ -1393,6 +1582,20 @@ function SettingsPage(props) {
|
|
|
1393
1582
|
})
|
|
1394
1583
|
}
|
|
1395
1584
|
|
|
1585
|
+
async function handleUpdateModelContext(payload) {
|
|
1586
|
+
await performAction('update-context', async () => {
|
|
1587
|
+
const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
|
|
1588
|
+
method: 'POST',
|
|
1589
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1590
|
+
body: JSON.stringify(payload),
|
|
1591
|
+
})
|
|
1592
|
+
const data = await res.json().catch(() => ({}))
|
|
1593
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1594
|
+
setMsg(t('models.context_updated_msg'))
|
|
1595
|
+
await load()
|
|
1596
|
+
})
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1396
1599
|
async function handleSyncPlanModels() {
|
|
1397
1600
|
await performAction('sync-models', async () => {
|
|
1398
1601
|
const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
|
|
@@ -1689,6 +1892,7 @@ function SettingsPage(props) {
|
|
|
1689
1892
|
keyPresent,
|
|
1690
1893
|
busy,
|
|
1691
1894
|
handleSyncPlanModels,
|
|
1895
|
+
handleUpdateModelContext,
|
|
1692
1896
|
handleSetModelsFilter,
|
|
1693
1897
|
handleToggleModel,
|
|
1694
1898
|
planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
|
package/lib/cline-client.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
toCredentialRef,
|
|
3
|
+
resolveApiKey,
|
|
4
|
+
saveCredentialKey,
|
|
5
|
+
deleteCredentialKey,
|
|
6
|
+
resolveKeyValue,
|
|
7
|
+
} from './credential-refs.js'
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* ClineBot / ClinePass client helpers for DeepSeek Harness.
|
|
3
11
|
*
|
|
@@ -22,20 +30,7 @@ export const USAGE_CACHE_TTL_MS = 60000
|
|
|
22
30
|
|
|
23
31
|
export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, DEFAULT_MODEL_ID }
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
* Resolve or fallback credential reference descriptor.
|
|
27
|
-
*/
|
|
28
|
-
export async function toCredentialRef(name) {
|
|
29
|
-
try {
|
|
30
|
-
const mod = await import('@deepseek-ai/dsh-credentials')
|
|
31
|
-
if (typeof mod.credentialRef === 'function') {
|
|
32
|
-
return mod.credentialRef(name)
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
/* fallback when executed in standalone unit tests outside DSH bundle */
|
|
36
|
-
}
|
|
37
|
-
return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
|
|
38
|
-
}
|
|
33
|
+
// Credentials helpers extracted to ./credential-refs.js
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
36
|
* Normalize base URL ensuring clean format without trailing slashes.
|
|
@@ -57,57 +52,7 @@ export {
|
|
|
57
52
|
resetSessionStats,
|
|
58
53
|
} from './telemetry.js'
|
|
59
54
|
|
|
60
|
-
/**
|
|
61
|
-
* Resolve API key from environment variables.
|
|
62
|
-
*/
|
|
63
|
-
export function resolveApiKey(apiKeyEnv, env = process.env) {
|
|
64
|
-
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
65
|
-
return {
|
|
66
|
-
envName: name,
|
|
67
|
-
value: String(env[name] || ''),
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Save API key directly into DSH credentials service (~/.dsh/.credentials.yaml).
|
|
73
|
-
*/
|
|
74
|
-
export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
|
|
75
|
-
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
76
|
-
const value = String(apiKey || '').trim()
|
|
77
|
-
|
|
78
|
-
if (!value) {
|
|
79
|
-
throw new Error('API key cannot be empty')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
83
|
-
if (!credentials || typeof credentials.set !== 'function') {
|
|
84
|
-
throw new Error('DSH credentials service is unavailable in this runtime profile')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ref = await toCredentialRef(name)
|
|
88
|
-
await credentials.set(ref, value)
|
|
89
|
-
return { ok: true, envName: name }
|
|
90
|
-
}
|
|
91
55
|
|
|
92
|
-
/**
|
|
93
|
-
* Delete API key from DSH credentials service.
|
|
94
|
-
*/
|
|
95
|
-
export async function deleteCredentialKey(ctx, apiKeyEnv) {
|
|
96
|
-
const name = String(apiKeyEnv || '').trim()
|
|
97
|
-
if (!name) return { ok: false }
|
|
98
|
-
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
99
|
-
if (!credentials) return { ok: false }
|
|
100
|
-
try {
|
|
101
|
-
const ref = await toCredentialRef(name)
|
|
102
|
-
if (typeof credentials.unset === 'function') {
|
|
103
|
-
await credentials.unset(ref)
|
|
104
|
-
return { ok: true, envName: name }
|
|
105
|
-
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.warn('[dsh-clinebot] Failed unsetting credential:', err)
|
|
108
|
-
}
|
|
109
|
-
return { ok: false }
|
|
110
|
-
}
|
|
111
56
|
|
|
112
57
|
function abortAfter(ms) {
|
|
113
58
|
const ac = new AbortController()
|
|
@@ -560,31 +505,7 @@ export function buildPiAiProvider({
|
|
|
560
505
|
}
|
|
561
506
|
}
|
|
562
507
|
|
|
563
|
-
/**
|
|
564
|
-
* Safely resolve key value from DSH credentials service or process.env.
|
|
565
|
-
*/
|
|
566
|
-
export async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
567
|
-
const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
568
|
-
const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
|
|
569
|
-
if (creds && typeof creds.resolve === 'function') {
|
|
570
|
-
try {
|
|
571
|
-
const ref = await toCredentialRef(refName)
|
|
572
|
-
const hit = await creds.resolve(ref)
|
|
573
|
-
if (hit?.value) {
|
|
574
|
-
return { envName: refName, value: hit.value, source: 'credentials' }
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
/* miss */
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const fromEnv = resolveApiKey(refName)
|
|
582
|
-
if (fromEnv.value) {
|
|
583
|
-
return { ...fromEnv, source: 'env' }
|
|
584
|
-
}
|
|
585
508
|
|
|
586
|
-
return { envName: refName, value: '', source: 'none' }
|
|
587
|
-
}
|
|
588
509
|
|
|
589
510
|
|
|
590
511
|
export { resolveAccountPool, isAccountQuotaExhausted, rotateToNextAccount, getLastRotation, setLastRotation, isQuotaExceededError } from './account-pool.js'
|
package/lib/config.js
CHANGED
|
@@ -53,6 +53,12 @@ export const Config = z.object({
|
|
|
53
53
|
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
54
54
|
planSyncedAt: z.number().default(0)
|
|
55
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)
|
|
@@ -106,8 +112,9 @@ export function volatileConfig(cfg) {
|
|
|
106
112
|
export function publicConfig(cfg) {
|
|
107
113
|
cfg = plainConfig(cfg)
|
|
108
114
|
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
115
|
+
const modelContextOverrides = Array.isArray(cfg?.modelContextOverrides) ? cfg.modelContextOverrides : []
|
|
109
116
|
const isSynced = dynamic.length > 0
|
|
110
|
-
const allModels = getAllModels(dynamic)
|
|
117
|
+
const allModels = getAllModels(dynamic, modelContextOverrides)
|
|
111
118
|
const allDefaultIds = allModels.map((m) => m.id)
|
|
112
119
|
|
|
113
120
|
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
@@ -141,6 +148,7 @@ export function publicConfig(cfg) {
|
|
|
141
148
|
dynamicModels: dynamic,
|
|
142
149
|
planSynced: isSynced,
|
|
143
150
|
planSyncedAt,
|
|
151
|
+
modelContextOverrides,
|
|
144
152
|
disabledModels: disabledList,
|
|
145
153
|
enabledModels: activeIds,
|
|
146
154
|
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials management helper for @goodandready/dsh-clinebot.
|
|
3
|
+
* Isolates DSH Credentials storage and environment resolution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_API_KEY_ENV = 'CLINEBOT_API_KEY'
|
|
7
|
+
|
|
8
|
+
export async function toCredentialRef(name) {
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import('@deepseek-ai/dsh-credentials')
|
|
11
|
+
if (typeof mod.credentialRef === 'function') {
|
|
12
|
+
return mod.credentialRef(name)
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
/* fallback when executed in standalone unit tests outside DSH bundle */
|
|
16
|
+
}
|
|
17
|
+
return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveApiKey(apiKeyEnv, env = process.env) {
|
|
21
|
+
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
22
|
+
return {
|
|
23
|
+
envName: name,
|
|
24
|
+
value: String(env[name] || ''),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
|
|
29
|
+
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
30
|
+
const value = String(apiKey || '').trim()
|
|
31
|
+
|
|
32
|
+
if (!value) {
|
|
33
|
+
throw new Error('API key cannot be empty')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
37
|
+
if (!credentials || typeof credentials.set !== 'function') {
|
|
38
|
+
throw new Error('DSH credentials service is unavailable in this runtime profile')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ref = await toCredentialRef(name)
|
|
42
|
+
await credentials.set(ref, value)
|
|
43
|
+
return { ok: true, envName: name }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function deleteCredentialKey(ctx, apiKeyEnv) {
|
|
47
|
+
const name = String(apiKeyEnv || '').trim()
|
|
48
|
+
if (!name) return { ok: false }
|
|
49
|
+
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
50
|
+
if (!credentials) return { ok: false }
|
|
51
|
+
try {
|
|
52
|
+
const ref = await toCredentialRef(name)
|
|
53
|
+
if (typeof credentials.unset === 'function') {
|
|
54
|
+
await credentials.unset(ref)
|
|
55
|
+
return { ok: true, envName: name }
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed unsetting credential: ' + (err?.message || err))
|
|
59
|
+
}
|
|
60
|
+
return { ok: false }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
64
|
+
const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
65
|
+
const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
|
|
66
|
+
if (creds && typeof creds.resolve === 'function') {
|
|
67
|
+
try {
|
|
68
|
+
const ref = await toCredentialRef(refName)
|
|
69
|
+
const hit = await creds.resolve(ref)
|
|
70
|
+
if (hit?.value) {
|
|
71
|
+
return { envName: refName, value: hit.value, source: 'credentials' }
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
/* miss */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromEnv = resolveApiKey(refName)
|
|
79
|
+
if (fromEnv.value) {
|
|
80
|
+
return { ...fromEnv, source: 'env' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { envName: refName, value: '', source: 'none' }
|
|
84
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -122,7 +122,11 @@ export function apply(ctx, config) {
|
|
|
122
122
|
if (typeof ctx.effect === 'function') {
|
|
123
123
|
ctx.effect(() => {
|
|
124
124
|
const timer = setTimeout(triggerAutoDiscover, 500)
|
|
125
|
-
|
|
125
|
+
const retryTimer = setTimeout(triggerAutoDiscover, 3000)
|
|
126
|
+
return () => {
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
clearTimeout(retryTimer)
|
|
129
|
+
}
|
|
126
130
|
}, 'dsh-clinebot: auto-discover')
|
|
127
131
|
}
|
|
128
132
|
|
package/lib/models.js
CHANGED
|
@@ -6,18 +6,77 @@
|
|
|
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'
|
|
14
|
+
|
|
15
|
+
export const OBSOLETE_MODEL_ID_MAP = Object.freeze({
|
|
16
|
+
'cline-pass/deepseek-v41-flash': 'cline-pass/deepseek-v4.1-flash',
|
|
17
|
+
'cline-pass/qwen38-max': 'cline-pass/qwen3.8-max',
|
|
18
|
+
'cline-pass/glm-53': 'cline-pass/glm-5.3',
|
|
19
|
+
'cline-pass/glm-53-flash': 'cline-pass/glm-5.3-flash',
|
|
20
|
+
'cline-pass/muse-spark-13-contributor': 'cline-pass/muse-spark-1.3-contributor',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function migrateModelId(id) {
|
|
24
|
+
if (typeof id !== 'string') return id
|
|
25
|
+
return OBSOLETE_MODEL_ID_MAP[id] || id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function migrateModelEntry(model) {
|
|
29
|
+
if (!model || typeof model !== 'object') return model
|
|
30
|
+
const canonicalId = migrateModelId(model.id)
|
|
31
|
+
if (canonicalId === model.id) return model
|
|
32
|
+
return {
|
|
33
|
+
...model,
|
|
34
|
+
id: canonicalId,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
13
37
|
export const PROVIDER_DISPLAY_NAME = 'ClineBot (ClinePass)'
|
|
14
38
|
export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4-flash'
|
|
15
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Authentic original model specifications from original creators:
|
|
42
|
+
* Moonshot AI (Kimi): 2,000,000 tokens
|
|
43
|
+
* Alibaba Cloud (Qwen): 1,000,000 tokens
|
|
44
|
+
* MiniMax: 1,000,000 tokens
|
|
45
|
+
* Xiaomi (MiMo): 1,000,000 tokens
|
|
46
|
+
* DeepSeek: 128,000 tokens
|
|
47
|
+
* Zhipu AI (GLM): 128,000 tokens
|
|
48
|
+
*/
|
|
49
|
+
export function getOriginalModelContext(nameOrId) {
|
|
50
|
+
const norm = String(nameOrId || '').toLowerCase()
|
|
51
|
+
if (norm.includes('kimi')) return 2000000
|
|
52
|
+
if (norm.includes('qwen')) return 1000000
|
|
53
|
+
if (norm.includes('minimax')) return 1000000
|
|
54
|
+
if (norm.includes('mimo')) return 1000000
|
|
55
|
+
if (norm.includes('deepseek')) return 128000
|
|
56
|
+
if (norm.includes('glm')) return 128000
|
|
57
|
+
return 200000
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getOriginalModelProvider(nameOrId) {
|
|
61
|
+
const norm = String(nameOrId || '').toLowerCase()
|
|
62
|
+
if (norm.includes('kimi')) return 'Moonshot AI'
|
|
63
|
+
if (norm.includes('qwen')) return 'Alibaba Cloud'
|
|
64
|
+
if (norm.includes('minimax')) return 'MiniMax'
|
|
65
|
+
if (norm.includes('mimo')) return 'Xiaomi'
|
|
66
|
+
if (norm.includes('deepseek')) return 'DeepSeek'
|
|
67
|
+
if (norm.includes('glm')) return 'Zhipu AI'
|
|
68
|
+
return 'Original Vendor'
|
|
69
|
+
}
|
|
70
|
+
|
|
16
71
|
export const CLINE_MODELS = Object.freeze([
|
|
17
72
|
{
|
|
18
73
|
id: 'cline-pass/deepseek-v4-flash',
|
|
19
74
|
name: 'DeepSeek V4 Flash',
|
|
20
75
|
description: 'High-speed reasoning & code completion model optimized for agentic loops.',
|
|
76
|
+
clineContextLength: 128000,
|
|
77
|
+
defaultContextLength: 128000,
|
|
78
|
+
originalContextLength: 128000,
|
|
79
|
+
originalProvider: 'DeepSeek',
|
|
21
80
|
contextLength: 128000,
|
|
22
81
|
maxTokens: 8192,
|
|
23
82
|
input: ['text', 'image'],
|
|
@@ -30,6 +89,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
30
89
|
id: 'cline-pass/deepseek-v4-pro',
|
|
31
90
|
name: 'DeepSeek V4 Pro',
|
|
32
91
|
description: 'Flagship reasoning and multi-turn architectural coding model.',
|
|
92
|
+
clineContextLength: 128000,
|
|
93
|
+
defaultContextLength: 128000,
|
|
94
|
+
originalContextLength: 128000,
|
|
95
|
+
originalProvider: 'DeepSeek',
|
|
33
96
|
contextLength: 128000,
|
|
34
97
|
maxTokens: 8192,
|
|
35
98
|
input: ['text', 'image'],
|
|
@@ -42,6 +105,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
42
105
|
id: 'cline-pass/glm-5.2',
|
|
43
106
|
name: 'GLM 5.2',
|
|
44
107
|
description: 'Bilingual general & coding model with strong instruction following.',
|
|
108
|
+
clineContextLength: 128000,
|
|
109
|
+
defaultContextLength: 128000,
|
|
110
|
+
originalContextLength: 128000,
|
|
111
|
+
originalProvider: 'Zhipu AI',
|
|
45
112
|
contextLength: 128000,
|
|
46
113
|
maxTokens: 8192,
|
|
47
114
|
input: ['text'],
|
|
@@ -54,6 +121,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
54
121
|
id: 'cline-pass/kimi-k3',
|
|
55
122
|
name: 'Kimi K3',
|
|
56
123
|
description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
|
|
124
|
+
clineContextLength: 200000,
|
|
125
|
+
defaultContextLength: 200000,
|
|
126
|
+
originalContextLength: 2000000,
|
|
127
|
+
originalProvider: 'Moonshot AI',
|
|
57
128
|
contextLength: 200000,
|
|
58
129
|
maxTokens: 8192,
|
|
59
130
|
input: ['text'],
|
|
@@ -66,6 +137,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
66
137
|
id: 'cline-pass/kimi-k2.7-code',
|
|
67
138
|
name: 'Kimi K2.7 Code',
|
|
68
139
|
description: 'Specialized coding model with agentic tool calling support.',
|
|
140
|
+
clineContextLength: 200000,
|
|
141
|
+
defaultContextLength: 200000,
|
|
142
|
+
originalContextLength: 2000000,
|
|
143
|
+
originalProvider: 'Moonshot AI',
|
|
69
144
|
contextLength: 200000,
|
|
70
145
|
maxTokens: 8192,
|
|
71
146
|
input: ['text'],
|
|
@@ -77,6 +152,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
77
152
|
id: 'cline-pass/kimi-k2.6',
|
|
78
153
|
name: 'Kimi K2.6',
|
|
79
154
|
description: 'Balanced long-context conversational model.',
|
|
155
|
+
clineContextLength: 200000,
|
|
156
|
+
defaultContextLength: 200000,
|
|
157
|
+
originalContextLength: 2000000,
|
|
158
|
+
originalProvider: 'Moonshot AI',
|
|
80
159
|
contextLength: 200000,
|
|
81
160
|
maxTokens: 8192,
|
|
82
161
|
input: ['text'],
|
|
@@ -88,6 +167,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
88
167
|
id: 'cline-pass/qwen3.7-max',
|
|
89
168
|
name: 'Qwen 3.7 Max',
|
|
90
169
|
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',
|
|
91
174
|
contextLength: 128000,
|
|
92
175
|
maxTokens: 8192,
|
|
93
176
|
input: ['text', 'image'],
|
|
@@ -100,6 +183,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
100
183
|
id: 'cline-pass/qwen3.7-plus',
|
|
101
184
|
name: 'Qwen 3.7 Plus',
|
|
102
185
|
description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
|
|
186
|
+
clineContextLength: 128000,
|
|
187
|
+
defaultContextLength: 128000,
|
|
188
|
+
originalContextLength: 1000000,
|
|
189
|
+
originalProvider: 'Alibaba Cloud',
|
|
103
190
|
contextLength: 128000,
|
|
104
191
|
maxTokens: 8192,
|
|
105
192
|
input: ['text', 'image'],
|
|
@@ -112,6 +199,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
112
199
|
id: 'cline-pass/minimax-m3',
|
|
113
200
|
name: 'MiniMax M3',
|
|
114
201
|
description: 'High throughput reasoning and synthesis model.',
|
|
202
|
+
clineContextLength: 200000,
|
|
203
|
+
defaultContextLength: 200000,
|
|
204
|
+
originalContextLength: 1000000,
|
|
205
|
+
originalProvider: 'MiniMax',
|
|
115
206
|
contextLength: 200000,
|
|
116
207
|
maxTokens: 8192,
|
|
117
208
|
input: ['text'],
|
|
@@ -124,6 +215,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
124
215
|
id: 'cline-pass/mimo-v2.5',
|
|
125
216
|
name: 'MiMo V2.5',
|
|
126
217
|
description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
|
|
218
|
+
clineContextLength: 128000,
|
|
219
|
+
defaultContextLength: 128000,
|
|
220
|
+
originalContextLength: 1000000,
|
|
221
|
+
originalProvider: 'Xiaomi',
|
|
127
222
|
contextLength: 128000,
|
|
128
223
|
maxTokens: 8192,
|
|
129
224
|
input: ['text', 'image'],
|
|
@@ -136,6 +231,10 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
136
231
|
id: 'cline-pass/mimo-v2.5-pro',
|
|
137
232
|
name: 'MiMo V2.5 Pro',
|
|
138
233
|
description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
|
|
234
|
+
clineContextLength: 128000,
|
|
235
|
+
defaultContextLength: 128000,
|
|
236
|
+
originalContextLength: 1000000,
|
|
237
|
+
originalProvider: 'Xiaomi',
|
|
139
238
|
contextLength: 128000,
|
|
140
239
|
maxTokens: 8192,
|
|
141
240
|
input: ['text', 'image'],
|
|
@@ -203,10 +302,16 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
203
302
|
.replace(/-+/g, '-')
|
|
204
303
|
const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
|
|
205
304
|
const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
|
|
305
|
+
const origCtx = getOriginalModelContext(name)
|
|
306
|
+
const origProv = getOriginalModelProvider(name)
|
|
206
307
|
matched.push({
|
|
207
308
|
id: `cline-pass/${idPart}`,
|
|
208
309
|
name,
|
|
209
310
|
description: `Official ClinePass subscription model: ${name}`,
|
|
311
|
+
clineContextLength: 200000,
|
|
312
|
+
defaultContextLength: 200000,
|
|
313
|
+
originalContextLength: origCtx,
|
|
314
|
+
originalProvider: origProv,
|
|
210
315
|
contextLength: 200000,
|
|
211
316
|
maxTokens: 8192,
|
|
212
317
|
input: isVision ? ['text', 'image'] : ['text'],
|
|
@@ -228,51 +333,98 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
228
333
|
* with curated capabilities, reasoning efforts, and descriptions from CLINE_MODELS.
|
|
229
334
|
* 2. When plan is unsynced (dynamicModels is empty):
|
|
230
335
|
* Falls back to the full curated CLINE_MODELS catalog marked with unverified: true.
|
|
336
|
+
*
|
|
337
|
+
* Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
|
|
231
338
|
*/
|
|
232
|
-
export function getAllModels(dynamicModels = []) {
|
|
233
|
-
|
|
339
|
+
export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
|
|
340
|
+
const normalizedDynamic = (Array.isArray(dynamicModels) ? dynamicModels : []).map(migrateModelEntry)
|
|
341
|
+
const overridesMap = new Map()
|
|
342
|
+
if (Array.isArray(modelContextOverrides)) {
|
|
343
|
+
for (const ov of modelContextOverrides) {
|
|
344
|
+
if (ov && ov.modelId) {
|
|
345
|
+
overridesMap.set(ov.modelId, ov)
|
|
346
|
+
const canonicalId = migrateModelId(ov.modelId)
|
|
347
|
+
if (canonicalId !== ov.modelId) {
|
|
348
|
+
overridesMap.set(canonicalId, { ...ov, modelId: canonicalId })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function applyOverride(m) {
|
|
355
|
+
const defaultCtx = m.clineContextLength || m.defaultContextLength || m.contextLength || 200000
|
|
356
|
+
const origCtx = m.originalContextLength || getOriginalModelContext(m.id)
|
|
357
|
+
const origProv = m.originalProvider || getOriginalModelProvider(m.id)
|
|
358
|
+
const ov = overridesMap.get(m.id)
|
|
359
|
+
const activeCtx = ov && typeof ov.contextLength === 'number' && ov.contextLength > 0
|
|
360
|
+
? ov.contextLength
|
|
361
|
+
: defaultCtx
|
|
362
|
+
const activeMaxTokens = ov && typeof ov.maxTokens === 'number' && ov.maxTokens > 0
|
|
363
|
+
? ov.maxTokens
|
|
364
|
+
: m.maxTokens || 8192
|
|
365
|
+
return {
|
|
366
|
+
...m,
|
|
367
|
+
contextLength: activeCtx,
|
|
368
|
+
maxTokens: activeMaxTokens,
|
|
369
|
+
defaultContextLength: defaultCtx,
|
|
370
|
+
clineContextLength: defaultCtx,
|
|
371
|
+
originalContextLength: origCtx,
|
|
372
|
+
originalProvider: origProv,
|
|
373
|
+
isContextOverridden: activeCtx !== defaultCtx,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (normalizedDynamic.length > 0) {
|
|
234
378
|
const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
|
|
235
|
-
return
|
|
379
|
+
return normalizedDynamic.map((dm) => {
|
|
236
380
|
const targetNorm = normalize(dm.id.replace(/^cline-pass\//, ''))
|
|
237
381
|
const curated = CLINE_MODELS.find(
|
|
238
382
|
(cm) => cm.id === dm.id || normalize(cm.id.replace(/^cline-pass\//, '')) === targetNorm || normalize(cm.name) === targetNorm
|
|
239
383
|
)
|
|
240
384
|
if (curated) {
|
|
241
|
-
return {
|
|
385
|
+
return applyOverride({
|
|
242
386
|
...curated,
|
|
243
387
|
...dm,
|
|
244
388
|
description: dm.description || curated.description,
|
|
245
389
|
contextLength: dm.contextLength || curated.contextLength,
|
|
246
390
|
maxTokens: dm.maxTokens || curated.maxTokens,
|
|
391
|
+
clineContextLength: curated.clineContextLength || curated.contextLength,
|
|
392
|
+
defaultContextLength: curated.defaultContextLength || curated.contextLength,
|
|
393
|
+
originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
|
|
394
|
+
originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
|
|
247
395
|
input: dm.input && dm.input.length ? dm.input : curated.input,
|
|
248
396
|
category: dm.category || curated.category,
|
|
249
397
|
reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
|
|
250
398
|
unverified: false,
|
|
251
|
-
}
|
|
399
|
+
})
|
|
252
400
|
}
|
|
253
|
-
return {
|
|
401
|
+
return applyOverride({
|
|
254
402
|
...dm,
|
|
403
|
+
clineContextLength: dm.contextLength || 200000,
|
|
404
|
+
defaultContextLength: dm.contextLength || 200000,
|
|
405
|
+
originalContextLength: dm.originalContextLength || getOriginalModelContext(dm.id),
|
|
406
|
+
originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
|
|
255
407
|
unverified: false,
|
|
256
|
-
}
|
|
408
|
+
})
|
|
257
409
|
})
|
|
258
410
|
}
|
|
259
411
|
|
|
260
|
-
return CLINE_MODELS.map((m) => ({ ...m, unverified: true }))
|
|
412
|
+
return CLINE_MODELS.map((m) => applyOverride({ ...m, unverified: true }))
|
|
261
413
|
}
|
|
262
414
|
|
|
263
|
-
export function findModel(id, dynamicModels = []) {
|
|
264
|
-
const all = getAllModels(dynamicModels)
|
|
415
|
+
export function findModel(id, dynamicModels = [], modelContextOverrides = []) {
|
|
416
|
+
const all = getAllModels(dynamicModels, modelContextOverrides)
|
|
265
417
|
const found = all.find((m) => m.id === id)
|
|
266
418
|
if (found) return found
|
|
267
419
|
return CLINE_MODELS.find((m) => m.id === id) || null
|
|
268
420
|
}
|
|
269
421
|
|
|
270
|
-
export function isSupportedModel(id, dynamicModels = []) {
|
|
271
|
-
return findModel(id, dynamicModels) !== null
|
|
422
|
+
export function isSupportedModel(id, dynamicModels = [], modelContextOverrides = []) {
|
|
423
|
+
return findModel(id, dynamicModels, modelContextOverrides) !== null
|
|
272
424
|
}
|
|
273
425
|
|
|
274
|
-
export function getDefaultModelIds(dynamicModels = []) {
|
|
275
|
-
return getAllModels(dynamicModels).map((m) => m.id)
|
|
426
|
+
export function getDefaultModelIds(dynamicModels = [], modelContextOverrides = []) {
|
|
427
|
+
return getAllModels(dynamicModels, modelContextOverrides).map((m) => m.id)
|
|
276
428
|
}
|
|
277
429
|
|
|
278
430
|
/**
|
|
@@ -287,12 +439,18 @@ export function getActiveModelIds(allModels = [], disabledModelIds = []) {
|
|
|
287
439
|
}
|
|
288
440
|
|
|
289
441
|
/**
|
|
290
|
-
* Format context length cleanly (e.g. 200000 -> '200K', 128000 -> '128K').
|
|
442
|
+
* Format context length cleanly (e.g. 2000000 -> '2M', 1000000 -> '1M', 200000 -> '200K', 128000 -> '128K').
|
|
291
443
|
*/
|
|
292
444
|
export function formatModelContext(contextLength) {
|
|
293
445
|
const num = Number(contextLength) || 200000
|
|
294
|
-
if (num >= 1000000)
|
|
295
|
-
|
|
446
|
+
if (num >= 1000000) {
|
|
447
|
+
const m = num / 1000000
|
|
448
|
+
return `${m % 1 === 0 ? m : m.toFixed(1)}M`
|
|
449
|
+
}
|
|
450
|
+
if (num >= 1000) {
|
|
451
|
+
const k = num / 1000
|
|
452
|
+
return `${k % 1 === 0 ? k : k.toFixed(1)}K`
|
|
453
|
+
}
|
|
296
454
|
return String(num)
|
|
297
455
|
}
|
|
298
456
|
|
|
@@ -318,8 +476,8 @@ export function formatModelDescription(model) {
|
|
|
318
476
|
/**
|
|
319
477
|
* Check if a model supports vision input.
|
|
320
478
|
*/
|
|
321
|
-
export function isVisionModel(id, dynamicModels = []) {
|
|
322
|
-
const m = findModel(id, dynamicModels)
|
|
479
|
+
export function isVisionModel(id, dynamicModels = [], modelContextOverrides = []) {
|
|
480
|
+
const m = findModel(id, dynamicModels, modelContextOverrides)
|
|
323
481
|
if (!m) return false
|
|
324
482
|
return Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
325
483
|
}
|
|
@@ -348,12 +506,20 @@ export async function loadModelsDiskCache(cachePath) {
|
|
|
348
506
|
const fs = await import('node:fs/promises')
|
|
349
507
|
const raw = await fs.readFile(cachePath, 'utf8')
|
|
350
508
|
const parsed = JSON.parse(raw)
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
509
|
+
const rawList = Array.isArray(parsed?.models) ? parsed.models : (Array.isArray(parsed) ? parsed : null)
|
|
510
|
+
if (!rawList) return null
|
|
511
|
+
let hasObsolete = false
|
|
512
|
+
const list = rawList.map((m) => {
|
|
513
|
+
const migrated = migrateModelEntry(m)
|
|
514
|
+
if (migrated.id !== m.id) hasObsolete = true
|
|
515
|
+
return migrated
|
|
516
|
+
})
|
|
353
517
|
list.planSyncedAt = typeof parsed?.planSyncedAt === 'number' ? parsed.planSyncedAt : (typeof parsed?.savedAt === 'number' ? parsed.savedAt : 0)
|
|
518
|
+
if (hasObsolete) {
|
|
519
|
+
saveModelsDiskCache(cachePath, list, list.planSyncedAt).catch(() => {})
|
|
520
|
+
}
|
|
354
521
|
return list
|
|
355
522
|
} catch {
|
|
356
523
|
return null
|
|
357
524
|
}
|
|
358
525
|
}
|
|
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
|
|
|
@@ -217,12 +217,26 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
|
|
|
217
217
|
const cacheFile = resolvePathWithHome(pub.modelsCachePath)
|
|
218
218
|
const settingsApi = getSettingsApi()
|
|
219
219
|
|
|
220
|
+
// Auto-migrate obsolete IDs (e.g. deepseek-v41-flash -> deepseek-v4.1-flash)
|
|
221
|
+
const existingDynamic = pub.dynamicModels || []
|
|
222
|
+
const hasObsolete = existingDynamic.some((m) => Boolean(OBSOLETE_MODEL_ID_MAP[m?.id]))
|
|
223
|
+
if (hasObsolete && settingsApi?.replace) {
|
|
224
|
+
const migrated = existingDynamic.map(migrateModelEntry)
|
|
225
|
+
const next = Config({
|
|
226
|
+
...plainConfig(live()),
|
|
227
|
+
dynamicModels: migrated,
|
|
228
|
+
})
|
|
229
|
+
await settingsApi.replace(next)
|
|
230
|
+
await syncProviderState(next)
|
|
231
|
+
}
|
|
232
|
+
|
|
220
233
|
if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
|
|
221
234
|
const fromDisk = await loadModelsDiskCache(cacheFile)
|
|
222
235
|
if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
|
|
236
|
+
const migratedDisk = fromDisk.map(migrateModelEntry)
|
|
223
237
|
const next = Config({
|
|
224
238
|
...plainConfig(live()),
|
|
225
|
-
dynamicModels:
|
|
239
|
+
dynamicModels: migratedDisk,
|
|
226
240
|
planSyncedAt: fromDisk.planSyncedAt || Date.now(),
|
|
227
241
|
})
|
|
228
242
|
await settingsApi.replace(next)
|
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
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"README.ru.md",
|
|
19
19
|
"README.zh.md",
|
|
20
20
|
"CHANGELOG.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"media/visual-verification.png"
|
|
22
23
|
],
|
|
23
24
|
"keywords": [
|
|
24
25
|
"dsh",
|