@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.
- package/lib/adapter-manager.js +2 -0
- package/lib/adapter.js +12 -1
- package/lib/client.js +45 -12
- package/lib/config-schema.js +4 -0
- package/lib/forecast.js +16 -0
- package/lib/http.js +2 -2
- package/lib/index.js +8 -2
- 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/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', '
|
|
2366
|
-
|
|
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
|
|
2372
|
-
return
|
|
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(
|
|
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(
|
|
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) => {
|
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/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(), {
|
|
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",
|