@goodandready/dsh-subscriptions 0.6.52 → 0.6.54

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.
@@ -42,6 +42,8 @@ export function createAdapterManager({
42
42
  vendorConfig: (provider) => vendorConfig(provider, live()),
43
43
  cooldownMs: () => live().cooldownMs,
44
44
  autoPacing: () => live().autoPacing,
45
+ drainBeforeReset: () => live().drainBeforeReset,
46
+ drainSafetyPercent: () => live().drainSafetyPercent,
45
47
  switchAtRemaining: () => live().switchAtRemaining,
46
48
  rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
47
49
  rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
package/lib/adapter.js CHANGED
@@ -121,7 +121,18 @@ export class SubscriptionAdapter extends LlmAdapter {
121
121
  const effectiveAutoPacing = options.autoPacing !== undefined
122
122
  ? options.autoPacing
123
123
  : (typeof deps.autoPacing === 'function' ? deps.autoPacing() : (deps.autoPacing ?? true))
124
- const streamOptions = { ...options, autoPacing: effectiveAutoPacing }
124
+ const effectiveDrainBeforeReset = options.drainBeforeReset !== undefined
125
+ ? options.drainBeforeReset
126
+ : (typeof deps.drainBeforeReset === 'function' ? deps.drainBeforeReset() : (deps.drainBeforeReset ?? false))
127
+ const effectiveDrainSafetyPercent = options.drainSafetyPercent !== undefined
128
+ ? options.drainSafetyPercent
129
+ : (typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : (deps.drainSafetyPercent ?? 5))
130
+ const streamOptions = {
131
+ ...options,
132
+ autoPacing: effectiveAutoPacing,
133
+ drainBeforeReset: effectiveDrainBeforeReset,
134
+ drainSafetyPercent: effectiveDrainSafetyPercent,
135
+ }
125
136
  for await (const event of streamWithRotation({
126
137
  accounts: await deps.listAccounts(provider),
127
138
  nowMs: () => Date.now(),
package/lib/client.js CHANGED
@@ -2362,14 +2362,41 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2362
2362
  return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
2363
2363
  }
2364
2364
 
2365
- const PLUGIN_LABELS = new Set(['plugins', 'плагины', '插件', 'plugin'])
2366
- function matchesPluginLabel(el) {
2365
+ const PLUGIN_LABELS = new Set(['plugins', '插件', 'plugin'])
2366
+
2367
+ function resolvePluginLabels(customCtx) {
2368
+ const labels = new Set(PLUGIN_LABELS)
2369
+ const loc = (customCtx && customCtx.locale) || (typeof window !== 'undefined' && window.__dshCtx && window.__dshCtx.locale)
2370
+ if (loc && typeof loc.bind === 'function') {
2371
+ try {
2372
+ const sp = loc.bind('settings.plugins')
2373
+ const nav = typeof sp === 'function' ? sp('nav') : null
2374
+ if (nav && typeof nav === 'string') labels.add(nav.trim().toLowerCase())
2375
+ } catch { /* bestEffort */ }
2376
+ try {
2377
+ const pm = loc.bind('pluginManager')
2378
+ const panel = typeof pm === 'function' ? (pm('panel') || pm('title')) : null
2379
+ if (panel && typeof panel === 'string') labels.add(panel.trim().toLowerCase())
2380
+ } catch { /* bestEffort */ }
2381
+ }
2382
+ return labels
2383
+ }
2384
+
2385
+ function matchesPluginLabel(el, customLabels) {
2367
2386
  if (!el) return false
2387
+ const dataTab = (el.getAttribute('data-tab') || '').trim().toLowerCase()
2388
+ const dataSection = (el.getAttribute('data-section') || '').trim().toLowerCase()
2389
+ const dataKey = (el.getAttribute('data-key') || '').trim().toLowerCase()
2390
+ const id = (el.getAttribute('id') || '').trim().toLowerCase()
2391
+ if (dataTab === 'plugins' || dataSection === 'plugins' || dataKey === 'plugins' || id === 'plugins') return true
2392
+
2393
+ if (el.querySelector && el.querySelector('path[d*="M10.3232"]')) return true
2394
+
2368
2395
  const txt = (el.textContent || '').trim().toLowerCase()
2369
2396
  const aria = (el.getAttribute('aria-label') || '').trim().toLowerCase()
2370
2397
  const title = (el.getAttribute('title') || '').trim().toLowerCase()
2371
- const dataTab = (el.getAttribute('data-tab') || '').trim().toLowerCase()
2372
- return PLUGIN_LABELS.has(txt) || PLUGIN_LABELS.has(aria) || PLUGIN_LABELS.has(title) || PLUGIN_LABELS.has(dataTab)
2398
+ const labels = customLabels || PLUGIN_LABELS
2399
+ return labels.has(txt) || labels.has(aria) || labels.has(title) || labels.has(dataTab)
2373
2400
  }
2374
2401
 
2375
2402
  function findAndClickCard(doc) {
@@ -2387,12 +2414,15 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2387
2414
  return false
2388
2415
  }
2389
2416
 
2390
- function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms)) {
2417
+ function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms), navCtx) {
2391
2418
  try {
2392
2419
  if (findAndClickCard(doc)) return
2393
2420
 
2421
+ const activeLabels = resolvePluginLabels(navCtx)
2422
+ const checkEl = (el) => matchesPluginLabel(el, activeLabels)
2423
+
2394
2424
  const buttons = [...doc.querySelectorAll('button, a, div[role="button"], div[role="tab"]')]
2395
- const panel = buttons.find(matchesPluginLabel)
2425
+ const panel = buttons.find(checkEl)
2396
2426
  if (panel) {
2397
2427
  panel.click()
2398
2428
  const poll = (left) => {
@@ -2410,7 +2440,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2410
2440
  trigger.click()
2411
2441
  const pollSection = (left) => {
2412
2442
  try {
2413
- const section = [...doc.querySelectorAll('button, a, div[role="tab"], [role="button"]')].find(matchesPluginLabel)
2443
+ const section = [...doc.querySelectorAll('button, a, div[role="tab"], [role="button"]')].find(checkEl)
2414
2444
  if (section && typeof section.click === 'function') {
2415
2445
  section.click()
2416
2446
  const poll = (cLeft) => {
@@ -2420,13 +2450,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2420
2450
  schedule(() => poll(40), 100)
2421
2451
  return
2422
2452
  }
2423
- } catch {}
2453
+ } catch { /* bestEffort */ }
2424
2454
  if (left > 0) schedule(() => pollSection(left - 1), 80)
2425
2455
  }
2426
2456
  schedule(() => pollSection(20), 80)
2427
2457
  }
2428
2458
  }
2429
- } catch {}
2459
+ } catch { /* bestEffort */ }
2430
2460
  }
2431
2461
  function SubsPill(props) {
2432
2462
  const t = (props && props.t) || ((k) => k)
@@ -2645,16 +2675,16 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2645
2675
  if (nav && typeof nav.openBundle === 'function') {
2646
2676
  nav.openBundle(PKG)
2647
2677
  }
2648
- } catch {}
2678
+ } catch { /* bestEffort */ }
2649
2679
 
2650
2680
  try {
2651
2681
  const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
2652
2682
  if (layout && typeof layout.selectPanel === 'function') {
2653
2683
  layout.selectPanel('plugins')
2654
2684
  }
2655
- } catch {}
2685
+ } catch { /* bestEffort */ }
2656
2686
 
2657
- openPluginSettings(document)
2687
+ openPluginSettings(document, undefined, clientCtx)
2658
2688
  },
2659
2689
  }, t('subsOpenSettings')),
2660
2690
  ),
@@ -2758,6 +2788,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2758
2788
 
2759
2789
  exports.inject = ['slots', 'locale', 'sessions']
2760
2790
  exports.apply = function apply(ctx) {
2791
+ if (typeof window !== 'undefined') {
2792
+ window.__dshCtx = ctx
2793
+ }
2761
2794
  setT(ctx.locale.bind(NS))
2762
2795
  try {
2763
2796
  ctx.inject(['pluginNavigation'], (navCtx) => {
@@ -94,6 +94,10 @@ export const Config = z.object({
94
94
  .description('#351 Quota usage percentage thresholds for firing alerts.'),
95
95
  autoPacing: z.boolean().volatile().default(true)
96
96
  .description('#352 Predictive burn-rate load balancing between accounts.'),
97
+ drainBeforeReset: z.boolean().volatile().default(false)
98
+ .description('#485 Prioritize accounts with closest reset window to burn expiring quota first.'),
99
+ drainSafetyPercent: z.number().volatile().default(5)
100
+ .description('#485 Safety margin percent: avoid draining accounts below this threshold when alternatives exist.'),
97
101
  customVendors: z.array(z.any()).volatile().default([])
98
102
  .description('Declarative OpenAI-Responses-compatible providers. See README.'),
99
103
  })
package/lib/forecast.js CHANGED
@@ -125,3 +125,19 @@ export function computePacingRisk(remainingPercent, pacePerHour, resetsAt, now =
125
125
  pacingRisk: hoursUntilExhaustion < 1 ? 50 : 0,
126
126
  }
127
127
  }
128
+
129
+ // #485: Smart Quota Drainer urgency calculation
130
+ export function calculateBurnUrgency(remainingPercent, resetAt, nowMs = Date.now(), safetyFloor = 5) {
131
+ const rem = Number(remainingPercent)
132
+ if (!Number.isFinite(rem) || rem <= 0) return 0
133
+ const floor = Number(safetyFloor) || 0
134
+ if (rem <= floor) return 0
135
+
136
+ const reset = Number(resetAt)
137
+ if (!Number.isFinite(reset) || reset <= nowMs) return 0
138
+
139
+ const hoursUntilReset = (reset - nowMs) / HOUR_MS
140
+ // Denominator clamped to minimum 0.05 hours (3 mins) to avoid infinity
141
+ const clampedHours = Math.max(hoursUntilReset, 0.05)
142
+ return rem / clampedHours
143
+ }
package/lib/http.js CHANGED
@@ -157,13 +157,13 @@ export function isTrustedSettingsRequest(request, targetCtx) {
157
157
  try {
158
158
  const u = new URL(origin)
159
159
  if (u.host.toLowerCase() === host.toLowerCase()) return true
160
- } catch {}
160
+ } catch { /* bestEffort */ }
161
161
  }
162
162
  if (referer && host) {
163
163
  try {
164
164
  const u = new URL(referer)
165
165
  if (u.host.toLowerCase() === host.toLowerCase()) return true
166
- } catch {}
166
+ } catch { /* bestEffort */ }
167
167
  }
168
168
  }
169
169
 
package/lib/index.js CHANGED
@@ -142,7 +142,7 @@ export function apply(ctx, rawConfig) {
142
142
  syncCustomVendors()
143
143
  } catch (err) {
144
144
  cachedConfig = prevConfig
145
- try { syncCustomVendors() } catch {}
145
+ try { syncCustomVendors() } catch { /* bestEffort */ }
146
146
  throw err
147
147
  }
148
148
  return parsed
@@ -314,6 +314,8 @@ export function apply(ctx, rawConfig) {
314
314
  ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
315
315
  vendorConfig: (provider) => vendorConfig(provider, live()),
316
316
  cooldownMs: () => live().cooldownMs,
317
+ drainBeforeReset: () => live().drainBeforeReset,
318
+ drainSafetyPercent: () => live().drainSafetyPercent,
317
319
  switchAtRemaining: () => live().switchAtRemaining,
318
320
  rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
319
321
  rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
@@ -348,7 +350,11 @@ export function apply(ctx, rawConfig) {
348
350
  throw new Error(`unknown subscription provider: ${provider}`)
349
351
  }
350
352
  const accounts = await store.listAccounts(provider)
351
- const eligible = pickAccount(accounts || [], Date.now(), { family: provider === 'codex' ? 'gpt' : 'grok' })
353
+ const eligible = pickAccount(accounts || [], Date.now(), {
354
+ family: provider === 'codex' ? 'gpt' : 'grok',
355
+ drainBeforeReset: live().drainBeforeReset,
356
+ drainSafetyPercent: live().drainSafetyPercent,
357
+ })
352
358
  const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken)
353
359
  if (!slot) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
354
360
  const raw = await store.resolveRaw(slot.ref)
package/lib/rotate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { computePacingRisk, calculateBurnRatePerHour } from "./forecast.js"
1
+ import { computePacingRisk, calculateBurnRatePerHour, calculateBurnUrgency } from "./forecast.js"
2
2
  import { isQuarantined } from './quarantine.js'
3
3
  import { getPinnedAccountRef, pinSession } from './session-pin.js'
4
4
 
@@ -41,6 +41,28 @@ export function cooldownBlocks(acc, family) {
41
41
  return fams.includes(family)
42
42
  }
43
43
 
44
+
45
+ export function getAccountRemainingPercent(acc) {
46
+ if (!acc) return null
47
+ if (acc.quota?.remainingPercent != null) {
48
+ const val = Number(acc.quota.remainingPercent)
49
+ return Number.isFinite(val) ? val : null
50
+ }
51
+ if (acc.quota?.usedPercent != null) {
52
+ const val = 100 - Number(acc.quota.usedPercent)
53
+ return Number.isFinite(val) ? val : null
54
+ }
55
+ if (acc.usagePercent != null) {
56
+ const val = 100 - Number(acc.usagePercent)
57
+ return Number.isFinite(val) ? val : null
58
+ }
59
+ if (acc.quota?.remaining != null && acc.quota?.limit != null && Number(acc.quota.limit) > 0) {
60
+ const val = (Number(acc.quota.remaining) / Number(acc.quota.limit)) * 100
61
+ return Number.isFinite(val) ? val : null
62
+ }
63
+ return null
64
+ }
65
+
44
66
  export function pickAccount(accounts, nowMs, opts) {
45
67
  const list = Array.isArray(accounts) ? accounts : []
46
68
  const now = Number(nowMs) || 0
@@ -111,6 +133,15 @@ export function pickAccount(accounts, nowMs, opts) {
111
133
  }
112
134
  }
113
135
 
136
+ if (opts && opts.drainBeforeReset) {
137
+ if (acc.burnUrgency === undefined) {
138
+ const rem = getAccountRemainingPercent(acc)
139
+ const resetAt = acc.quota?.resetAt || 0
140
+ const safetyFloor = opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5
141
+ acc.burnUrgency = calculateBurnUrgency(rem, resetAt, now, safetyFloor)
142
+ }
143
+ }
144
+
114
145
  // VIP Slot Reservation (#175)
115
146
  if (acc.vipOnly && !isVipRequest) continue
116
147
 
@@ -142,6 +173,15 @@ export function pickAccount(accounts, nowMs, opts) {
142
173
  for (const tier of tiers) {
143
174
  if (tier.length) {
144
175
  tier.sort((a, b) => {
176
+ // #485: Smart Quota Drainer - prioritize higher burn urgency first
177
+ if (opts && opts.drainBeforeReset) {
178
+ const aUrg = Number(a.burnUrgency) || 0
179
+ const bUrg = Number(b.burnUrgency) || 0
180
+ if (Math.abs(aUrg - bUrg) > 0.01) {
181
+ return bUrg - aUrg
182
+ }
183
+ }
184
+
145
185
  // 0. Auto-Pacing: lower pacing risk preferred (#352)
146
186
  const aRisk = Number(a.pacingRisk) || 0
147
187
  const bRisk = Number(b.pacingRisk) || 0
@@ -28,6 +28,8 @@ export async function* streamWithRotation({
28
28
  tag: options && options.tag,
29
29
  vip: options && options.vip,
30
30
  autoPacing: options && (options.autoPacing !== undefined ? options.autoPacing : (options.config && options.config.autoPacing)),
31
+ drainBeforeReset: options && (options.drainBeforeReset !== undefined ? options.drainBeforeReset : (options.config && options.config.drainBeforeReset)),
32
+ drainSafetyPercent: options && (options.drainSafetyPercent !== undefined ? options.drainSafetyPercent : (options.config && options.config.drainSafetyPercent)),
31
33
  })
32
34
 
33
35
  if (!account || tried.has(account.ref || account.id)) {
@@ -108,7 +108,12 @@ export function createSubscriptionsService(deps) {
108
108
  const tried = new Set()
109
109
  let lastError = null
110
110
  while (true) {
111
- const account = pickAccount(pool, Date.now(), { switchAtRemaining: thr, family: modelFamily(provider, body && body.model) })
111
+ const account = pickAccount(pool, Date.now(), {
112
+ switchAtRemaining: thr,
113
+ family: modelFamily(provider, body && body.model),
114
+ drainBeforeReset: typeof deps.drainBeforeReset === 'function' ? deps.drainBeforeReset() : deps.drainBeforeReset,
115
+ drainSafetyPercent: typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : deps.drainSafetyPercent,
116
+ })
112
117
  if (!account) {
113
118
  if (lastError) throw lastError
114
119
  const err = new Error("no usable subscription account for this provider")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.52",
3
+ "version": "0.6.54",
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",