@goodandready/dsh-subscriptions 0.7.44 → 0.7.46
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/lib/client.js +57 -0
- package/lib/client.panels.js +156 -1
- package/lib/config-schema.js +2 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -283,6 +283,27 @@ window.__ModuleLoader__.load({
|
|
|
283
283
|
"providerGallery": "Add provider",
|
|
284
284
|
"tabOverview": "Overview",
|
|
285
285
|
"tabAccounts": "Accounts",
|
|
286
|
+
"tabDashboard": "Dashboard",
|
|
287
|
+
"accountsDashboard": "Accounts dashboard",
|
|
288
|
+
"sortRisk": "Risk",
|
|
289
|
+
"sortQuota": "Quota",
|
|
290
|
+
"sortName": "Name",
|
|
291
|
+
"speedSubagent": "Subagent speed",
|
|
292
|
+
"statusHealthy": "healthy",
|
|
293
|
+
"statusCooling": "cooling",
|
|
294
|
+
"statusQuarantined": "quarantined",
|
|
295
|
+
"quotaUsed": "quota used",
|
|
296
|
+
"requests": "requests",
|
|
297
|
+
"errors": "errors",
|
|
298
|
+
"loadUsage": "Load usage",
|
|
299
|
+
"modelEditor": "Model editor",
|
|
300
|
+
"discovered": "discovered",
|
|
301
|
+
"shown": "shown",
|
|
302
|
+
"searchModels": "Search models by name or id",
|
|
303
|
+
"compact": "Compact",
|
|
304
|
+
"heatmap": "Heatmap · last 24h",
|
|
305
|
+
"editModels": "Edit models",
|
|
306
|
+
"hidden": "hidden",
|
|
286
307
|
"tabRouting": "Routing",
|
|
287
308
|
"tabModels": "Models",
|
|
288
309
|
"tabVault": "Vault",
|
|
@@ -691,6 +712,27 @@ window.__ModuleLoader__.load({
|
|
|
691
712
|
"providerGallery": "添加服务商",
|
|
692
713
|
"tabOverview": "概览",
|
|
693
714
|
"tabAccounts": "账户",
|
|
715
|
+
"tabDashboard": "仪表盘",
|
|
716
|
+
"accountsDashboard": "账户仪表盘",
|
|
717
|
+
"sortRisk": "风险",
|
|
718
|
+
"sortQuota": "配额",
|
|
719
|
+
"sortName": "名称",
|
|
720
|
+
"speedSubagent": "子代理速度",
|
|
721
|
+
"statusHealthy": "正常",
|
|
722
|
+
"statusCooling": "冷却中",
|
|
723
|
+
"statusQuarantined": "隔离中",
|
|
724
|
+
"quotaUsed": "已用配额",
|
|
725
|
+
"requests": "请求",
|
|
726
|
+
"errors": "错误",
|
|
727
|
+
"loadUsage": "加载用量",
|
|
728
|
+
"modelEditor": "模型编辑器",
|
|
729
|
+
"discovered": "已发现",
|
|
730
|
+
"shown": "已显示",
|
|
731
|
+
"searchModels": "按名称或 ID 搜索模型",
|
|
732
|
+
"compact": "紧凑模式",
|
|
733
|
+
"heatmap": "热力图 · 最近 24 小时",
|
|
734
|
+
"editModels": "编辑模型",
|
|
735
|
+
"hidden": "隐藏",
|
|
694
736
|
"tabRouting": "路由",
|
|
695
737
|
"tabModels": "模型",
|
|
696
738
|
"tabVault": "Vault",
|
|
@@ -1046,6 +1088,7 @@ window.__ModuleLoader__.load({
|
|
|
1046
1088
|
const VaultSnapshotsPanel = lazyPanel('VaultSnapshotsPanel')
|
|
1047
1089
|
const ProviderPacksPanel = lazyPanel('ProviderPacksPanel')
|
|
1048
1090
|
const SourcesPanel = lazyPanel('SourcesPanel')
|
|
1091
|
+
const AccountsDashboardPanel = lazyPanel('AccountsDashboardPanel')
|
|
1049
1092
|
|
|
1050
1093
|
// #839 model-list helpers
|
|
1051
1094
|
function modelListSearchText(model) {
|
|
@@ -1955,6 +1998,7 @@ window.__ModuleLoader__.load({
|
|
|
1955
1998
|
[
|
|
1956
1999
|
['overview', t('tabOverview')],
|
|
1957
2000
|
['accounts', t('tabAccounts')],
|
|
2001
|
+
['dashboard', t('tabDashboard')],
|
|
1958
2002
|
['routing', t('tabRouting')],
|
|
1959
2003
|
['models', t('tabModels')],
|
|
1960
2004
|
['vault', t('tabVault')],
|
|
@@ -2221,6 +2265,19 @@ h('div', { className: 'dsub-row' },
|
|
|
2221
2265
|
h('summary', { className: 'dsub-groupHead' }, t('sources')),
|
|
2222
2266
|
h(SourcesPanel, { t }),
|
|
2223
2267
|
),
|
|
2268
|
+
h('div', { className: 'dsub-group', 'data-pane': 'dashboard' },
|
|
2269
|
+
h(AccountsDashboardPanel, {
|
|
2270
|
+
t,
|
|
2271
|
+
labels: {
|
|
2272
|
+
accountsDashboard: t('accountsDashboard'),
|
|
2273
|
+
sortRisk: t('sortRisk'), sortQuota: t('sortQuota'), sortName: t('sortName'),
|
|
2274
|
+
speedSubagent: t('speedSubagent'),
|
|
2275
|
+
statusHealthy: t('statusHealthy'), statusCooling: t('statusCooling'), statusQuarantined: t('statusQuarantined'),
|
|
2276
|
+
quotaUsed: t('quotaUsed'), requests: t('requests'), errors: t('errors'), loadUsage: t('loadUsage'),
|
|
2277
|
+
modelEditor: t('modelEditor'), discovered: t('discovered'), shown: t('shown'), searchModels: t('searchModels'),
|
|
2278
|
+
},
|
|
2279
|
+
}),
|
|
2280
|
+
),
|
|
2224
2281
|
h('details', { className: 'dsub-group', 'data-pane': 'models' },
|
|
2225
2282
|
h('summary', { className: 'dsub-groupHead' }, t('providerPacks')),
|
|
2226
2283
|
h(ProviderPacksPanel, { t }),
|
package/lib/client.panels.js
CHANGED
|
@@ -182,6 +182,161 @@ window.__ModuleLoader__.load({
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
// #789/#807/#812/#802: account dashboard, model editor, lazy usage and speed controls.
|
|
186
|
+
function AccountsDashboardPanel(props) {
|
|
187
|
+
const t = (props && props.t) || ((k) => k)
|
|
188
|
+
const [data, setData] = React.useState(null)
|
|
189
|
+
const [provider, setProvider] = React.useState('')
|
|
190
|
+
const [query, setQuery] = React.useState('')
|
|
191
|
+
const [sort, setSort] = React.useState('risk')
|
|
192
|
+
const [usage, setUsage] = React.useState({})
|
|
193
|
+
const [speed, setSpeed] = React.useState('standard')
|
|
194
|
+
const [analytics, setAnalytics] = React.useState(null)
|
|
195
|
+
const [compact, setCompact] = React.useState(false)
|
|
196
|
+
const [editing, setEditing] = React.useState(false)
|
|
197
|
+
const [draft, setDraft] = React.useState({})
|
|
198
|
+
const usageRef = React.useRef({ inflight: {}, gen: {} })
|
|
199
|
+
const pull = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
200
|
+
.then((res) => res.json())
|
|
201
|
+
.then((cfg) => {
|
|
202
|
+
setData(cfg)
|
|
203
|
+
setSpeed((cfg.config && cfg.config.codexSubagentSpeed) || 'standard')
|
|
204
|
+
setDraft(JSON.parse(JSON.stringify((cfg.config && cfg.config.modelOverrides) || {})))
|
|
205
|
+
if (!provider && cfg.accounts && cfg.accounts.length) setProvider(cfg.accounts[0].provider)
|
|
206
|
+
})
|
|
207
|
+
.catch(() => {})
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
pull()
|
|
210
|
+
fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
|
|
211
|
+
.then((res) => res.json())
|
|
212
|
+
.then((out) => setAnalytics((out && out.analytics) || null))
|
|
213
|
+
.catch(() => {})
|
|
214
|
+
}, [])
|
|
215
|
+
const saveOverrides = () => {
|
|
216
|
+
if (!data || !data.config) return
|
|
217
|
+
fetch('/dsh-subscriptions/config', {
|
|
218
|
+
method: 'PUT',
|
|
219
|
+
headers: { 'content-type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ config: Object.assign({}, data.config, { modelOverrides: draft }), revision: data.revision }),
|
|
221
|
+
}).then(() => { setEditing(false); pull() }).catch(() => {})
|
|
222
|
+
}
|
|
223
|
+
const cancelOverrides = () => {
|
|
224
|
+
setDraft(JSON.parse(JSON.stringify((data && data.config && data.config.modelOverrides) || {})))
|
|
225
|
+
setEditing(false)
|
|
226
|
+
}
|
|
227
|
+
const loadUsage = (row) => {
|
|
228
|
+
const key = row.provider + ':' + row.index
|
|
229
|
+
if (usageRef.current.inflight[key]) return
|
|
230
|
+
const gen = (usageRef.current.gen[key] || 0) + 1
|
|
231
|
+
usageRef.current.gen[key] = gen
|
|
232
|
+
usageRef.current.inflight[key] = true
|
|
233
|
+
setUsage((m) => Object.assign({}, m, { [key]: { loading: true } }))
|
|
234
|
+
fetch('/dsh-subscriptions/usage?provider=' + encodeURIComponent(row.provider) + '&index=' + encodeURIComponent(row.index), { cache: 'no-store' })
|
|
235
|
+
.then((res) => res.json())
|
|
236
|
+
.then((out) => {
|
|
237
|
+
if (usageRef.current.gen[key] !== gen) return
|
|
238
|
+
setUsage((m) => Object.assign({}, m, { [key]: { value: out, loading: false } }))
|
|
239
|
+
})
|
|
240
|
+
.catch((e) => {
|
|
241
|
+
if (usageRef.current.gen[key] === gen) setUsage((m) => Object.assign({}, m, { [key]: { loading: false, error: String(e && e.message || e) } }))
|
|
242
|
+
})
|
|
243
|
+
.finally(() => { usageRef.current.inflight[key] = false })
|
|
244
|
+
}
|
|
245
|
+
const saveSpeed = (value) => {
|
|
246
|
+
setSpeed(value)
|
|
247
|
+
if (!data || !data.config) return
|
|
248
|
+
fetch('/dsh-subscriptions/config', {
|
|
249
|
+
method: 'PUT',
|
|
250
|
+
headers: { 'content-type': 'application/json' },
|
|
251
|
+
body: JSON.stringify({ config: Object.assign({}, data.config, { codexSubagentSpeed: value }), revision: data.revision }),
|
|
252
|
+
}).then(pull).catch(() => {})
|
|
253
|
+
}
|
|
254
|
+
const accounts = (data && data.accounts) || []
|
|
255
|
+
const selected = provider || (accounts[0] && accounts[0].provider) || ''
|
|
256
|
+
const rows = accounts.filter((row) => row.provider === selected).sort((a, b) => {
|
|
257
|
+
if (sort === 'name') return String(a.label || a.email || a.ref).localeCompare(String(b.label || b.email || b.ref))
|
|
258
|
+
if (sort === 'quota') return (b.usagePercent || 0) - (a.usagePercent || 0)
|
|
259
|
+
return (b.usagePercent || 0) + (b.errors || 0) * 5 - ((a.usagePercent || 0) + (a.errors || 0) * 5)
|
|
260
|
+
})
|
|
261
|
+
const models = ((data && data.modelsCatalog) || []).filter((m) => m.provider === selected)
|
|
262
|
+
const q = query.trim().toLowerCase()
|
|
263
|
+
const shown = models.filter((m) => !q || [m.id, m.name, m.alias].some((v) => String(v || '').toLowerCase().includes(q)))
|
|
264
|
+
const providers = Array.from(new Set(accounts.map((row) => row.provider).filter(Boolean)))
|
|
265
|
+
return h('div', { className: 'dsub-section-card' },
|
|
266
|
+
h('div', { className: 'dsub-section-title' }, t('accountsDashboard'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
267
|
+
h('div', { className: 'dsub-row' },
|
|
268
|
+
h('select', { className: 'dsub-select', value: selected, onChange: (e) => setProvider(e.target.value) },
|
|
269
|
+
providers.map((id) => h('option', { key: id, value: id }, id))),
|
|
270
|
+
h('select', { className: 'dsub-select', value: sort, onChange: (e) => setSort(e.target.value) },
|
|
271
|
+
['risk', 'quota', 'name'].map((id) => h('option', { key: id, value: id }, t('sort' + id[0].toUpperCase() + id.slice(1))))),
|
|
272
|
+
h('span', { className: 'dsub-sub' }, t('speedSubagent') + ':'),
|
|
273
|
+
h('select', { className: 'dsub-select', value: speed, onChange: (e) => saveSpeed(e.target.value) },
|
|
274
|
+
h('option', { value: 'standard' }, 'Standard'), h('option', { value: 'fast' }, 'Fast')),
|
|
275
|
+
),
|
|
276
|
+
rows.map((row, i) => {
|
|
277
|
+
const key = row.provider + ':' + row.index
|
|
278
|
+
const u = usage[key]
|
|
279
|
+
return h('div', { key: key + ':' + i, className: 'dsub-group', style: { padding: '8px' } },
|
|
280
|
+
h('div', { className: 'dsub-row' },
|
|
281
|
+
h('span', { className: 'dsub-h' }, '#' + row.index + ' ' + (row.label || row.email || row.ref)),
|
|
282
|
+
h('span', { className: 'dsub-pillTag' }, row.quarantineUntil > Date.now() ? t('statusQuarantined') : (row.cooldownUntil > Date.now() ? t('statusCooling') : t('statusHealthy')))),
|
|
283
|
+
h('div', { className: 'dsub-sub' }, t('quotaUsed') + ': ' + (row.usagePercent == null ? '—' : row.usagePercent + '%') + ' · ' + t('requests') + ' ' + (row.requests || 0) + ' · ' + t('errors') + ' ' + (row.errors || 0)),
|
|
284
|
+
h('div', { className: 'dsub-row' },
|
|
285
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => loadUsage(row) }, u && u.loading ? '…' : t('loadUsage')),
|
|
286
|
+
u && u.error ? h('span', { className: 'dsub-bad' }, u.error) : null,
|
|
287
|
+
u && u.value ? h('span', { className: 'dsub-dim' }, JSON.stringify(u.value).slice(0, 180)) : null),
|
|
288
|
+
)
|
|
289
|
+
}),
|
|
290
|
+
h('div', { className: 'dsub-row' },
|
|
291
|
+
h('label', { className: 'dsub-sub' }, h('input', { type: 'checkbox', checked: compact, onChange: (e) => setCompact(e.target.checked) }), ' ' + t('compact')),
|
|
292
|
+
),
|
|
293
|
+
h('div', { className: 'dsub-h' }, t('heatmap')),
|
|
294
|
+
h('div', { className: 'dsub-row', style: { gap: 2 } },
|
|
295
|
+
((analytics && analytics.hourly) || []).slice(-24).map((hour, i) => h('span', {
|
|
296
|
+
key: i,
|
|
297
|
+
title: (hour && hour.requests || 0) + ' req',
|
|
298
|
+
style: { display: 'inline-block', width: 8, height: 18, background: 'rgba(88,166,255,' + Math.min(1, ((hour && hour.requests) || 0) / 10 + 0.1) + ')' },
|
|
299
|
+
}))),
|
|
300
|
+
h('div', { className: 'dsub-row' },
|
|
301
|
+
h('span', { className: 'dsub-h' }, t('modelEditor') + ' · ' + t('discovered') + ' ' + models.length + ' · ' + t('shown') + ' ' + shown.length),
|
|
302
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => setEditing(!editing) }, t('editModels')),
|
|
303
|
+
editing ? h('button', { type: 'button', className: 'dsub-mini', onClick: saveOverrides }, t('save')) : null,
|
|
304
|
+
editing ? h('button', { type: 'button', className: 'dsub-mini', onClick: cancelOverrides }, t('cancel')) : null,
|
|
305
|
+
),
|
|
306
|
+
h('input', { className: 'dsub-grow', value: query, placeholder: t('searchModels'), onChange: (e) => setQuery(e.target.value) }),
|
|
307
|
+
shown.slice(0, 40).map((m) => h('div', { key: m.id, className: 'dsub-row' },
|
|
308
|
+
h('span', { className: 'dsub-h' }, m.independent ? '★ ' : ''),
|
|
309
|
+
editing
|
|
310
|
+
? h('input', {
|
|
311
|
+
className: 'dsub-grow',
|
|
312
|
+
value: (draft[m.id] && draft[m.id].name) || '',
|
|
313
|
+
placeholder: m.name || m.id,
|
|
314
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { name: e.target.value }) })),
|
|
315
|
+
})
|
|
316
|
+
: h('span', { className: 'dsub-sub' }, (draft[m.id] && draft[m.id].name) || m.name || m.id),
|
|
317
|
+
editing ? h('input', {
|
|
318
|
+
type: 'number',
|
|
319
|
+
className: 'dsub-inp',
|
|
320
|
+
style: { width: '90px', minWidth: '90px' },
|
|
321
|
+
value: (draft[m.id] && draft[m.id].contextWindow) || '',
|
|
322
|
+
placeholder: 'ctx',
|
|
323
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { contextWindow: Number(e.target.value) || undefined }) })),
|
|
324
|
+
}) : null,
|
|
325
|
+
editing ? h('select', {
|
|
326
|
+
className: 'dsub-select',
|
|
327
|
+
value: (draft[m.id] && draft[m.id].reasoningEffort) || '',
|
|
328
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { reasoningEffort: e.target.value || undefined }) })),
|
|
329
|
+
}, ['', 'low', 'medium', 'high'].map((level) => h('option', { key: level || 'default', value: level }, level || 'effort'))) : null,
|
|
330
|
+
editing ? h('label', { className: 'dsub-dim' },
|
|
331
|
+
h('input', {
|
|
332
|
+
type: 'checkbox',
|
|
333
|
+
checked: !!(draft[m.id] && draft[m.id].hidden),
|
|
334
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { [m.id]: Object.assign({}, d[m.id], { hidden: e.target.checked }) })),
|
|
335
|
+
}), ' ' + t('hidden')) : null,
|
|
336
|
+
h('span', { className: 'dsub-dim' }, m.id))),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel, AccountsDashboardPanel }
|
|
186
341
|
}
|
|
187
342
|
})
|
package/lib/config-schema.js
CHANGED
|
@@ -171,6 +171,8 @@ export const Config = z.object({
|
|
|
171
171
|
.description('#788 Per-provider model glob rules. First match wins; account may be a ref, label or index.'),
|
|
172
172
|
tiers: z.array(z.any()).volatile().default([])
|
|
173
173
|
.description('#817 Named picker tiers: { id, name, members: [accountRef] }. No cross-tier fallback.'),
|
|
174
|
+
modelOverrides: z.any().volatile().default({})
|
|
175
|
+
.description('#807 Per-model editor overrides: display name, hidden, context window, default reasoning effort.'),
|
|
174
176
|
rotationStrategy: z.string().volatile().default('round-robin')
|
|
175
177
|
.description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), quota-aware (urgent quota first), or priority (sequential).'),
|
|
176
178
|
poolEnabled: z.boolean().volatile().default(true)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.46",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|