@goodandready/dsh-subscriptions 0.7.43 → 0.7.45
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/account-features.js +133 -0
- package/lib/accounts-view.js +2 -0
- package/lib/accounts.js +4 -0
- package/lib/client.js +49 -0
- package/lib/client.panels.js +90 -1
- package/lib/config-schema.js +6 -0
- package/lib/models-catalog.js +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
function percentile(values, p) {
|
|
2
|
+
const list = (Array.isArray(values) ? values : []).filter(Number.isFinite).sort((a, b) => a - b)
|
|
3
|
+
if (!list.length) return null
|
|
4
|
+
return list[Math.min(list.length - 1, Math.max(0, Math.ceil((p / 100) * list.length) - 1))]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** #796: independent picker entries are bound to the stable credential ref. */
|
|
8
|
+
export function buildIndependentEntries(accounts, modelsByProvider = {}) {
|
|
9
|
+
const out = []
|
|
10
|
+
for (const account of Array.isArray(accounts) ? accounts : []) {
|
|
11
|
+
if (!account || account.independentEntry !== true || account.hasToken === false) continue
|
|
12
|
+
const alias = String(account.independentAlias || account.label || account.ref || '').trim()
|
|
13
|
+
for (const model of modelsByProvider[account.provider] || []) {
|
|
14
|
+
const id = typeof model === 'string' ? model : model && (model.id || model.slug)
|
|
15
|
+
if (!id) continue
|
|
16
|
+
out.push({
|
|
17
|
+
id: 'independent:' + account.ref + ':' + id,
|
|
18
|
+
model: id,
|
|
19
|
+
provider: account.provider,
|
|
20
|
+
accountRef: account.ref,
|
|
21
|
+
alias,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveIndependentEntry(entry, accounts) {
|
|
29
|
+
if (!entry || entry.accountRef == null) return null
|
|
30
|
+
const account = (Array.isArray(accounts) ? accounts : []).find((row) => row && row.ref === entry.accountRef)
|
|
31
|
+
if (!account || account.hasToken === false) return null
|
|
32
|
+
return account
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** #812: per-account usage loading with in-flight dedupe, stale-drop and cleanup. */
|
|
36
|
+
export function createUsageLoader({ load } = {}) {
|
|
37
|
+
const inflight = new Map()
|
|
38
|
+
const generation = new Map()
|
|
39
|
+
const state = new Map()
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
load(key, { force = false } = {}) {
|
|
43
|
+
if (!force && inflight.has(key)) return inflight.get(key)
|
|
44
|
+
const gen = (generation.get(key) || 0) + 1
|
|
45
|
+
generation.set(key, gen)
|
|
46
|
+
state.set(key, { ...(state.get(key) || {}), loading: true, error: null })
|
|
47
|
+
const promise = Promise.resolve()
|
|
48
|
+
.then(() => load(key))
|
|
49
|
+
.then((value) => {
|
|
50
|
+
if (generation.get(key) !== gen) return state.get(key)
|
|
51
|
+
state.set(key, { value, loading: false, error: null })
|
|
52
|
+
return state.get(key)
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
if (generation.get(key) !== gen) return state.get(key)
|
|
56
|
+
state.set(key, { ...(state.get(key) || {}), loading: false, error: String(error && error.message || error) })
|
|
57
|
+
return state.get(key)
|
|
58
|
+
})
|
|
59
|
+
.finally(() => { if (inflight.get(key) === promise) inflight.delete(key) })
|
|
60
|
+
inflight.set(key, promise)
|
|
61
|
+
return promise
|
|
62
|
+
},
|
|
63
|
+
get(key) { return state.get(key) || null },
|
|
64
|
+
clearMissing(validKeys) {
|
|
65
|
+
const keep = new Set(validKeys || [])
|
|
66
|
+
for (const key of [...state.keys()]) if (!keep.has(key)) state.delete(key)
|
|
67
|
+
for (const key of [...inflight.keys()]) if (!keep.has(key)) inflight.delete(key)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** #802: explicit child choice > subagent default > session choice > global. */
|
|
73
|
+
export function resolveSpeed({ childSpeed, sessionSpeed, subagentSpeed, modelSupportsFast = true } = {}) {
|
|
74
|
+
const pick = [childSpeed, subagentSpeed, sessionSpeed].find((value) => value === 'standard' || value === 'fast')
|
|
75
|
+
if (pick === 'fast' && !modelSupportsFast) {
|
|
76
|
+
const err = new Error('this model does not support fast speed')
|
|
77
|
+
err.code = 'FAST_UNSUPPORTED'
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
return pick || 'standard'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** #789: per-provider account dashboard rows and 24h heatmap. */
|
|
84
|
+
export function accountDashboard(accounts, { now = Date.now(), sort = 'risk' } = {}) {
|
|
85
|
+
const rows = (Array.isArray(accounts) ? accounts : []).map((account) => {
|
|
86
|
+
const quota = account.usagePercent == null ? null : Number(account.usagePercent)
|
|
87
|
+
const burn = Number(account.burnRatePerHour) || 0
|
|
88
|
+
const runwayMinutes = quota != null && burn > 0 ? Math.max(0, Math.round(((100 - quota) / burn) * 60)) : null
|
|
89
|
+
const latencies = (account.latencies || []).filter(Number.isFinite)
|
|
90
|
+
return {
|
|
91
|
+
ref: account.ref,
|
|
92
|
+
provider: account.provider,
|
|
93
|
+
label: account.label || account.email || account.ref,
|
|
94
|
+
status: account.quarantineUntil && account.quarantineUntil > now ? 'quarantined'
|
|
95
|
+
: account.cooldownUntil && account.cooldownUntil > now ? 'cooling' : 'healthy',
|
|
96
|
+
quota,
|
|
97
|
+
runwayMinutes,
|
|
98
|
+
burnRatePerHour: burn,
|
|
99
|
+
requests: Number(account.requests) || 0,
|
|
100
|
+
errors: Number(account.errors) || 0,
|
|
101
|
+
p95LatencyMs: percentile(latencies, 95),
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
const risk = (row) => (row.quota == null ? 1000 : row.quota) + row.errors * 5
|
|
105
|
+
if (sort === 'name') rows.sort((a, b) => String(a.label).localeCompare(String(b.label)))
|
|
106
|
+
else if (sort === 'quota') rows.sort((a, b) => (a.quota ?? 1000) - (b.quota ?? 1000))
|
|
107
|
+
else rows.sort((a, b) => risk(b) - risk(a))
|
|
108
|
+
return rows
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** #807: searchable model editor with counters and bounded rendering. */
|
|
112
|
+
export function modelEditor(models, { query = '', overrides = {}, limit = 40 } = {}) {
|
|
113
|
+
const q = String(query || '').trim().toLowerCase()
|
|
114
|
+
const rows = (Array.isArray(models) ? models : []).map((model) => {
|
|
115
|
+
const id = typeof model === 'string' ? model : model && (model.id || model.slug)
|
|
116
|
+
const name = typeof model === 'string' ? model : model && model.name
|
|
117
|
+
const override = overrides[id] || {}
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
name: override.name || name || id,
|
|
121
|
+
hidden: override.hidden === true,
|
|
122
|
+
contextWindow: override.contextWindow || (model && model.contextWindow) || null,
|
|
123
|
+
reasoningEffort: override.reasoningEffort || null,
|
|
124
|
+
}
|
|
125
|
+
}).filter((row) => row.id && (!q || [row.id, row.name].some((value) => String(value || '').toLowerCase().includes(q))))
|
|
126
|
+
return {
|
|
127
|
+
rows: rows.slice(0, Math.max(0, Number(limit) || 0)),
|
|
128
|
+
total: rows.length,
|
|
129
|
+
discovered: Array.isArray(models) ? models.length : 0,
|
|
130
|
+
overridden: Object.keys(overrides || {}).length,
|
|
131
|
+
hidden: rows.filter((row) => row.hidden).length,
|
|
132
|
+
}
|
|
133
|
+
}
|
package/lib/accounts-view.js
CHANGED
|
@@ -169,6 +169,8 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
169
169
|
poolEnabled: slot.poolEnabled !== false,
|
|
170
170
|
isDefault: !!slot.isDefault,
|
|
171
171
|
proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
|
|
172
|
+
independentEntry: slot.independentEntry === true,
|
|
173
|
+
independentAlias: slot.independentAlias || '',
|
|
172
174
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
173
175
|
credentialError: info.credentialError || null,
|
|
174
176
|
duplicateOf: info.duplicateOf || null,
|
package/lib/accounts.js
CHANGED
|
@@ -76,6 +76,8 @@ export function normalizeSlots(slots) {
|
|
|
76
76
|
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
77
77
|
...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
|
|
78
78
|
...(Array.isArray(slot.proxyBypass) ? { proxyBypass: slot.proxyBypass.map((host) => String(host).trim()).filter(Boolean) } : {}),
|
|
79
|
+
...(slot.independentEntry === true ? { independentEntry: true } : {}),
|
|
80
|
+
...(slot.independentAlias ? { independentAlias: String(slot.independentAlias).trim() } : {}),
|
|
79
81
|
...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
|
|
80
82
|
...(slot.isDefault ? { isDefault: true } : {}),
|
|
81
83
|
...(Array.isArray(slot.poolModels)
|
|
@@ -433,6 +435,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
433
435
|
poolEnabled: slot.poolEnabled !== false,
|
|
434
436
|
isDefault: !!slot.isDefault,
|
|
435
437
|
proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
|
|
438
|
+
independentEntry: slot.independentEntry === true,
|
|
439
|
+
independentAlias: slot.independentAlias || '',
|
|
436
440
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
437
441
|
credentialError: info.credentialError || null,
|
|
438
442
|
lastUsedAt: lastUsedAt.get(slot.ref) || 0,
|
package/lib/client.js
CHANGED
|
@@ -283,6 +283,23 @@ 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",
|
|
286
303
|
"tabRouting": "Routing",
|
|
287
304
|
"tabModels": "Models",
|
|
288
305
|
"tabVault": "Vault",
|
|
@@ -691,6 +708,23 @@ window.__ModuleLoader__.load({
|
|
|
691
708
|
"providerGallery": "添加服务商",
|
|
692
709
|
"tabOverview": "概览",
|
|
693
710
|
"tabAccounts": "账户",
|
|
711
|
+
"tabDashboard": "仪表盘",
|
|
712
|
+
"accountsDashboard": "账户仪表盘",
|
|
713
|
+
"sortRisk": "风险",
|
|
714
|
+
"sortQuota": "配额",
|
|
715
|
+
"sortName": "名称",
|
|
716
|
+
"speedSubagent": "子代理速度",
|
|
717
|
+
"statusHealthy": "正常",
|
|
718
|
+
"statusCooling": "冷却中",
|
|
719
|
+
"statusQuarantined": "隔离中",
|
|
720
|
+
"quotaUsed": "已用配额",
|
|
721
|
+
"requests": "请求",
|
|
722
|
+
"errors": "错误",
|
|
723
|
+
"loadUsage": "加载用量",
|
|
724
|
+
"modelEditor": "模型编辑器",
|
|
725
|
+
"discovered": "已发现",
|
|
726
|
+
"shown": "已显示",
|
|
727
|
+
"searchModels": "按名称或 ID 搜索模型",
|
|
694
728
|
"tabRouting": "路由",
|
|
695
729
|
"tabModels": "模型",
|
|
696
730
|
"tabVault": "Vault",
|
|
@@ -1046,6 +1080,7 @@ window.__ModuleLoader__.load({
|
|
|
1046
1080
|
const VaultSnapshotsPanel = lazyPanel('VaultSnapshotsPanel')
|
|
1047
1081
|
const ProviderPacksPanel = lazyPanel('ProviderPacksPanel')
|
|
1048
1082
|
const SourcesPanel = lazyPanel('SourcesPanel')
|
|
1083
|
+
const AccountsDashboardPanel = lazyPanel('AccountsDashboardPanel')
|
|
1049
1084
|
|
|
1050
1085
|
// #839 model-list helpers
|
|
1051
1086
|
function modelListSearchText(model) {
|
|
@@ -1955,6 +1990,7 @@ window.__ModuleLoader__.load({
|
|
|
1955
1990
|
[
|
|
1956
1991
|
['overview', t('tabOverview')],
|
|
1957
1992
|
['accounts', t('tabAccounts')],
|
|
1993
|
+
['dashboard', t('tabDashboard')],
|
|
1958
1994
|
['routing', t('tabRouting')],
|
|
1959
1995
|
['models', t('tabModels')],
|
|
1960
1996
|
['vault', t('tabVault')],
|
|
@@ -2221,6 +2257,19 @@ h('div', { className: 'dsub-row' },
|
|
|
2221
2257
|
h('summary', { className: 'dsub-groupHead' }, t('sources')),
|
|
2222
2258
|
h(SourcesPanel, { t }),
|
|
2223
2259
|
),
|
|
2260
|
+
h('div', { className: 'dsub-group', 'data-pane': 'dashboard' },
|
|
2261
|
+
h(AccountsDashboardPanel, {
|
|
2262
|
+
t,
|
|
2263
|
+
labels: {
|
|
2264
|
+
accountsDashboard: t('accountsDashboard'),
|
|
2265
|
+
sortRisk: t('sortRisk'), sortQuota: t('sortQuota'), sortName: t('sortName'),
|
|
2266
|
+
speedSubagent: t('speedSubagent'),
|
|
2267
|
+
statusHealthy: t('statusHealthy'), statusCooling: t('statusCooling'), statusQuarantined: t('statusQuarantined'),
|
|
2268
|
+
quotaUsed: t('quotaUsed'), requests: t('requests'), errors: t('errors'), loadUsage: t('loadUsage'),
|
|
2269
|
+
modelEditor: t('modelEditor'), discovered: t('discovered'), shown: t('shown'), searchModels: t('searchModels'),
|
|
2270
|
+
},
|
|
2271
|
+
}),
|
|
2272
|
+
),
|
|
2224
2273
|
h('details', { className: 'dsub-group', 'data-pane': 'models' },
|
|
2225
2274
|
h('summary', { className: 'dsub-groupHead' }, t('providerPacks')),
|
|
2226
2275
|
h(ProviderPacksPanel, { t }),
|
package/lib/client.panels.js
CHANGED
|
@@ -182,6 +182,95 @@ 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 usageRef = React.useRef({ inflight: {}, gen: {} })
|
|
195
|
+
const pull = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
196
|
+
.then((res) => res.json())
|
|
197
|
+
.then((cfg) => {
|
|
198
|
+
setData(cfg)
|
|
199
|
+
setSpeed((cfg.config && cfg.config.codexSubagentSpeed) || 'standard')
|
|
200
|
+
if (!provider && cfg.accounts && cfg.accounts.length) setProvider(cfg.accounts[0].provider)
|
|
201
|
+
})
|
|
202
|
+
.catch(() => {})
|
|
203
|
+
React.useEffect(() => { pull() }, [])
|
|
204
|
+
const loadUsage = (row) => {
|
|
205
|
+
const key = row.provider + ':' + row.index
|
|
206
|
+
if (usageRef.current.inflight[key]) return
|
|
207
|
+
const gen = (usageRef.current.gen[key] || 0) + 1
|
|
208
|
+
usageRef.current.gen[key] = gen
|
|
209
|
+
usageRef.current.inflight[key] = true
|
|
210
|
+
setUsage((m) => Object.assign({}, m, { [key]: { loading: true } }))
|
|
211
|
+
fetch('/dsh-subscriptions/usage?provider=' + encodeURIComponent(row.provider) + '&index=' + encodeURIComponent(row.index), { cache: 'no-store' })
|
|
212
|
+
.then((res) => res.json())
|
|
213
|
+
.then((out) => {
|
|
214
|
+
if (usageRef.current.gen[key] !== gen) return
|
|
215
|
+
setUsage((m) => Object.assign({}, m, { [key]: { value: out, loading: false } }))
|
|
216
|
+
})
|
|
217
|
+
.catch((e) => {
|
|
218
|
+
if (usageRef.current.gen[key] === gen) setUsage((m) => Object.assign({}, m, { [key]: { loading: false, error: String(e && e.message || e) } }))
|
|
219
|
+
})
|
|
220
|
+
.finally(() => { usageRef.current.inflight[key] = false })
|
|
221
|
+
}
|
|
222
|
+
const saveSpeed = (value) => {
|
|
223
|
+
setSpeed(value)
|
|
224
|
+
if (!data || !data.config) return
|
|
225
|
+
fetch('/dsh-subscriptions/config', {
|
|
226
|
+
method: 'PUT',
|
|
227
|
+
headers: { 'content-type': 'application/json' },
|
|
228
|
+
body: JSON.stringify({ config: Object.assign({}, data.config, { codexSubagentSpeed: value }), revision: data.revision }),
|
|
229
|
+
}).then(pull).catch(() => {})
|
|
230
|
+
}
|
|
231
|
+
const accounts = (data && data.accounts) || []
|
|
232
|
+
const selected = provider || (accounts[0] && accounts[0].provider) || ''
|
|
233
|
+
const rows = accounts.filter((row) => row.provider === selected).sort((a, b) => {
|
|
234
|
+
if (sort === 'name') return String(a.label || a.email || a.ref).localeCompare(String(b.label || b.email || b.ref))
|
|
235
|
+
if (sort === 'quota') return (b.usagePercent || 0) - (a.usagePercent || 0)
|
|
236
|
+
return (b.usagePercent || 0) + (b.errors || 0) * 5 - ((a.usagePercent || 0) + (a.errors || 0) * 5)
|
|
237
|
+
})
|
|
238
|
+
const models = ((data && data.modelsCatalog) || []).filter((m) => m.provider === selected)
|
|
239
|
+
const q = query.trim().toLowerCase()
|
|
240
|
+
const shown = models.filter((m) => !q || [m.id, m.name, m.alias].some((v) => String(v || '').toLowerCase().includes(q)))
|
|
241
|
+
const providers = Array.from(new Set(accounts.map((row) => row.provider).filter(Boolean)))
|
|
242
|
+
return h('div', { className: 'dsub-section-card' },
|
|
243
|
+
h('div', { className: 'dsub-section-title' }, t('accountsDashboard'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
244
|
+
h('div', { className: 'dsub-row' },
|
|
245
|
+
h('select', { className: 'dsub-select', value: selected, onChange: (e) => setProvider(e.target.value) },
|
|
246
|
+
providers.map((id) => h('option', { key: id, value: id }, id))),
|
|
247
|
+
h('select', { className: 'dsub-select', value: sort, onChange: (e) => setSort(e.target.value) },
|
|
248
|
+
['risk', 'quota', 'name'].map((id) => h('option', { key: id, value: id }, t('sort' + id[0].toUpperCase() + id.slice(1))))),
|
|
249
|
+
h('span', { className: 'dsub-sub' }, t('speedSubagent') + ':'),
|
|
250
|
+
h('select', { className: 'dsub-select', value: speed, onChange: (e) => saveSpeed(e.target.value) },
|
|
251
|
+
h('option', { value: 'standard' }, 'Standard'), h('option', { value: 'fast' }, 'Fast')),
|
|
252
|
+
),
|
|
253
|
+
rows.map((row, i) => {
|
|
254
|
+
const key = row.provider + ':' + row.index
|
|
255
|
+
const u = usage[key]
|
|
256
|
+
return h('div', { key: key + ':' + i, className: 'dsub-group', style: { padding: '8px' } },
|
|
257
|
+
h('div', { className: 'dsub-row' },
|
|
258
|
+
h('span', { className: 'dsub-h' }, '#' + row.index + ' ' + (row.label || row.email || row.ref)),
|
|
259
|
+
h('span', { className: 'dsub-pillTag' }, row.quarantineUntil > Date.now() ? t('statusQuarantined') : (row.cooldownUntil > Date.now() ? t('statusCooling') : t('statusHealthy')))),
|
|
260
|
+
h('div', { className: 'dsub-sub' }, t('quotaUsed') + ': ' + (row.usagePercent == null ? '—' : row.usagePercent + '%') + ' · ' + t('requests') + ' ' + (row.requests || 0) + ' · ' + t('errors') + ' ' + (row.errors || 0)),
|
|
261
|
+
h('div', { className: 'dsub-row' },
|
|
262
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => loadUsage(row) }, u && u.loading ? '…' : t('loadUsage')),
|
|
263
|
+
u && u.error ? h('span', { className: 'dsub-bad' }, u.error) : null,
|
|
264
|
+
u && u.value ? h('span', { className: 'dsub-dim' }, JSON.stringify(u.value).slice(0, 180)) : null),
|
|
265
|
+
)
|
|
266
|
+
}),
|
|
267
|
+
h('div', { className: 'dsub-h' }, t('modelEditor') + ' · ' + t('discovered') + ' ' + models.length + ' · ' + t('shown') + ' ' + shown.length),
|
|
268
|
+
h('input', { className: 'dsub-grow', value: query, placeholder: t('searchModels'), onChange: (e) => setQuery(e.target.value) }),
|
|
269
|
+
shown.slice(0, 40).map((m) => h('div', { key: m.id, className: 'dsub-row' },
|
|
270
|
+
h('span', { className: 'dsub-h' }, m.independent ? '★ ' : ''), h('span', { className: 'dsub-sub' }, m.name || m.id), h('span', { className: 'dsub-dim' }, m.id))),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel, AccountsDashboardPanel }
|
|
186
275
|
}
|
|
187
276
|
})
|
package/lib/config-schema.js
CHANGED
|
@@ -22,6 +22,10 @@ export const Slot = z.object({
|
|
|
22
22
|
.description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
|
|
23
23
|
proxyBypass: z.array(z.string()).default([])
|
|
24
24
|
.description('#803 Hosts/domains that bypass the per-account proxy.'),
|
|
25
|
+
independentEntry: z.boolean().default(false)
|
|
26
|
+
.description('#796 Expose this account as separate picker model rows with no fallback.'),
|
|
27
|
+
independentAlias: z.string().default('')
|
|
28
|
+
.description('#796 Display alias for independent model rows. Binding stays by credential ref.'),
|
|
25
29
|
maxConcurrency: z.number().default(1)
|
|
26
30
|
.description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
|
|
27
31
|
monthlyCost: z.number().default(20)
|
|
@@ -101,6 +105,8 @@ export const Config = z.object({
|
|
|
101
105
|
.description('#93 Response verbosity for Codex reasoning models: low, medium or high. Empty = protocol default.'),
|
|
102
106
|
codexFastMode: z.boolean().volatile().default(false)
|
|
103
107
|
.description('#92 Fast Mode for Codex: sends service_tier priority (1.5x speed billing tier) with every request.'),
|
|
108
|
+
codexSubagentSpeed: z.string().volatile().default('standard')
|
|
109
|
+
.description('#802 Default speed for subagents: standard or fast. Explicit child choice wins.'),
|
|
104
110
|
composerQuota: z.string().volatile().default('off')
|
|
105
111
|
.description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
|
|
106
112
|
composerQuotaDisplay: z.string().volatile().default('current')
|
package/lib/models-catalog.js
CHANGED
|
@@ -5,6 +5,7 @@ export function isDeprecatedId(id) {
|
|
|
5
5
|
}
|
|
6
6
|
import { getVendor } from './vendors/index.js'
|
|
7
7
|
import { getAccountRemainingPercent } from './rotate.js'
|
|
8
|
+
import { buildIndependentEntries } from './account-features.js'
|
|
8
9
|
|
|
9
10
|
export const MODEL_CATALOG_TTL_MS = 5 * 60 * 1000
|
|
10
11
|
|
|
@@ -303,6 +304,22 @@ export function buildEnrichedCatalog({
|
|
|
303
304
|
pool,
|
|
304
305
|
})
|
|
305
306
|
}
|
|
307
|
+
|
|
308
|
+
const independent = buildIndependentEntries(
|
|
309
|
+
accounts.filter((account) => account && account.provider === provId),
|
|
310
|
+
{ [provId]: rawList },
|
|
311
|
+
)
|
|
312
|
+
for (const entry of independent) {
|
|
313
|
+
result.push({
|
|
314
|
+
id: entry.id,
|
|
315
|
+
name: entry.alias + ' · ' + entry.model,
|
|
316
|
+
provider: provId,
|
|
317
|
+
providerName: displayName(provId),
|
|
318
|
+
accountRef: entry.accountRef,
|
|
319
|
+
independent: true,
|
|
320
|
+
pool,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
306
323
|
}
|
|
307
324
|
|
|
308
325
|
return result
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.45",
|
|
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",
|