@goodandready/dsh-subscriptions 0.7.25 → 0.7.26
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/accounts-view.js +2 -0
- package/lib/accounts.js +6 -0
- package/lib/client.js +81 -0
- package/lib/config-schema.js +4 -0
- package/lib/index.js +2 -2
- package/lib/rotate.js +31 -1
- package/lib/stream-rotate.js +1 -0
- package/lib/subscriptions.js +1 -0
- package/package.json +1 -1
package/lib/accounts-view.js
CHANGED
|
@@ -165,6 +165,8 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
165
165
|
latencyGrade: info.latencyGrade ?? null,
|
|
166
166
|
lastDoctorVerdict: info.lastDoctorVerdict || null,
|
|
167
167
|
availableModels: info.availableModels || null,
|
|
168
|
+
poolEnabled: slot.poolEnabled !== false,
|
|
169
|
+
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
168
170
|
capabilities,
|
|
169
171
|
billing,
|
|
170
172
|
renewsAt: billing.renewsAt,
|
package/lib/accounts.js
CHANGED
|
@@ -73,6 +73,10 @@ export function normalizeSlots(slots) {
|
|
|
73
73
|
...(slot.proxyUrl ? { proxyUrl: String(slot.proxyUrl) } : {}),
|
|
74
74
|
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
75
75
|
...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
|
|
76
|
+
...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
|
|
77
|
+
...(Array.isArray(slot.poolModels)
|
|
78
|
+
? { poolModels: [...new Set(slot.poolModels.map((id) => String(id).trim()).filter(Boolean))] }
|
|
79
|
+
: {}),
|
|
76
80
|
})
|
|
77
81
|
}
|
|
78
82
|
return out
|
|
@@ -384,6 +388,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
384
388
|
tags: Array.isArray(slot.tags) ? slot.tags : [],
|
|
385
389
|
weight: Number(slot.weight) || 0,
|
|
386
390
|
isPro: !!slot.isPro,
|
|
391
|
+
poolEnabled: slot.poolEnabled !== false,
|
|
392
|
+
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
387
393
|
})
|
|
388
394
|
}
|
|
389
395
|
return out
|
package/lib/client.js
CHANGED
|
@@ -284,6 +284,15 @@ window.__ModuleLoader__.load({
|
|
|
284
284
|
"vipOnlyHint": "Reserve this account for VIP-tagged requests.",
|
|
285
285
|
"isPro": "Pro",
|
|
286
286
|
"isProHint": "Treat this account as Pro for weighted priority.",
|
|
287
|
+
"poolEnabled": "Use in automatic pool",
|
|
288
|
+
"poolEnabledHint": "When off, this account is not selected automatically; it stays available for explicit use only.",
|
|
289
|
+
"poolModelsTitle": "Pool models",
|
|
290
|
+
"poolModelsAutomatic": "Automatic",
|
|
291
|
+
"poolModelsAll": "Select all",
|
|
292
|
+
"poolModelsNone": "None",
|
|
293
|
+
"poolModelsSelected": "selected",
|
|
294
|
+
"poolModelsHint": "Automatic allows every current and future model. None removes this account from model routing. An explicit list permits only checked model ids.",
|
|
295
|
+
"poolModelsNoCatalog": "No model catalog yet; sync models or use Automatic.",
|
|
287
296
|
"tags": "Tags",
|
|
288
297
|
"tagsPlaceholder": "team, prod",
|
|
289
298
|
"weight": "Weight",
|
|
@@ -658,6 +667,15 @@ window.__ModuleLoader__.load({
|
|
|
658
667
|
"vipOnlyHint": "将此账户保留给 VIP 请求。",
|
|
659
668
|
"isPro": "Pro",
|
|
660
669
|
"isProHint": "按 Pro 账户参与加权优先级。",
|
|
670
|
+
"poolEnabled": "参与自动池",
|
|
671
|
+
"poolEnabledHint": "关闭后该账户不会被自动选择,仅保留给显式使用。",
|
|
672
|
+
"poolModelsTitle": "池模型",
|
|
673
|
+
"poolModelsAutomatic": "自动",
|
|
674
|
+
"poolModelsAll": "全选",
|
|
675
|
+
"poolModelsNone": "无",
|
|
676
|
+
"poolModelsSelected": "已选",
|
|
677
|
+
"poolModelsHint": "自动允许当前及未来全部模型。无会将该账户移出模型调度。显式列表只允许勾选的模型 ID。",
|
|
678
|
+
"poolModelsNoCatalog": "暂无模型目录;请同步模型或使用自动。",
|
|
661
679
|
"tags": "标签",
|
|
662
680
|
"tagsPlaceholder": "team, prod",
|
|
663
681
|
"weight": "权重",
|
|
@@ -2724,6 +2742,17 @@ h('div', { className: 'dsub-row' },
|
|
|
2724
2742
|
const account = accountOf(row.slot.provider, row.slot.index)
|
|
2725
2743
|
const key = row.slot.provider + ':' + row.slot.index
|
|
2726
2744
|
const on = !!account.configured
|
|
2745
|
+
const poolEnabled = row.slot.poolEnabled !== false
|
|
2746
|
+
const poolModels = Array.isArray(row.slot.poolModels) ? row.slot.poolModels : null
|
|
2747
|
+
const modelIdOf = (m) => (typeof m === 'string' ? m : (m && (m.id || m.slug || m.name)))
|
|
2748
|
+
const poolModelIds = Array.from(new Set(
|
|
2749
|
+
(Array.isArray(modelsCatalog) ? modelsCatalog : [])
|
|
2750
|
+
.filter((m) => m && m.provider === row.slot.provider)
|
|
2751
|
+
.map((m) => modelIdOf(m))
|
|
2752
|
+
.concat(Array.isArray(account.availableModels) ? account.availableModels.map(modelIdOf) : [])
|
|
2753
|
+
.concat(poolModels || [])
|
|
2754
|
+
.filter(Boolean)
|
|
2755
|
+
)).sort()
|
|
2727
2756
|
const avatarSrc = safeExternalUrl(account.avatarUrl, true)
|
|
2728
2757
|
const avatarEl = (avatarSrc ? h('img', {
|
|
2729
2758
|
src: avatarSrc,
|
|
@@ -3023,6 +3052,58 @@ h('div', { className: 'dsub-row' },
|
|
|
3023
3052
|
}),
|
|
3024
3053
|
' ' + t('isPro')
|
|
3025
3054
|
),
|
|
3055
|
+
h('label', { className: 'dsub-ref-tag', title: t('poolEnabledHint') },
|
|
3056
|
+
h('input', {
|
|
3057
|
+
type: 'checkbox',
|
|
3058
|
+
checked: poolEnabled,
|
|
3059
|
+
onChange: (e) => updateSlot(row.slot.provider, row.slot.index, { poolEnabled: e.target.checked }),
|
|
3060
|
+
}),
|
|
3061
|
+
' ' + t('poolEnabled')
|
|
3062
|
+
),
|
|
3063
|
+
),
|
|
3064
|
+
h('details', { className: 'dsub-details' },
|
|
3065
|
+
h('summary', { className: 'dsub-summary' },
|
|
3066
|
+
'🎯 ' + t('poolModelsTitle') + ' · ' + (poolModels ? (poolModels.length + ' ' + t('poolModelsSelected')) : t('poolModelsAutomatic'))
|
|
3067
|
+
),
|
|
3068
|
+
h('div', { className: 'dsub-sub' }, t('poolModelsHint')),
|
|
3069
|
+
h('div', { className: 'dsub-row' },
|
|
3070
|
+
h('button', {
|
|
3071
|
+
type: 'button', className: 'dsub-mini' + (!poolModels ? ' dsub-btnActive' : ''),
|
|
3072
|
+
onClick: () => updateSlot(row.slot.provider, row.slot.index, { poolModels: undefined }),
|
|
3073
|
+
}, t('poolModelsAutomatic')),
|
|
3074
|
+
h('button', {
|
|
3075
|
+
type: 'button', className: 'dsub-mini',
|
|
3076
|
+
onClick: () => updateSlot(row.slot.provider, row.slot.index, { poolModels: poolModelIds.slice() }),
|
|
3077
|
+
}, t('poolModelsAll')),
|
|
3078
|
+
h('button', {
|
|
3079
|
+
type: 'button', className: 'dsub-mini' + (poolModels && poolModels.length === 0 ? ' dsub-btnActive' : ''),
|
|
3080
|
+
onClick: () => updateSlot(row.slot.provider, row.slot.index, { poolModels: [] }),
|
|
3081
|
+
}, t('poolModelsNone')),
|
|
3082
|
+
),
|
|
3083
|
+
poolModelIds.length ? h('div', { className: 'dsub-row', style: { flexWrap: 'wrap', gap: 6 } },
|
|
3084
|
+
poolModelIds.map((modelId) => {
|
|
3085
|
+
const checked = poolModels ? poolModels.includes(modelId) : true
|
|
3086
|
+
return h('label', { key: modelId, className: 'dsub-ref-tag', title: modelId },
|
|
3087
|
+
h('input', {
|
|
3088
|
+
type: 'checkbox',
|
|
3089
|
+
checked,
|
|
3090
|
+
onChange: () => {
|
|
3091
|
+
let next
|
|
3092
|
+
if (poolModels) {
|
|
3093
|
+
next = checked
|
|
3094
|
+
? poolModels.filter((id) => id !== modelId)
|
|
3095
|
+
: poolModels.concat([modelId])
|
|
3096
|
+
} else {
|
|
3097
|
+
next = poolModelIds.filter((id) => id !== modelId)
|
|
3098
|
+
}
|
|
3099
|
+
updateSlot(row.slot.provider, row.slot.index, { poolModels: next })
|
|
3100
|
+
},
|
|
3101
|
+
}),
|
|
3102
|
+
' ',
|
|
3103
|
+
h('code', null, modelId)
|
|
3104
|
+
)
|
|
3105
|
+
})
|
|
3106
|
+
) : h('div', { className: 'dsub-sub' }, t('poolModelsNoCatalog')),
|
|
3026
3107
|
),
|
|
3027
3108
|
h('div', { className: 'dsub-row' },
|
|
3028
3109
|
h('span', { className: 'dsub-ref-tag' }, t('tags')),
|
package/lib/config-schema.js
CHANGED
|
@@ -34,6 +34,10 @@ export const Slot = z.object({
|
|
|
34
34
|
.description('#171 Weighted rotation priority. 0 = derive from isPro.'),
|
|
35
35
|
isPro: z.boolean().default(false)
|
|
36
36
|
.description('#171 Treat this account as Pro for weighted rotation.'),
|
|
37
|
+
poolEnabled: z.boolean().default(true)
|
|
38
|
+
.description('#795 Include this account in automatic pool selection. false keeps it available for explicit/manual use only.'),
|
|
39
|
+
poolModels: z.array(z.string()).default(undefined)
|
|
40
|
+
.description('#795 Model allowlist for automatic pool selection. Absent = every model, empty = none, explicit list = only those ids.'),
|
|
37
41
|
})
|
|
38
42
|
|
|
39
43
|
export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
|
package/lib/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { createDiagnosticsReport } from './diagnostics.js'
|
|
|
27
27
|
import { createAccountsView } from './accounts-view.js'
|
|
28
28
|
import { inspectScopes } from './scope-inspector.js'
|
|
29
29
|
import { createAdapterManager } from './adapter-manager.js'
|
|
30
|
-
import { pickAccount, isAuthError } from './rotate.js'
|
|
30
|
+
import { pickAccount, poolBlockReason, isAuthError } from './rotate.js'
|
|
31
31
|
import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret, setWebhookAllowPrivate, setWebhookFormat } from './alerts.js'
|
|
32
32
|
import { isDigestDue } from './notification-scheduler.js'
|
|
33
33
|
import { clearSessionPins } from './session-pin.js'
|
|
@@ -394,7 +394,7 @@ export function apply(ctx, rawConfig) {
|
|
|
394
394
|
drainBeforeReset: live().drainBeforeReset,
|
|
395
395
|
drainSafetyPercent: live().drainSafetyPercent,
|
|
396
396
|
})
|
|
397
|
-
const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken)
|
|
397
|
+
const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken && !poolBlockReason(row, {}))
|
|
398
398
|
if (!slot) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
|
|
399
399
|
const raw = await store.resolveRaw(slot.ref)
|
|
400
400
|
if (!raw) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
|
package/lib/rotate.js
CHANGED
|
@@ -79,6 +79,19 @@ export function getAccountRemainingPercent(acc) {
|
|
|
79
79
|
return null
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function poolBlockReason(account, opts) {
|
|
83
|
+
if (!account) return null
|
|
84
|
+
if (account.poolEnabled === false) return 'pool_disabled'
|
|
85
|
+
if (Array.isArray(account.poolModels)) {
|
|
86
|
+
const model = String(opts && opts.model || '').trim().toLowerCase()
|
|
87
|
+
if (model) {
|
|
88
|
+
const allowed = account.poolModels.some((id) => String(id).trim().toLowerCase() === model)
|
|
89
|
+
if (!allowed) return 'model_not_allowed'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
83
96
|
const list = Array.isArray(accounts) ? accounts : []
|
|
84
97
|
const now = Number(nowMs) || 0
|
|
@@ -119,7 +132,16 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
119
132
|
const pinnedRef = getPinnedAccountRef(targetSession, now)
|
|
120
133
|
if (pinnedRef) {
|
|
121
134
|
const pinned = list.find((a) => (a.ref || a.id) === pinnedRef)
|
|
122
|
-
|
|
135
|
+
const pinnedBlock = pinned ? poolBlockReason(pinned, opts) : null
|
|
136
|
+
if (pinned && pinnedBlock) {
|
|
137
|
+
unpinSession(targetSession, pinnedBlock)
|
|
138
|
+
evaluations.push({
|
|
139
|
+
ref: pinnedRef,
|
|
140
|
+
eligible: false,
|
|
141
|
+
reason: pinnedBlock,
|
|
142
|
+
pinned: true,
|
|
143
|
+
})
|
|
144
|
+
} else if (pinned && pinned.hasToken && !isQuarantined(pinned, now)) {
|
|
123
145
|
const isCooldown = pinned.cooldownUntil && Number(pinned.cooldownUntil) > now && cooldownBlocks(pinned, opts && opts.family)
|
|
124
146
|
const isPinnedZero = Boolean(pinned.quota && (
|
|
125
147
|
(pinned.quota.remaining != null && Number(pinned.quota.remaining) <= 0) ||
|
|
@@ -191,6 +213,14 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
191
213
|
continue
|
|
192
214
|
}
|
|
193
215
|
|
|
216
|
+
// #795: pool participation and per-account model allowlists are checked
|
|
217
|
+
// before weighting, quota tiers and session pinning.
|
|
218
|
+
const blockedByPool = poolBlockReason(acc, opts)
|
|
219
|
+
if (blockedByPool) {
|
|
220
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: blockedByPool })
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
194
224
|
if (!opts || opts.autoPacing !== false) {
|
|
195
225
|
if (acc.pacingRisk === undefined) {
|
|
196
226
|
const rem = acc.quota?.remainingPercent ?? (acc.quota?.usedPercent != null ? (100 - Number(acc.quota.usedPercent)) : (acc.usagePercent != null ? (100 - Number(acc.usagePercent)) : null))
|
package/lib/stream-rotate.js
CHANGED
|
@@ -51,6 +51,7 @@ export async function* streamWithRotation({
|
|
|
51
51
|
while (true) {
|
|
52
52
|
const pickRes = pickAccountWithTrace(pool, nowMs(), {
|
|
53
53
|
switchAtRemaining,
|
|
54
|
+
model: options && options.model,
|
|
54
55
|
family: modelFamily(options && options.provider, options && options.model),
|
|
55
56
|
sessionId: options && options.sessionId,
|
|
56
57
|
tag: options && options.tag,
|
package/lib/subscriptions.js
CHANGED
|
@@ -147,6 +147,7 @@ export function createSubscriptionsService(deps) {
|
|
|
147
147
|
while (true) {
|
|
148
148
|
const pickRes = pickAccountWithTrace(pool, Date.now(), {
|
|
149
149
|
switchAtRemaining: thr,
|
|
150
|
+
model: body && body.model,
|
|
150
151
|
family: modelFamily(provider, body && body.model),
|
|
151
152
|
drainBeforeReset: typeof deps.drainBeforeReset === 'function' ? deps.drainBeforeReset() : deps.drainBeforeReset,
|
|
152
153
|
drainSafetyPercent: typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : deps.drainSafetyPercent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.26",
|
|
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",
|