@goodandready/dsh-subscriptions 0.6.38 → 0.6.39
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/accounts.js +37 -4
- package/lib/adapter-manager.js +18 -1
- package/lib/adapter.js +11 -1
- package/lib/cascade.js +10 -0
- package/lib/rotate.js +21 -7
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure, STATUS_QUARANTINE, STATUS_PROBING } from "./quarantine.js"
|
|
1
2
|
import { fetchWithTimeout } from "./http.js"
|
|
2
3
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
4
|
import { oauthRef, isProvider } from './refs.js'
|
|
@@ -348,19 +349,51 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
348
349
|
quarantines.delete(ref)
|
|
349
350
|
if (health.has(ref)) health.set(ref, null)
|
|
350
351
|
},
|
|
351
|
-
rememberQuarantine(ref, reason, until) {
|
|
352
|
+
rememberQuarantine(ref, reason, until, attempts = 1) {
|
|
352
353
|
if (!ref) return
|
|
353
|
-
quarantines.set(ref, {
|
|
354
|
+
quarantines.set(ref, {
|
|
355
|
+
reason: reason || 'RATE_LIMIT',
|
|
356
|
+
until: Number(until) || (Date.now() + 3600000),
|
|
357
|
+
attempts: Number(attempts) || 1,
|
|
358
|
+
status: STATUS_QUARANTINE,
|
|
359
|
+
})
|
|
354
360
|
},
|
|
355
|
-
getQuarantine(ref) {
|
|
361
|
+
getQuarantine(ref, now = Date.now()) {
|
|
356
362
|
const q = quarantines.get(ref)
|
|
357
363
|
if (!q) return null
|
|
358
|
-
if (Number(q.until) <=
|
|
364
|
+
if (Number(q.until) <= now) {
|
|
365
|
+
if (q.status === STATUS_PROBING) return q
|
|
359
366
|
quarantines.delete(ref)
|
|
360
367
|
return null
|
|
361
368
|
}
|
|
362
369
|
return q
|
|
363
370
|
},
|
|
371
|
+
async probeWarmup(ref, probeFn) {
|
|
372
|
+
const q = quarantines.get(ref)
|
|
373
|
+
if (!q) return { success: true }
|
|
374
|
+
const acc = { quarantineReason: q.reason, quarantineUntil: q.until, quarantineAttempts: q.attempts || 1, status: q.status }
|
|
375
|
+
if (!needsWarmupProbe(acc)) {
|
|
376
|
+
return { success: false, reason: 'NOT_EXPIRED' }
|
|
377
|
+
}
|
|
378
|
+
quarantines.set(ref, enterProbing(acc))
|
|
379
|
+
try {
|
|
380
|
+
if (typeof probeFn === 'function') {
|
|
381
|
+
await probeFn(ref)
|
|
382
|
+
}
|
|
383
|
+
resolveWarmupSuccess(acc)
|
|
384
|
+
quarantines.delete(ref)
|
|
385
|
+
return { success: true }
|
|
386
|
+
} catch (err) {
|
|
387
|
+
const updated = resolveWarmupFailure(acc, err?.message || q.reason)
|
|
388
|
+
quarantines.set(ref, {
|
|
389
|
+
reason: updated.quarantineReason,
|
|
390
|
+
until: updated.quarantineUntil,
|
|
391
|
+
attempts: updated.quarantineAttempts,
|
|
392
|
+
status: updated.status,
|
|
393
|
+
})
|
|
394
|
+
return { success: false, account: updated, error: err }
|
|
395
|
+
}
|
|
396
|
+
},
|
|
364
397
|
releaseQuarantine(ref) {
|
|
365
398
|
quarantines.delete(ref)
|
|
366
399
|
},
|
package/lib/adapter-manager.js
CHANGED
|
@@ -21,6 +21,7 @@ export function createAdapterManager({
|
|
|
21
21
|
}) {
|
|
22
22
|
let handle
|
|
23
23
|
let adapterDisposed = false
|
|
24
|
+
let retryTimer
|
|
24
25
|
let registeredByUs = new Set()
|
|
25
26
|
|
|
26
27
|
function disposeAdapterHandle(h) {
|
|
@@ -48,6 +49,11 @@ export function createAdapterManager({
|
|
|
48
49
|
saveBlob: (ref, blob) => store.saveBlob(ref, blob),
|
|
49
50
|
alertThresholds: () => live().alertThresholds,
|
|
50
51
|
webhookUrl: () => live().webhookUrl,
|
|
52
|
+
rememberRequest: (ref) => store.rememberRequest(ref),
|
|
53
|
+
recordSuccess: (ref) => store.recordSuccess(ref),
|
|
54
|
+
recordSwitch: (ref) => store.recordSwitch(ref),
|
|
55
|
+
recordExhaust: (ref) => store.recordExhaust(ref),
|
|
56
|
+
recordBroken: (ref) => store.recordBroken(ref),
|
|
51
57
|
recordHistory,
|
|
52
58
|
fetchImpl: fetch,
|
|
53
59
|
fetchForRef,
|
|
@@ -83,6 +89,7 @@ export function createAdapterManager({
|
|
|
83
89
|
async function syncAdapter() {
|
|
84
90
|
if (adapterDisposed) return
|
|
85
91
|
const desired = await store.loggedInProviders()
|
|
92
|
+
if (adapterDisposed) return
|
|
86
93
|
const globalProviders = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
87
94
|
? ctx.llm.listProviders().map((p) => p.id)
|
|
88
95
|
: []
|
|
@@ -95,6 +102,7 @@ export function createAdapterManager({
|
|
|
95
102
|
logger.info(`[dsh-subscriptions] syncAdapter skipping already-bound providers: ${skipped.join(', ')}`)
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
if (adapterDisposed) return
|
|
98
106
|
if (!handle && claimable.length) {
|
|
99
107
|
handle = ctx.llm.registerAdapter(claimable, adapter)
|
|
100
108
|
registeredByUs = new Set(claimable)
|
|
@@ -239,7 +247,6 @@ export function createAdapterManager({
|
|
|
239
247
|
})
|
|
240
248
|
|
|
241
249
|
let retries = 0
|
|
242
|
-
let retryTimer
|
|
243
250
|
const scheduleRetry = () => {
|
|
244
251
|
if (adapterDisposed || retries >= 30) return
|
|
245
252
|
retryTimer = setTimeout(retrySync, 1000)
|
|
@@ -303,5 +310,15 @@ export function createAdapterManager({
|
|
|
303
310
|
refreshModels,
|
|
304
311
|
cascadeFallbackStream,
|
|
305
312
|
registerAdapterLifecycle,
|
|
313
|
+
dispose: () => {
|
|
314
|
+
adapterDisposed = true
|
|
315
|
+
adapter.dispose()
|
|
316
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
317
|
+
if (handle) {
|
|
318
|
+
disposeAdapterHandle(handle)
|
|
319
|
+
handle = undefined
|
|
320
|
+
registeredByUs.clear()
|
|
321
|
+
}
|
|
322
|
+
},
|
|
306
323
|
}
|
|
307
324
|
}
|
package/lib/adapter.js
CHANGED
|
@@ -32,6 +32,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
32
32
|
constructor(deps) {
|
|
33
33
|
super()
|
|
34
34
|
this.deps = deps
|
|
35
|
+
this.disposed = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dispose() {
|
|
39
|
+
this.disposed = true
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
providerInfo(provider) {
|
|
@@ -238,6 +243,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
238
243
|
} catch { /* ignore quota capture failure */ }
|
|
239
244
|
}
|
|
240
245
|
} catch (err) {
|
|
246
|
+
if (err && (err.code === 'QUOTA' || err.code === 'QUOTA_EXCEEDED' || err.status === 429)) {
|
|
247
|
+
if (typeof deps.recordExhaust === 'function') deps.recordExhaust(account.ref)
|
|
248
|
+
} else if (err && (err.status === 401 || err.status === 403 || err.code === 'TOKEN_REVOKED' || err.code === 'BROKEN')) {
|
|
249
|
+
if (typeof deps.recordBroken === 'function') deps.recordBroken(account.ref)
|
|
250
|
+
}
|
|
241
251
|
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
242
252
|
await deps.saveBlob(account.ref, {
|
|
243
253
|
...blob,
|
|
@@ -275,7 +285,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
275
285
|
}
|
|
276
286
|
// #91: when the whole pool is exhausted and nothing has been streamed
|
|
277
287
|
// yet, continue seamlessly on local Ollama instead of failing the chat.
|
|
278
|
-
if (!yieldedAny && typeof deps.ollamaFallback === 'function') {
|
|
288
|
+
if (!yieldedAny && !options.signal?.aborted && typeof deps.ollamaFallback === 'function') {
|
|
279
289
|
yield* deps.ollamaFallback({ options, provider, err })
|
|
280
290
|
return
|
|
281
291
|
}
|
package/lib/cascade.js
CHANGED
|
@@ -9,6 +9,16 @@ export const DEFAULT_CASCADE_CHAINS = {
|
|
|
9
9
|
copilot: ['cursor', 'codex'],
|
|
10
10
|
cursor: ['copilot', 'claude'],
|
|
11
11
|
antigravity: ['copilot', 'claude'],
|
|
12
|
+
kimi: ['qwen', 'glm', 'spark'],
|
|
13
|
+
glm: ['kimi', 'qwen', 'spark'],
|
|
14
|
+
qwen: ['kimi', 'glm', 'spark'],
|
|
15
|
+
spark: ['qwen', 'kimi', 'glm'],
|
|
16
|
+
jetbrains: ['copilot', 'cursor'],
|
|
17
|
+
perplexity: ['copilot', 'cursor'],
|
|
18
|
+
replit: ['copilot', 'cursor'],
|
|
19
|
+
cody: ['copilot', 'cursor'],
|
|
20
|
+
kiro: ['copilot', 'cursor'],
|
|
21
|
+
ernie: ['qwen', 'glm'],
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
/**
|
package/lib/rotate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { computePacingRisk, calculateBurnRatePerHour } from "./forecast.js"
|
|
1
2
|
import { isQuarantined } from './quarantine.js'
|
|
2
3
|
import { getPinnedAccountRef, pinSession } from './session-pin.js'
|
|
3
4
|
|
|
@@ -43,7 +44,7 @@ export function cooldownBlocks(acc, family) {
|
|
|
43
44
|
export function pickAccount(accounts, nowMs, opts) {
|
|
44
45
|
const list = Array.isArray(accounts) ? accounts : []
|
|
45
46
|
const now = Number(nowMs) || 0
|
|
46
|
-
const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0
|
|
47
|
+
const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0.01
|
|
47
48
|
const thr = Number.isFinite(thrRaw) ? thrRaw : 0
|
|
48
49
|
const isVipRequest = Boolean(opts && opts.vip)
|
|
49
50
|
const targetTag = opts && opts.tag
|
|
@@ -92,11 +93,20 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
92
93
|
// Tier 1: Quota unknown (null) & healthy
|
|
93
94
|
// Tier 2: Cooldown, quarantine or exhausted
|
|
94
95
|
const tiers = [[], [], []]
|
|
95
|
-
const fallback = []
|
|
96
96
|
|
|
97
97
|
for (const acc of list) {
|
|
98
98
|
if (!acc || !acc.hasToken) continue
|
|
99
99
|
|
|
100
|
+
if (!opts || opts.autoPacing !== false) {
|
|
101
|
+
if (acc.pacingRisk === undefined) {
|
|
102
|
+
const rem = acc.quota?.remainingPercent ?? (acc.usagePercent != null ? (100 - Number(acc.usagePercent)) : null)
|
|
103
|
+
const pace = acc.pacePerHour || (Array.isArray(acc.samples) ? calculateBurnRatePerHour(acc.samples, now) : 0)
|
|
104
|
+
const resetAt = acc.quota?.resetAt || 0
|
|
105
|
+
const riskInfo = computePacingRisk(rem, pace, resetAt, now)
|
|
106
|
+
acc.pacingRisk = riskInfo.pacingRisk
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
// VIP Slot Reservation (#175)
|
|
101
111
|
if (acc.vipOnly && !isVipRequest) continue
|
|
102
112
|
|
|
@@ -105,11 +115,16 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
105
115
|
|
|
106
116
|
const isCooldown = acc.cooldownUntil && Number(acc.cooldownUntil) > now && cooldownBlocks(acc, opts && opts.family)
|
|
107
117
|
const inQuarantine = isQuarantined(acc, now)
|
|
108
|
-
const
|
|
118
|
+
const isHardExhausted = (acc.quota && acc.quota.remaining != null && Number(acc.quota.remaining) <= 0 && acc.quota.resetAt && Number(acc.quota.resetAt) > now) ||
|
|
119
|
+
(acc.usagePercent != null && Number(acc.usagePercent) >= 100 && (!acc.quota || !acc.quota.resetAt || Number(acc.quota.resetAt) > now))
|
|
120
|
+
|
|
121
|
+
if (isCooldown || inQuarantine || isHardExhausted) {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
109
124
|
|
|
110
|
-
|
|
125
|
+
const softExhausted = isQuotaExhausted(acc) || isUsageExhausted(acc)
|
|
126
|
+
if (softExhausted) {
|
|
111
127
|
tiers[2].push(acc)
|
|
112
|
-
fallback.push(acc)
|
|
113
128
|
continue
|
|
114
129
|
}
|
|
115
130
|
|
|
@@ -140,7 +155,7 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
140
155
|
if (aWeight !== bWeight) return bWeight - aWeight
|
|
141
156
|
|
|
142
157
|
// 3. Health Score
|
|
143
|
-
return (b.healthScore
|
|
158
|
+
return (b.healthScore ?? 100) - (a.healthScore ?? 100)
|
|
144
159
|
})
|
|
145
160
|
|
|
146
161
|
const chosen = tier[0]
|
|
@@ -151,7 +166,6 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
151
166
|
}
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
if (fallback.length) return fallback[0]
|
|
155
169
|
return null
|
|
156
170
|
}
|
|
157
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.39",
|
|
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",
|