@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.
- package/lib/adapter-manager.js +2 -0
- package/lib/adapter.js +12 -1
- package/lib/config-schema.js +4 -0
- package/lib/forecast.js +16 -0
- package/lib/index.js +7 -1
- package/lib/rotate.js +41 -1
- package/lib/stream-rotate.js +2 -0
- package/lib/subscriptions.js +6 -1
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -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
|
|
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/config-schema.js
CHANGED
|
@@ -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(), {
|
|
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
|
package/lib/stream-rotate.js
CHANGED
|
@@ -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)) {
|
package/lib/subscriptions.js
CHANGED
|
@@ -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(), {
|
|
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.
|
|
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",
|