@goodandready/dsh-subscriptions 0.6.53 → 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(),
@@ -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/index.js CHANGED
@@ -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.53",
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",