@goodandready/dsh-subscriptions 0.6.22 → 0.6.23
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-view.js +84 -0
- package/lib/accounts.js +5 -2
- package/lib/adapter-manager.js +300 -0
- package/lib/adapter.js +7 -7
- package/lib/alerts.js +2 -2
- package/lib/backoff.js +1 -1
- package/lib/client.js +89 -75
- package/lib/code-assist.js +1 -1
- package/lib/config-schema.js +46 -6
- package/lib/diagnostics.js +60 -0
- package/lib/history.js +21 -1
- package/lib/http.js +58 -11
- package/lib/index.js +143 -543
- package/lib/mask.js +1 -1
- package/lib/ollama.js +4 -4
- package/lib/routes/accounts.js +5 -1
- package/lib/routes/proxy.js +1 -1
- package/lib/routes/status.js +42 -7
- package/lib/routes.js +0 -1
- package/lib/session-pin.js +0 -8
- package/lib/subscriptions.js +5 -6
- package/lib/vault.js +2 -2
- package/lib/vendor-factory.js +0 -1
- package/lib/vendors/antigravity.js +1 -1
- package/lib/vendors/codex.js +1 -1
- package/lib/vendors/grok.js +1 -1
- package/lib/vendors/index.js +0 -1
- package/lib/vendors/qwen.js +1 -1
- package/lib/vendors/spark.js +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { normalizeSlots } from './accounts.js'
|
|
2
|
+
import {
|
|
3
|
+
inspectGoogleAccount,
|
|
4
|
+
antigravityMetadata,
|
|
5
|
+
antigravityIdentityHeaders,
|
|
6
|
+
} from './code-assist.js'
|
|
7
|
+
|
|
8
|
+
export async function enrichAntigravityAccount(slot, info, store, fetch) {
|
|
9
|
+
if (!info.configured || slot.provider !== 'antigravity') return info
|
|
10
|
+
let blob
|
|
11
|
+
try { blob = await store.loadBlob(slot.ref) } catch { return info }
|
|
12
|
+
if (blob.validationUrl) {
|
|
13
|
+
return {
|
|
14
|
+
...info,
|
|
15
|
+
validationUrl: blob.validationUrl,
|
|
16
|
+
validationMessage: blob.validationMessage || info.validationMessage || '',
|
|
17
|
+
paidTierName: blob.paidTierName || info.paidTierName || '',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const fresh = await store.ensureFresh(slot.provider, blob, slot.ref)
|
|
22
|
+
const probe = await inspectGoogleAccount(fetch, fresh.accessToken, {
|
|
23
|
+
metadata: antigravityMetadata(fresh.projectId || ''),
|
|
24
|
+
extraHeaders: antigravityIdentityHeaders(fresh.projectId || ''),
|
|
25
|
+
projectId: fresh.projectId,
|
|
26
|
+
})
|
|
27
|
+
const next = { ...fresh }
|
|
28
|
+
if (probe.projectId && probe.projectId !== fresh.projectId) next.projectId = probe.projectId
|
|
29
|
+
if (probe.paidTierId) next.paidTierId = probe.paidTierId
|
|
30
|
+
if (probe.paidTierName) next.paidTierName = probe.paidTierName
|
|
31
|
+
if (probe.validation?.validationUrl) {
|
|
32
|
+
next.validationUrl = probe.validation.validationUrl
|
|
33
|
+
next.validationMessage = probe.validation.message || ''
|
|
34
|
+
await store.saveBlob(slot.ref, next)
|
|
35
|
+
return {
|
|
36
|
+
...info,
|
|
37
|
+
validationUrl: next.validationUrl,
|
|
38
|
+
validationMessage: next.validationMessage,
|
|
39
|
+
paidTierName: next.paidTierName || '',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (probe.notice?.message) {
|
|
43
|
+
next.accountNotice = probe.notice.message
|
|
44
|
+
await store.saveBlob(slot.ref, next)
|
|
45
|
+
return { ...info, accountNotice: next.accountNotice, paidTierName: next.paidTierName || '' }
|
|
46
|
+
}
|
|
47
|
+
if (probe.projectId || probe.paidTierId) await store.saveBlob(slot.ref, next)
|
|
48
|
+
return { ...info, paidTierName: next.paidTierName || info.paidTierName || '' }
|
|
49
|
+
} catch { /* keep settings responsive */ }
|
|
50
|
+
return info
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function createAccountsView({ live, store, fetch, pmL }) {
|
|
54
|
+
const out = []
|
|
55
|
+
for (const slot of normalizeSlots(live().slots)) {
|
|
56
|
+
const info = await enrichAntigravityAccount(slot, await store.describeRef(slot.ref), store, fetch)
|
|
57
|
+
out.push({
|
|
58
|
+
provider: slot.provider,
|
|
59
|
+
index: slot.index,
|
|
60
|
+
ref: slot.ref,
|
|
61
|
+
label: pmL(slot.label || info.label),
|
|
62
|
+
configured: info.configured,
|
|
63
|
+
writable: info.writable,
|
|
64
|
+
cooldownUntil: info.cooldownUntil,
|
|
65
|
+
// Issue 303: expose the cooldown scope and quarantine so the card can
|
|
66
|
+
// explain why only some models are blocked.
|
|
67
|
+
cooldownFamilies: info.cooldownFamilies || null,
|
|
68
|
+
quarantineUntil: info.quarantineUntil || 0,
|
|
69
|
+
quarantineReason: info.quarantineReason || null,
|
|
70
|
+
healthScore: info.healthScore,
|
|
71
|
+
healthBadge: info.healthBadge,
|
|
72
|
+
usagePercent: info.usagePercent,
|
|
73
|
+
quota: info.quota || null,
|
|
74
|
+
usage: info.usage || null,
|
|
75
|
+
requests: store.getRequestCount(slot.ref) || null,
|
|
76
|
+
refreshError: info.refreshError || '',
|
|
77
|
+
validationUrl: info.validationUrl || '',
|
|
78
|
+
validationMessage: info.validationMessage || '',
|
|
79
|
+
accountNotice: info.accountNotice || '',
|
|
80
|
+
paidTierName: info.paidTierName || '',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
package/lib/accounts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { fetchWithTimeout } from "./http.js"
|
|
|
2
2
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
3
|
import { oauthRef, isProvider } from './refs.js'
|
|
4
4
|
import { parseBlob, serializeBlob } from './blob.js'
|
|
5
|
-
import { recordSwitch, recordExhaust, recordBroken, computeHealthScore } from './health.js'
|
|
5
|
+
import { recordSwitch, recordExhaust, recordBroken, computeHealthScore, healthBadge } from './health.js'
|
|
6
6
|
import { executeWithRetry } from './backoff.js'
|
|
7
7
|
import { getVendor } from './vendors/index.js'
|
|
8
8
|
|
|
@@ -153,6 +153,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
153
153
|
quota: quotas.get(ref) || null,
|
|
154
154
|
usage: windows.get(ref) || base.usage || null,
|
|
155
155
|
health: health.get(ref) || null,
|
|
156
|
+
healthScore: computeHealthScore(health.get(ref) || null),
|
|
157
|
+
healthBadge: healthBadge(computeHealthScore(health.get(ref) || null)),
|
|
156
158
|
usagePercent: usage.has(ref) ? usage.get(ref) : null,
|
|
157
159
|
refreshError: refreshFailures.get(ref)?.error || '',
|
|
158
160
|
validationUrl: base.validationUrl || '',
|
|
@@ -179,6 +181,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
179
181
|
cooldownFamilies: info.cooldownFamilies || null,
|
|
180
182
|
quota: info.quota || null,
|
|
181
183
|
healthScore: computeHealthScore(info.health),
|
|
184
|
+
healthBadge: healthBadge(computeHealthScore(info.health)),
|
|
182
185
|
quarantineUntil: inQ ? q.until : 0,
|
|
183
186
|
quarantineReason: inQ ? q.reason : null,
|
|
184
187
|
label: slot.label || info.label,
|
|
@@ -271,7 +274,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
271
274
|
for (const th of THRESHOLDS) {
|
|
272
275
|
if (pct >= th && prev < th) {
|
|
273
276
|
notifyThresholds.set(key, th)
|
|
274
|
-
try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch
|
|
277
|
+
try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch { /* ignore notification failure */ }
|
|
275
278
|
break
|
|
276
279
|
}
|
|
277
280
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { SubscriptionAdapter } from './adapter.js'
|
|
2
|
+
import { resolveFallbackVendor, mapFallbackModel } from './cascade.js'
|
|
3
|
+
import { normalizeSlots, vendorConfig } from './accounts.js'
|
|
4
|
+
import { PROVIDERS, isProvider, oauthRef } from './refs.js'
|
|
5
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
6
|
+
import { getVendor } from './vendors/index.js'
|
|
7
|
+
import { Config } from './config-schema.js'
|
|
8
|
+
|
|
9
|
+
export function createAdapterManager({
|
|
10
|
+
ctx,
|
|
11
|
+
live,
|
|
12
|
+
store,
|
|
13
|
+
logger,
|
|
14
|
+
fetch,
|
|
15
|
+
fetchForRef,
|
|
16
|
+
recordHistory,
|
|
17
|
+
getSettingsApi,
|
|
18
|
+
syncCustomVendors,
|
|
19
|
+
syncOllama,
|
|
20
|
+
stripLegacySlots,
|
|
21
|
+
}) {
|
|
22
|
+
let handle
|
|
23
|
+
let adapterDisposed = false
|
|
24
|
+
let registeredByUs = new Set()
|
|
25
|
+
|
|
26
|
+
function disposeAdapterHandle(h) {
|
|
27
|
+
if (!h) return
|
|
28
|
+
try {
|
|
29
|
+
if (typeof h === 'function') h()
|
|
30
|
+
else if (typeof h.dispose === 'function') h.dispose()
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] dispose handle error: ' + (err && err.message || err))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const adapter = new SubscriptionAdapter({
|
|
37
|
+
listAccounts: (provider) => store.listAccounts(provider),
|
|
38
|
+
loadBlob: (ref) => store.loadBlob(ref),
|
|
39
|
+
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
40
|
+
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
41
|
+
cooldownMs: () => live().cooldownMs,
|
|
42
|
+
switchAtRemaining: () => live().switchAtRemaining,
|
|
43
|
+
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
44
|
+
rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
|
|
45
|
+
rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
|
|
46
|
+
getQuota: (ref) => store.getQuota(ref),
|
|
47
|
+
refreshUsage: (provider) => store.refreshUsage(provider),
|
|
48
|
+
saveBlob: (ref, blob) => store.saveBlob(ref, blob),
|
|
49
|
+
recordHistory,
|
|
50
|
+
fetchImpl: fetch,
|
|
51
|
+
fetchForRef,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
async function* cascadeFallbackStream({ options, provider, err, visited = new Set() }) {
|
|
55
|
+
const cfg = live()
|
|
56
|
+
if (!cfg.cascadingFallback) throw err
|
|
57
|
+
const logged = await store.loggedInProviders()
|
|
58
|
+
const available = Object.keys(logged || {}).filter((p) => logged[p] && p !== provider)
|
|
59
|
+
visited.add(provider)
|
|
60
|
+
const nextVendor = resolveFallbackVendor(provider, available, cfg.cascadingChain, visited)
|
|
61
|
+
if (!nextVendor) throw err
|
|
62
|
+
visited.add(nextVendor)
|
|
63
|
+
const targetModel = mapFallbackModel(provider, options && options.model, nextVendor)
|
|
64
|
+
if (logger && logger.warn) {
|
|
65
|
+
logger.warn('[dsh-subscriptions] ' + provider + ': all accounts exhausted, cascading fallback to ' + nextVendor + '/' + targetModel)
|
|
66
|
+
}
|
|
67
|
+
yield* adapter.stream({ ...options, provider: nextVendor, model: targetModel }, visited)
|
|
68
|
+
try {
|
|
69
|
+
recordHistory({
|
|
70
|
+
provider: nextVendor,
|
|
71
|
+
ref: String(nextVendor).toUpperCase() + '_CASCADE_FALLBACK',
|
|
72
|
+
model: targetModel,
|
|
73
|
+
path: '/v1/chat/completions',
|
|
74
|
+
method: 'POST',
|
|
75
|
+
status: 200,
|
|
76
|
+
kind: 'cascade',
|
|
77
|
+
})
|
|
78
|
+
} catch { /* ignore cascade history recording error */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function syncAdapter() {
|
|
82
|
+
if (adapterDisposed) return
|
|
83
|
+
const desired = await store.loggedInProviders()
|
|
84
|
+
const globalProviders = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
85
|
+
? ctx.llm.listProviders().map((p) => p.id)
|
|
86
|
+
: []
|
|
87
|
+
const globalSet = new Set(globalProviders)
|
|
88
|
+
|
|
89
|
+
const claimable = desired.filter((p) => !globalSet.has(p) || registeredByUs.has(p))
|
|
90
|
+
const skipped = desired.filter((p) => globalSet.has(p) && !registeredByUs.has(p))
|
|
91
|
+
|
|
92
|
+
if (skipped.length && logger && logger.info) {
|
|
93
|
+
logger.info(`[dsh-subscriptions] syncAdapter skipping already-bound providers: ${skipped.join(', ')}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!handle && claimable.length) {
|
|
97
|
+
handle = ctx.llm.registerAdapter(claimable, adapter)
|
|
98
|
+
registeredByUs = new Set(claimable)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (!handle) {
|
|
102
|
+
registeredByUs.clear()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (!claimable.length) {
|
|
106
|
+
disposeAdapterHandle(handle)
|
|
107
|
+
handle = undefined
|
|
108
|
+
registeredByUs.clear()
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (typeof handle.replace === 'function') {
|
|
112
|
+
try {
|
|
113
|
+
handle.replace(claimable)
|
|
114
|
+
registeredByUs = new Set(claimable)
|
|
115
|
+
return
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] handle.replace failed: ' + (err && err.message || err))
|
|
118
|
+
disposeAdapterHandle(handle)
|
|
119
|
+
handle = undefined
|
|
120
|
+
registeredByUs.clear()
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
disposeAdapterHandle(handle)
|
|
124
|
+
handle = undefined
|
|
125
|
+
registeredByUs.clear()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const currentGlobal = new Set((ctx.llm && typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []).map((p) => p.id))
|
|
129
|
+
const reClaimable = desired.filter((p) => !currentGlobal.has(p))
|
|
130
|
+
if (reClaimable.length) {
|
|
131
|
+
handle = ctx.llm.registerAdapter(reClaimable, adapter)
|
|
132
|
+
registeredByUs = new Set(reClaimable)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function reconcileSlots() {
|
|
137
|
+
const api = getSettingsApi()
|
|
138
|
+
if (!api || typeof api.replace !== 'function') return
|
|
139
|
+
const creds = ctx.credentials
|
|
140
|
+
if (!creds || typeof creds.describe !== 'function') return
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const existing = normalizeSlots(live().slots)
|
|
144
|
+
const existingRefs = new Set(existing.map((s) => s.ref))
|
|
145
|
+
const candidateProviders = PROVIDERS.filter((p) => isProvider(p))
|
|
146
|
+
const candidates = []
|
|
147
|
+
for (const provider of candidateProviders) {
|
|
148
|
+
for (let index = 1; index <= 10; index++) {
|
|
149
|
+
const ref = oauthRef(provider, index)
|
|
150
|
+
if (!existingRefs.has(ref)) {
|
|
151
|
+
candidates.push({ provider, index, ref })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const additionResults = await Promise.all(
|
|
157
|
+
candidates.map(async ({ provider, index, ref }) => {
|
|
158
|
+
let d
|
|
159
|
+
try {
|
|
160
|
+
d = await creds.describe(credentialRef(ref))
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] describe ${ref} error: ` + (err && err.message || err))
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
if (d && d.configured) {
|
|
166
|
+
let label = d.label || ''
|
|
167
|
+
try {
|
|
168
|
+
const blob = await store.loadBlob(ref).catch(() => null)
|
|
169
|
+
if (blob && (blob.email || blob.label)) {
|
|
170
|
+
label = blob.email || blob.label || label
|
|
171
|
+
}
|
|
172
|
+
} catch (loadErr) {
|
|
173
|
+
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] loadBlob ${ref} error: ` + (loadErr && loadErr.message || loadErr))
|
|
174
|
+
}
|
|
175
|
+
return { provider, index, label }
|
|
176
|
+
}
|
|
177
|
+
return null
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
const additions = additionResults.filter(Boolean)
|
|
181
|
+
|
|
182
|
+
if (!additions.length) return
|
|
183
|
+
if (logger && logger.info) {
|
|
184
|
+
logger.info(`[dsh-subscriptions reconcile] restoring orphaned vault slots: ${additions.map((a) => `${a.provider}#${a.index}`).join(', ')}`)
|
|
185
|
+
}
|
|
186
|
+
const currentSlots = Array.isArray(live().slots) ? live().slots : []
|
|
187
|
+
const newSlots = [...currentSlots, ...additions]
|
|
188
|
+
const parsed = Config({ ...live(), slots: stripLegacySlots(newSlots) })
|
|
189
|
+
await api.replace(parsed)
|
|
190
|
+
syncCustomVendors()
|
|
191
|
+
await syncAdapter()
|
|
192
|
+
refreshModels().catch((err) => {
|
|
193
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions reconcile] refreshModels error: ' + (err && err.message || err))
|
|
194
|
+
})
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (logger && logger.warn) {
|
|
197
|
+
logger.warn('[dsh-subscriptions reconcile] failed: ' + (e && e.message || e))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let lastModelRefresh = 0
|
|
203
|
+
async function refreshModels() {
|
|
204
|
+
if (Date.now() - lastModelRefresh < 60 * 1000) return
|
|
205
|
+
lastModelRefresh = Date.now()
|
|
206
|
+
const providers = await store.loggedInProviders()
|
|
207
|
+
for (const provider of providers) {
|
|
208
|
+
try {
|
|
209
|
+
const slot = normalizeSlots(live().slots).find((x) => x.provider === provider)
|
|
210
|
+
if (!slot) continue
|
|
211
|
+
const blob = await store.loadBlob(slot.ref).catch(() => null)
|
|
212
|
+
if (!blob) continue
|
|
213
|
+
const fresh = await store.ensureFresh(provider, blob, slot.ref)
|
|
214
|
+
const cfg = vendorConfig(provider, live())
|
|
215
|
+
await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => {})
|
|
216
|
+
} catch { /* best-effort */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function registerAdapterLifecycle() {
|
|
221
|
+
return ctx.effect(() => {
|
|
222
|
+
syncAdapter().catch((err) => {
|
|
223
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncAdapter initial error: ' + (err && err.message || err))
|
|
224
|
+
})
|
|
225
|
+
if (syncOllama) {
|
|
226
|
+
syncOllama().catch((err) => {
|
|
227
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncOllama initial error: ' + (err && err.message || err))
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
reconcileSlots().catch((err) => {
|
|
231
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] reconcileSlots effect error: ' + (err && err.message || err))
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
let retries = 0
|
|
235
|
+
let retryTimer
|
|
236
|
+
const scheduleRetry = () => {
|
|
237
|
+
if (adapterDisposed || retries >= 30) return
|
|
238
|
+
retryTimer = setTimeout(retrySync, 1000)
|
|
239
|
+
if (retryTimer && typeof retryTimer.unref === 'function') retryTimer.unref()
|
|
240
|
+
}
|
|
241
|
+
const retrySync = async () => {
|
|
242
|
+
if (adapterDisposed) return
|
|
243
|
+
try {
|
|
244
|
+
const loggedIn = await store.loggedInProviders()
|
|
245
|
+
if (!loggedIn.length) return
|
|
246
|
+
const registered = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
247
|
+
? ctx.llm.listProviders().map((p) => p.id)
|
|
248
|
+
: []
|
|
249
|
+
const regSet = new Set(registered)
|
|
250
|
+
const pending = loggedIn.filter((p) => !regSet.has(p))
|
|
251
|
+
if (pending.length === 0) return
|
|
252
|
+
retries += 1
|
|
253
|
+
await syncAdapter()
|
|
254
|
+
if (retries < 30) {
|
|
255
|
+
scheduleRetry()
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] retrySync error: ' + (err && err.message || err))
|
|
259
|
+
if (retries < 30) {
|
|
260
|
+
scheduleRetry()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
scheduleRetry()
|
|
265
|
+
|
|
266
|
+
// #75: eager usage refresh at startup so the windows (5h/7d) land in the blob immediately
|
|
267
|
+
const eager = async () => {
|
|
268
|
+
try {
|
|
269
|
+
for (const slot of normalizeSlots(live().slots)) {
|
|
270
|
+
store.refreshUsage(slot.provider).catch((err) => {
|
|
271
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager refresh error: ' + (err && err.message || err))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
eager()
|
|
279
|
+
|
|
280
|
+
return () => {
|
|
281
|
+
adapterDisposed = true
|
|
282
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
283
|
+
if (handle) {
|
|
284
|
+
disposeAdapterHandle(handle)
|
|
285
|
+
handle = undefined
|
|
286
|
+
registeredByUs.clear()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}, 'dsh-subscriptions: llm adapter')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
adapter,
|
|
294
|
+
syncAdapter,
|
|
295
|
+
reconcileSlots,
|
|
296
|
+
refreshModels,
|
|
297
|
+
cascadeFallbackStream,
|
|
298
|
+
registerAdapterLifecycle,
|
|
299
|
+
}
|
|
300
|
+
}
|
package/lib/adapter.js
CHANGED
|
@@ -37,7 +37,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
37
37
|
return { id: provider, name: displayName(provider) }
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
providerRetryPolicy(
|
|
40
|
+
providerRetryPolicy() {
|
|
41
41
|
return undefined
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -67,11 +67,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
67
67
|
return models
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
imageRequestPricing(
|
|
70
|
+
imageRequestPricing() {
|
|
71
71
|
return undefined
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
async resolveModel(provider, model
|
|
74
|
+
async resolveModel(provider, model) {
|
|
75
75
|
const rows = await this.listModels(provider)
|
|
76
76
|
const found = rows.find((row) => row && row.id === model)
|
|
77
77
|
if (found) {
|
|
@@ -127,11 +127,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
127
127
|
try {
|
|
128
128
|
const snap = quotaSnapshot(provider, res.headers, null, Date.now())
|
|
129
129
|
if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
|
|
130
|
-
} catch
|
|
130
|
+
} catch { /* ignore quota capture failure */ }
|
|
131
131
|
if (!res.ok && res.clone) {
|
|
132
132
|
try {
|
|
133
133
|
const txt = await res.clone().text()
|
|
134
|
-
let j=null; try { j = JSON.parse(txt) } catch
|
|
134
|
+
let j=null; try { j = JSON.parse(txt) } catch { j = null }
|
|
135
135
|
if (j) {
|
|
136
136
|
const b = parseBody(provider, j, Date.now())
|
|
137
137
|
if (b) {
|
|
@@ -139,7 +139,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
139
139
|
if (snap2) deps.rememberQuota(account.ref, snap2)
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
-
} catch
|
|
142
|
+
} catch { /* ignore quota capture failure */ }
|
|
143
143
|
}
|
|
144
144
|
return res
|
|
145
145
|
}
|
|
@@ -169,7 +169,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
169
169
|
ms: Date.now() - t0,
|
|
170
170
|
kind: 'stream',
|
|
171
171
|
})
|
|
172
|
-
} catch
|
|
172
|
+
} catch { /* ignore quota capture failure */ }
|
|
173
173
|
}
|
|
174
174
|
} catch (err) {
|
|
175
175
|
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
package/lib/alerts.js
CHANGED
|
@@ -8,7 +8,7 @@ const DEDUP_WINDOW_MS = 10 * 60 * 1000 // 10 minutes cooldown per (ref + type)
|
|
|
8
8
|
const alertBuffer = []
|
|
9
9
|
const lastAlertTime = new Map()
|
|
10
10
|
|
|
11
|
-
export function
|
|
11
|
+
export function clearAlerts() {
|
|
12
12
|
alertBuffer.length = 0
|
|
13
13
|
lastAlertTime.clear()
|
|
14
14
|
}
|
|
@@ -75,7 +75,7 @@ export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch)
|
|
|
75
75
|
body: JSON.stringify(payload),
|
|
76
76
|
})
|
|
77
77
|
return res.ok
|
|
78
|
-
} catch
|
|
78
|
+
} catch {
|
|
79
79
|
// Non-blocking fire-and-forget
|
|
80
80
|
return false
|
|
81
81
|
}
|