@goodandready/dsh-subscriptions 0.7.24 → 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.
@@ -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
@@ -186,6 +186,16 @@ window.__ModuleLoader__.load({
186
186
  "toastCooldownRecovered": "is now available (cooldown expired)",
187
187
  "modelsCatalogTitle": "Curated Subscription Models",
188
188
  "modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
189
+ "modelSearchPlaceholder": "Search models by id, name or alias",
190
+ "modelSortLabel": "Sort models",
191
+ "modelSortName": "Name",
192
+ "modelSortProvider": "Provider",
193
+ "modelSortContext": "Context window",
194
+ "modelSortAvailability": "Availability",
195
+ "modelListCounts": "{shown} of {matched} shown · {hidden} hidden · {total} total",
196
+ "modelShowMore": "Show more",
197
+ "modelShowAll": "Show all",
198
+ "modelNoMatches": "No models match this search.",
189
199
  "rotationDecisionsTitle": "Rotation & Routing Decisions",
190
200
  "rotationDecisionsDesc": "Real-time decision trace explaining account selection, skips, and fallback reasons.",
191
201
  "rotationAttempt": "attempt",
@@ -274,6 +284,15 @@ window.__ModuleLoader__.load({
274
284
  "vipOnlyHint": "Reserve this account for VIP-tagged requests.",
275
285
  "isPro": "Pro",
276
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.",
277
296
  "tags": "Tags",
278
297
  "tagsPlaceholder": "team, prod",
279
298
  "weight": "Weight",
@@ -550,6 +569,16 @@ window.__ModuleLoader__.load({
550
569
  "toastCooldownRecovered": "已解除冷却恢复可用",
551
570
  "modelsCatalogTitle": "精选订阅模型",
552
571
  "modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
572
+ "modelSearchPlaceholder": "搜索模型 ID、名称或别名",
573
+ "modelSortLabel": "模型排序",
574
+ "modelSortName": "名称",
575
+ "modelSortProvider": "服务商",
576
+ "modelSortContext": "上下文窗口",
577
+ "modelSortAvailability": "可用性",
578
+ "modelListCounts": "已显示 {shown} / {matched} · 隐藏 {hidden} · 共 {total} 个",
579
+ "modelShowMore": "显示更多",
580
+ "modelShowAll": "显示全部",
581
+ "modelNoMatches": "没有匹配的模型。",
553
582
  "rotationDecisionsTitle": "轮询调度与路由决策",
554
583
  "rotationDecisionsDesc": "实时决策追踪,透明展示每次请求的账号选择、跳过与回退原因。",
555
584
  "rotationAttempt": "次尝试",
@@ -638,6 +667,15 @@ window.__ModuleLoader__.load({
638
667
  "vipOnlyHint": "将此账户保留给 VIP 请求。",
639
668
  "isPro": "Pro",
640
669
  "isProHint": "按 Pro 账户参与加权优先级。",
670
+ "poolEnabled": "参与自动池",
671
+ "poolEnabledHint": "关闭后该账户不会被自动选择,仅保留给显式使用。",
672
+ "poolModelsTitle": "池模型",
673
+ "poolModelsAutomatic": "自动",
674
+ "poolModelsAll": "全选",
675
+ "poolModelsNone": "无",
676
+ "poolModelsSelected": "已选",
677
+ "poolModelsHint": "自动允许当前及未来全部模型。无会将该账户移出模型调度。显式列表只允许勾选的模型 ID。",
678
+ "poolModelsNoCatalog": "暂无模型目录;请同步模型或使用自动。",
641
679
  "tags": "标签",
642
680
  "tagsPlaceholder": "team, prod",
643
681
  "weight": "权重",
@@ -948,7 +986,52 @@ window.__ModuleLoader__.load({
948
986
  }
949
987
  const Chevron = ChevronIcon || FallbackChevron
950
988
 
989
+ // #839 model-list helpers
990
+ function modelListSearchText(model) {
991
+ const parts = [model && model.id, model && model.name, model && model.alias, model && model.provider, model && model.providerName]
992
+ if (model && Array.isArray(model.aliases)) parts.push.apply(parts, model.aliases)
993
+ return parts.filter(Boolean).join(' ').toLowerCase()
994
+ }
951
995
 
996
+ function filterModelList(models, query) {
997
+ const list = Array.isArray(models) ? models : []
998
+ const q = String(query || '').trim().toLowerCase()
999
+ if (!q) return list.slice()
1000
+ return list.filter((model) => modelListSearchText(model).includes(q))
1001
+ }
1002
+
1003
+ function modelAvailabilityRank(model) {
1004
+ const status = String(model && model.pool && model.pool.status || 'unconfigured').toLowerCase()
1005
+ const order = { available: 0, degraded: 1, cooldown: 2, quarantined: 3, exhausted: 4, unconfigured: 5 }
1006
+ return order[status] != null ? order[status] : 6
1007
+ }
1008
+
1009
+ function sortModelList(models, sortKey) {
1010
+ const list = Array.isArray(models) ? models.slice() : []
1011
+ const key = String(sortKey || 'name')
1012
+ const byName = (a, b) => String(a && (a.name || a.id) || '').localeCompare(String(b && (b.name || b.id) || ''), undefined, { numeric: true, sensitivity: 'base' })
1013
+ list.sort((a, b) => {
1014
+ if (key === 'provider') {
1015
+ const byProvider = String(a && (a.providerName || a.provider) || '').localeCompare(String(b && (b.providerName || b.provider) || ''), undefined, { sensitivity: 'base' })
1016
+ if (byProvider) return byProvider
1017
+ } else if (key === 'context') {
1018
+ const delta = Number(b && b.contextWindow || 0) - Number(a && a.contextWindow || 0)
1019
+ if (delta) return delta
1020
+ } else if (key === 'availability') {
1021
+ const delta = modelAvailabilityRank(a) - modelAvailabilityRank(b)
1022
+ if (delta) return delta
1023
+ }
1024
+ return byName(a, b)
1025
+ })
1026
+ return list
1027
+ }
1028
+
1029
+ function modelListCounts(all, shown) {
1030
+ const total = Array.isArray(all) ? all.length : 0
1031
+ const visible = Array.isArray(shown) ? shown.length : 0
1032
+ return { total, shown: visible, hidden: Math.max(0, total - visible) }
1033
+ }
1034
+ // #839 end model-list helpers
952
1035
 
953
1036
  function SubsSection(props) {
954
1037
  // The translator comes from the slot because its registration declares locale.
@@ -961,6 +1044,9 @@ window.__ModuleLoader__.load({
961
1044
  const [accounts, setAccounts] = React.useState([])
962
1045
  const [providers, setProviders] = React.useState([])
963
1046
  const [modelsCatalog, setModelsCatalog] = React.useState([])
1047
+ const [modelQuery, setModelQuery] = React.useState('')
1048
+ const [modelSort, setModelSort] = React.useState('name')
1049
+ const [modelLimit, setModelLimit] = React.useState(40)
964
1050
  const [paste, setPaste] = React.useState({})
965
1051
  const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
966
1052
  const [loginLinks, setLoginLinks] = React.useState({}) // #595 provider:index -> fallback auth URL when a popup is blocked
@@ -1676,6 +1762,20 @@ window.__ModuleLoader__.load({
1676
1762
 
1677
1763
  const connectedCount = accounts.filter((a) => a.configured).length
1678
1764
 
1765
+ const catalogAll = Array.isArray(modelsCatalog) && modelsCatalog.length ? modelsCatalog : [
1766
+ { id: 'gpt-4o', contextLabel: '128k', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
1767
+ { id: 'o1 / o3-mini', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
1768
+ { id: 'claude-3-7-sonnet', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
1769
+ { id: 'claude-3-5-sonnet', contextLabel: '200k', capabilities: ['coding'], pool: { status: t('sourceAvailable') } },
1770
+ { id: 'grok-beta', contextLabel: '128k', capabilities: ['fast'], pool: { status: t('sourceAvailable') } },
1771
+ { id: 'gemini-2.5-pro', contextLabel: '1M', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
1772
+ { id: 'kimi-k1.5', contextLabel: '128k', capabilities: ['math'], pool: { status: t('sourceAvailable') } },
1773
+ { id: 'glm-4-plus', contextLabel: '128k', capabilities: ['chinese'], pool: { status: t('sourceAvailable') } },
1774
+ ]
1775
+ const catalogSorted = sortModelList(filterModelList(catalogAll, modelQuery), modelSort)
1776
+ const catalogShown = catalogSorted.slice(0, modelLimit)
1777
+ const catalogCounts = modelListCounts(catalogAll, catalogShown)
1778
+
1679
1779
  return h('div', { className: 'dsub-wrap', 'data-tab': tab },
1680
1780
  h('div', { className: 'dsub-header-bar' },
1681
1781
  h('div', { className: 'dsub-row', style: { marginTop: 0 } },
@@ -2197,17 +2297,36 @@ h('div', { className: 'dsub-row' },
2197
2297
  h('details', { className: 'dsub-group', 'data-pane': 'models' },
2198
2298
  h('summary', { className: 'dsub-groupHead' }, '🎯 ' + t('modelsCatalogTitle')),
2199
2299
  h('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('modelsCatalogDesc')),
2300
+ h('div', { className: 'dsub-row', style: { marginTop: 8, gap: 6, alignItems: 'center' } },
2301
+ h('input', {
2302
+ className: 'dsub-inp dsub-grow',
2303
+ value: modelQuery,
2304
+ placeholder: t('modelSearchPlaceholder'),
2305
+ 'aria-label': t('modelSearchPlaceholder'),
2306
+ onChange: (e) => { setModelQuery(e.target.value); setModelLimit(40) },
2307
+ }),
2308
+ h('select', {
2309
+ className: 'dsub-select',
2310
+ value: modelSort,
2311
+ title: t('modelSortLabel'),
2312
+ 'aria-label': t('modelSortLabel'),
2313
+ onChange: (e) => { setModelSort(e.target.value); setModelLimit(40) },
2314
+ },
2315
+ h('option', { value: 'name' }, t('modelSortName')),
2316
+ h('option', { value: 'provider' }, t('modelSortProvider')),
2317
+ h('option', { value: 'context' }, t('modelSortContext')),
2318
+ h('option', { value: 'availability' }, t('modelSortAvailability')),
2319
+ ),
2320
+ ),
2321
+ h('div', { className: 'dsub-sub', style: { marginTop: 4 } },
2322
+ t('modelListCounts')
2323
+ .replace('{shown}', String(catalogCounts.shown))
2324
+ .replace('{matched}', String(catalogSorted.length))
2325
+ .replace('{hidden}', String(catalogCounts.hidden))
2326
+ .replace('{total}', String(catalogCounts.total))
2327
+ ),
2200
2328
  h('div', { className: 'dsub-model-grid' },
2201
- (Array.isArray(modelsCatalog) && modelsCatalog.length ? modelsCatalog : [
2202
- { id: 'gpt-4o', contextLabel: '128k', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
2203
- { id: 'o1 / o3-mini', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
2204
- { id: 'claude-3-7-sonnet', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: t('sourceAvailable') } },
2205
- { id: 'claude-3-5-sonnet', contextLabel: '200k', capabilities: ['coding'], pool: { status: t('sourceAvailable') } },
2206
- { id: 'grok-beta', contextLabel: '128k', capabilities: ['fast'], pool: { status: t('sourceAvailable') } },
2207
- { id: 'gemini-2.5-pro', contextLabel: '1M', capabilities: ['vision'], pool: { status: t('sourceAvailable') } },
2208
- { id: 'kimi-k1.5', contextLabel: '128k', capabilities: ['math'], pool: { status: t('sourceAvailable') } },
2209
- { id: 'glm-4-plus', contextLabel: '128k', capabilities: ['chinese'], pool: { status: t('sourceAvailable') } },
2210
- ]).map((m, idx) => {
2329
+ catalogShown.map((m, idx) => {
2211
2330
  const poolStatus = m.pool ? m.pool.status : 'unconfigured'
2212
2331
  const statusClass = 'dsub-model-status-' + poolStatus
2213
2332
  let poolLabel = ''
@@ -2244,6 +2363,17 @@ h('div', { className: 'dsub-row' },
2244
2363
  )
2245
2364
  })
2246
2365
  ),
2366
+ catalogSorted.length === 0 ? h('div', { className: 'dsub-sub', style: { marginTop: 6 } }, t('modelNoMatches')) : null,
2367
+ catalogCounts.shown < catalogSorted.length ? h('div', { className: 'dsub-row', style: { marginTop: 6, gap: 6 } },
2368
+ h('button', {
2369
+ type: 'button', className: 'dsub-mini',
2370
+ onClick: () => setModelLimit((n) => n + 40),
2371
+ }, t('modelShowMore')),
2372
+ h('button', {
2373
+ type: 'button', className: 'dsub-mini',
2374
+ onClick: () => setModelLimit(catalogSorted.length),
2375
+ }, t('modelShowAll')),
2376
+ ) : null,
2247
2377
  ),
2248
2378
  h('details', { className: 'dsub-group', 'data-pane': 'models' },
2249
2379
  h('summary', { className: 'dsub-groupHead' }, '🧠 ' + t('reasoningEffortTitle')),
@@ -2612,6 +2742,17 @@ h('div', { className: 'dsub-row' },
2612
2742
  const account = accountOf(row.slot.provider, row.slot.index)
2613
2743
  const key = row.slot.provider + ':' + row.slot.index
2614
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()
2615
2756
  const avatarSrc = safeExternalUrl(account.avatarUrl, true)
2616
2757
  const avatarEl = (avatarSrc ? h('img', {
2617
2758
  src: avatarSrc,
@@ -2911,6 +3052,58 @@ h('div', { className: 'dsub-row' },
2911
3052
  }),
2912
3053
  ' ' + t('isPro')
2913
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')),
2914
3107
  ),
2915
3108
  h('div', { className: 'dsub-row' },
2916
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.24",
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",