@goodandready/dsh-subscriptions 0.7.25 → 0.7.27

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.
@@ -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')),
@@ -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
- if (pinned && pinned.hasToken && !isQuarantined(pinned, now)) {
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))
@@ -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,
@@ -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.25",
3
+ "version": "0.7.27",
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",