@goodandready/dsh-subscriptions 0.6.37 → 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 +55 -7
- package/lib/adapter-manager.js +24 -2
- package/lib/adapter.js +28 -14
- package/lib/blob.js +17 -15
- package/lib/cascade.js +10 -0
- package/lib/config-schema.js +52 -0
- package/lib/rotate.js +21 -7
- package/lib/routes/status.js +7 -3
- package/lib/vendors/claude.js +42 -6
- package/lib/vendors/copilot.js +46 -3
- 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'
|
|
@@ -13,7 +14,6 @@ export function normalizeSlots(slots) {
|
|
|
13
14
|
const out = []
|
|
14
15
|
const seen = new Set()
|
|
15
16
|
for (const slot of Array.isArray(slots) ? slots : []) {
|
|
16
|
-
// User-editable config: a null or non-object entry must be skipped, not crash the plugin.
|
|
17
17
|
if (!slot || typeof slot !== 'object') continue
|
|
18
18
|
if (!isProvider(slot.provider)) continue
|
|
19
19
|
const index = Number(slot.index)
|
|
@@ -22,10 +22,13 @@ export function normalizeSlots(slots) {
|
|
|
22
22
|
if (seen.has(ref)) continue
|
|
23
23
|
seen.add(ref)
|
|
24
24
|
out.push({
|
|
25
|
+
...slot,
|
|
25
26
|
provider: slot.provider,
|
|
26
27
|
index,
|
|
27
28
|
label: String(slot.label || ''),
|
|
28
29
|
ref,
|
|
30
|
+
...(slot.proxyUrl ? { proxyUrl: String(slot.proxyUrl) } : {}),
|
|
31
|
+
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
29
32
|
})
|
|
30
33
|
}
|
|
31
34
|
return out
|
|
@@ -94,16 +97,27 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
async function saveBlob(ref, blob) {
|
|
100
|
+
if (!ref || typeof ref !== "string") throw new Error("invalid ref for saveBlob")
|
|
97
101
|
const prev = writeQueues.get(ref) || Promise.resolve()
|
|
98
102
|
const next = prev.catch(() => {}).then(async () => {
|
|
99
|
-
|
|
103
|
+
try {
|
|
104
|
+
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const wrapErr = new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`)
|
|
107
|
+
wrapErr.code = "CREDENTIAL_SAVE_FAILED"
|
|
108
|
+
wrapErr.cause = err
|
|
109
|
+
throw wrapErr
|
|
110
|
+
}
|
|
100
111
|
})
|
|
101
112
|
writeQueues.set(ref, next)
|
|
102
113
|
await next
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
async function clearRef(ref) {
|
|
106
|
-
|
|
117
|
+
if (!ref || typeof ref !== "string") return
|
|
118
|
+
try {
|
|
119
|
+
await credentials.unset(credentialRef(ref))
|
|
120
|
+
} catch { /* best-effort unset */ }
|
|
107
121
|
cooldowns.delete(ref)
|
|
108
122
|
quotas.delete(ref)
|
|
109
123
|
quarantines.delete(ref)
|
|
@@ -202,6 +216,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
async function ensureFresh(provider, blob, ref) {
|
|
219
|
+
if (!blob || typeof blob !== 'object') return blob
|
|
220
|
+
if (blob.apiKeyOnly || (blob.apiKey && (!blob.refreshToken || blob.refreshToken === blob.apiKey))) return blob
|
|
205
221
|
if (!blob.refreshToken) return blob
|
|
206
222
|
if (blob.expiresAt && blob.expiresAt - SKEW_MS > Date.now()) return blob
|
|
207
223
|
const lockKey = ref || (provider + ":" + (blob.accountId || blob.email || (blob.refreshToken && blob.refreshToken.slice(-16)) || "anon"))
|
|
@@ -333,19 +349,51 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
333
349
|
quarantines.delete(ref)
|
|
334
350
|
if (health.has(ref)) health.set(ref, null)
|
|
335
351
|
},
|
|
336
|
-
rememberQuarantine(ref, reason, until) {
|
|
352
|
+
rememberQuarantine(ref, reason, until, attempts = 1) {
|
|
337
353
|
if (!ref) return
|
|
338
|
-
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
|
+
})
|
|
339
360
|
},
|
|
340
|
-
getQuarantine(ref) {
|
|
361
|
+
getQuarantine(ref, now = Date.now()) {
|
|
341
362
|
const q = quarantines.get(ref)
|
|
342
363
|
if (!q) return null
|
|
343
|
-
if (Number(q.until) <=
|
|
364
|
+
if (Number(q.until) <= now) {
|
|
365
|
+
if (q.status === STATUS_PROBING) return q
|
|
344
366
|
quarantines.delete(ref)
|
|
345
367
|
return null
|
|
346
368
|
}
|
|
347
369
|
return q
|
|
348
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
|
+
},
|
|
349
397
|
releaseQuarantine(ref) {
|
|
350
398
|
quarantines.delete(ref)
|
|
351
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)
|
|
@@ -202,6 +210,8 @@ export function createAdapterManager({
|
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
let lastModelRefresh = 0
|
|
213
|
+
const providerModelCache = new Map()
|
|
214
|
+
|
|
205
215
|
async function refreshModels() {
|
|
206
216
|
if (Date.now() - lastModelRefresh < 60 * 1000) return
|
|
207
217
|
lastModelRefresh = Date.now()
|
|
@@ -214,7 +224,10 @@ export function createAdapterManager({
|
|
|
214
224
|
if (!blob) continue
|
|
215
225
|
const fresh = await store.ensureFresh(provider, blob, slot.ref)
|
|
216
226
|
const cfg = vendorConfig(provider, live())
|
|
217
|
-
await getVendor(provider).listModels(fresh, cfg, fetch).catch(() =>
|
|
227
|
+
const liveModels = await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => null)
|
|
228
|
+
if (Array.isArray(liveModels) && liveModels.length) {
|
|
229
|
+
providerModelCache.set(provider, liveModels)
|
|
230
|
+
}
|
|
218
231
|
} catch { /* best-effort */ }
|
|
219
232
|
}
|
|
220
233
|
}
|
|
@@ -234,7 +247,6 @@ export function createAdapterManager({
|
|
|
234
247
|
})
|
|
235
248
|
|
|
236
249
|
let retries = 0
|
|
237
|
-
let retryTimer
|
|
238
250
|
const scheduleRetry = () => {
|
|
239
251
|
if (adapterDisposed || retries >= 30) return
|
|
240
252
|
retryTimer = setTimeout(retrySync, 1000)
|
|
@@ -298,5 +310,15 @@ export function createAdapterManager({
|
|
|
298
310
|
refreshModels,
|
|
299
311
|
cascadeFallbackStream,
|
|
300
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
|
+
},
|
|
301
323
|
}
|
|
302
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) {
|
|
@@ -45,20 +50,24 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
45
50
|
async listModels(provider) {
|
|
46
51
|
const accounts = await this.deps.listAccounts(provider)
|
|
47
52
|
const targetAccount = accounts.find((a) => a.hasToken)
|
|
48
|
-
if (!targetAccount) return []
|
|
49
|
-
const cfg = this.deps.vendorConfig(provider)
|
|
50
|
-
const fetchImpl = pickFetch(this.deps, targetAccount.ref)
|
|
51
53
|
let models = null
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
if (targetAccount) {
|
|
55
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
56
|
+
const fetchImpl = pickFetch(this.deps, targetAccount.ref)
|
|
57
|
+
try {
|
|
58
|
+
const blob = await this.deps.ensureFresh(
|
|
59
|
+
provider,
|
|
60
|
+
await this.deps.loadBlob(targetAccount.ref),
|
|
61
|
+
targetAccount.ref,
|
|
62
|
+
)
|
|
63
|
+
const live = await getVendor(provider).listModels(blob, cfg, fetchImpl)
|
|
64
|
+
if (Array.isArray(live) && live.length) models = live
|
|
65
|
+
} catch { /* use built-in catalog */ }
|
|
66
|
+
}
|
|
67
|
+
if (!models) {
|
|
68
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
69
|
+
models = modelCatalog(provider, cfg ? cfg.models : undefined)
|
|
70
|
+
}
|
|
62
71
|
// #94: optionally hide test/preview/beta/legacy ids from the picker.
|
|
63
72
|
if (typeof this.deps.hideDeprecatedModels === 'function' && this.deps.hideDeprecatedModels()) {
|
|
64
73
|
models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
|
|
@@ -234,6 +243,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
234
243
|
} catch { /* ignore quota capture failure */ }
|
|
235
244
|
}
|
|
236
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
|
+
}
|
|
237
251
|
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
238
252
|
await deps.saveBlob(account.ref, {
|
|
239
253
|
...blob,
|
|
@@ -271,7 +285,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
271
285
|
}
|
|
272
286
|
// #91: when the whole pool is exhausted and nothing has been streamed
|
|
273
287
|
// yet, continue seamlessly on local Ollama instead of failing the chat.
|
|
274
|
-
if (!yieldedAny && typeof deps.ollamaFallback === 'function') {
|
|
288
|
+
if (!yieldedAny && !options.signal?.aborted && typeof deps.ollamaFallback === 'function') {
|
|
275
289
|
yield* deps.ollamaFallback({ options, provider, err })
|
|
276
290
|
return
|
|
277
291
|
}
|
package/lib/blob.js
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
function asString(value) {
|
|
2
|
-
return value == null ?
|
|
2
|
+
return value == null ? "" : String(value)
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function serializeBlob(obj) {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (!obj || typeof obj !== "object") throw new Error("invalid oauth blob")
|
|
7
|
+
const accessToken = asString(obj.accessToken)
|
|
8
|
+
const refreshToken = asString(obj.refreshToken)
|
|
9
|
+
if (!accessToken && !refreshToken && !obj.apiKey) {
|
|
10
|
+
throw new Error("oauth blob needs accessToken or refreshToken")
|
|
10
11
|
}
|
|
11
12
|
return JSON.stringify({
|
|
13
|
+
...obj,
|
|
12
14
|
accessToken,
|
|
13
15
|
refreshToken,
|
|
14
|
-
expiresAt: Number(obj
|
|
15
|
-
label: asString(obj
|
|
16
|
-
email: asString(obj
|
|
17
|
-
accountId: asString(obj
|
|
18
|
-
projectId: asString(obj
|
|
19
|
-
...(Array.isArray(obj
|
|
20
|
-
...(
|
|
16
|
+
expiresAt: Number(obj.expiresAt) || 0,
|
|
17
|
+
label: asString(obj.label),
|
|
18
|
+
email: asString(obj.email),
|
|
19
|
+
accountId: asString(obj.accountId),
|
|
20
|
+
projectId: asString(obj.projectId),
|
|
21
|
+
...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
|
|
22
|
+
...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
|
|
21
23
|
})
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function parseBlob(text) {
|
|
25
|
-
const obj = typeof text ===
|
|
26
|
-
if (!obj || typeof obj !==
|
|
27
|
+
const obj = typeof text === "string" ? JSON.parse(text) : text
|
|
28
|
+
if (!obj || typeof obj !== "object") throw new Error("invalid oauth blob")
|
|
27
29
|
return {
|
|
30
|
+
...obj,
|
|
28
31
|
accessToken: asString(obj.accessToken),
|
|
29
32
|
refreshToken: asString(obj.refreshToken),
|
|
30
33
|
expiresAt: Number(obj.expiresAt) || 0,
|
|
@@ -36,4 +39,3 @@ export function parseBlob(text) {
|
|
|
36
39
|
...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
|
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/config-schema.js
CHANGED
|
@@ -149,3 +149,55 @@ export function publicConfig(cfg) {
|
|
|
149
149
|
|
|
150
150
|
return clone
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
export function restoreMaskedSecrets(incoming, existing) {
|
|
154
|
+
if (!incoming || typeof incoming !== "object") return incoming
|
|
155
|
+
if (!existing || typeof existing !== "object") return incoming
|
|
156
|
+
|
|
157
|
+
const out = { ...incoming }
|
|
158
|
+
|
|
159
|
+
for (const key of Object.keys(out)) {
|
|
160
|
+
if ((key.endsWith("ClientSecret") || key.endsWith("Secret")) && typeof out[key] === "string") {
|
|
161
|
+
if (out[key].includes("••••")) {
|
|
162
|
+
out[key] = existing[key] || ""
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(out.slots) && Array.isArray(existing.slots)) {
|
|
168
|
+
out.slots = out.slots.map((s) => {
|
|
169
|
+
if (!s || typeof s !== "object" || typeof s.proxyUrl !== "string" || !s.proxyUrl.includes("••••")) return s
|
|
170
|
+
const match = existing.slots.find((e) => e && e.provider === s.provider && Number(e.index) === Number(s.index))
|
|
171
|
+
if (match && match.proxyUrl) {
|
|
172
|
+
return { ...s, proxyUrl: match.proxyUrl }
|
|
173
|
+
}
|
|
174
|
+
return s
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(out.customVendors) && Array.isArray(existing.customVendors)) {
|
|
179
|
+
out.customVendors = out.customVendors.map((cv) => {
|
|
180
|
+
if (!cv || typeof cv !== "object") return cv
|
|
181
|
+
const match = existing.customVendors.find((e) => e && e.id === cv.id)
|
|
182
|
+
if (!match) return cv
|
|
183
|
+
const copy = { ...cv }
|
|
184
|
+
for (const f of ["apiKey", "token", "secret", "clientSecret"]) {
|
|
185
|
+
if (typeof copy[f] === "string" && copy[f].includes("••••")) {
|
|
186
|
+
copy[f] = match[f] || ""
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (copy.headers && typeof copy.headers === "object" && match.headers && typeof match.headers === "object") {
|
|
190
|
+
const h = { ...copy.headers }
|
|
191
|
+
for (const hk of Object.keys(h)) {
|
|
192
|
+
if (typeof h[hk] === "string" && h[hk].includes("••••")) {
|
|
193
|
+
h[hk] = match.headers[hk] || ""
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
copy.headers = h
|
|
197
|
+
}
|
|
198
|
+
return copy
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return out
|
|
203
|
+
}
|
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/lib/routes/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #374: Sensitive GET routes protected by same-origin check
|
|
2
2
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
3
3
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
4
|
-
import { Config, plainConfig, publicConfig } from '../config-schema.js'
|
|
4
|
+
import { Config, plainConfig, publicConfig, restoreMaskedSecrets } from '../config-schema.js'
|
|
5
5
|
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
6
6
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
7
7
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
@@ -65,7 +65,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
65
65
|
return
|
|
66
66
|
}
|
|
67
67
|
// #379: revision conflict detection (CAS)
|
|
68
|
-
const
|
|
68
|
+
const rawRev = payload?.revision ?? payload?.config?.revision ?? req.headers?.['if-match']
|
|
69
|
+
const baseRev = rawRev != null ? Number(rawRev) : null
|
|
69
70
|
if (baseRev != null && !Number.isNaN(baseRev) && baseRev !== currentRevision) {
|
|
70
71
|
writeJson(res, 409, {
|
|
71
72
|
ok: false,
|
|
@@ -80,13 +81,16 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
80
81
|
|
|
81
82
|
if (payload && typeof payload.config === 'object') payload = payload.config
|
|
82
83
|
try {
|
|
84
|
+
payload = restoreMaskedSecrets(payload, live())
|
|
83
85
|
if (Array.isArray(payload.slots)) payload.slots = stripLegacySlots(payload.slots)
|
|
84
86
|
const parsed = plainConfig(Config(payload))
|
|
85
87
|
const dropped = droppedCredentialRefs(live().slots, parsed.slots)
|
|
86
88
|
await getSettingsApi().replace(parsed)
|
|
87
89
|
currentRevision++
|
|
88
90
|
syncCustomVendors()
|
|
89
|
-
for (const ref of dropped)
|
|
91
|
+
for (const ref of dropped) {
|
|
92
|
+
try { await store.clearRef(ref) } catch { /* best-effort cleanup */ }
|
|
93
|
+
}
|
|
90
94
|
await syncAdapter()
|
|
91
95
|
writeJson(res, 200, await configResponse())
|
|
92
96
|
} catch (e) {
|
package/lib/vendors/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchWithTimeout } from '../http.js'
|
|
1
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
2
3
|
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
3
4
|
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
@@ -119,15 +120,50 @@ export async function refresh(cfg, blob, fetchImpl) {
|
|
|
119
120
|
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
export async function listModels(blob, cfg) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
123
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
124
|
+
const impl = fetchImpl || fetch
|
|
125
|
+
const rawDefault = (cfg && cfg.models && cfg.models.length) ? cfg.models : defaults().models
|
|
126
|
+
const defaultCatalog = modelCatalog(id, rawDefault.map((entry) => {
|
|
127
|
+
const model = typeof entry === "string" ? { id: entry, name: entry } : { ...entry }
|
|
126
128
|
const levels = effortLevelsForModel(model.id || model.name)
|
|
127
129
|
if (!levels) return model
|
|
128
130
|
return { ...model, reasoning: { efforts: levels.map((level) => ({ id: level, name: level })) } }
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
+
}))
|
|
132
|
+
|
|
133
|
+
if (!blob || !blob.accessToken) return defaultCatalog
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const headers = {
|
|
137
|
+
"Accept": "application/json",
|
|
138
|
+
"anthropic-version": "2023-06-01",
|
|
139
|
+
}
|
|
140
|
+
if (blob.accessToken.startsWith("sk-ant-")) {
|
|
141
|
+
headers["x-api-key"] = blob.accessToken
|
|
142
|
+
} else {
|
|
143
|
+
headers["Authorization"] = `Bearer ${blob.accessToken}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const res = await fetchWithTimeout(impl, "https://api.anthropic.com/v1/models", { headers }, { timeoutMs: 15000 })
|
|
147
|
+
if (!res.ok) return defaultCatalog
|
|
148
|
+
const json = await readJson(res)
|
|
149
|
+
const items = json && json.data
|
|
150
|
+
if (!Array.isArray(items) || !items.length) return defaultCatalog
|
|
151
|
+
const rows = []
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
const modelId = item.id || item.display_name
|
|
154
|
+
if (!modelId) continue
|
|
155
|
+
const levels = effortLevelsForModel(modelId)
|
|
156
|
+
rows.push({
|
|
157
|
+
id: modelId,
|
|
158
|
+
name: item.display_name || modelId,
|
|
159
|
+
...(levels ? { reasoning: { efforts: levels.map((level) => ({ id: level, name: level })) } } : {}),
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
if (!rows.length) return defaultCatalog
|
|
163
|
+
return modelCatalog(id, rows)
|
|
164
|
+
} catch {
|
|
165
|
+
return defaultCatalog
|
|
166
|
+
}
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
// Advertise adaptive thinking + effort only for models whose capability table
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LlmError } from "@deepseek-ai/dsh-llm"
|
|
2
|
-
import { openaiMessages, openaiTools } from "../messages.js"
|
|
2
|
+
import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
|
|
3
|
+
import { fetchWithTimeout } from "../http.js"
|
|
3
4
|
import { openaiChatStream, readJson, httpError } from "../wire.js"
|
|
4
5
|
import { asUsageSnapshot } from "../usage.js"
|
|
5
6
|
|
|
@@ -68,8 +69,50 @@ export function authorizeUrl() {
|
|
|
68
69
|
return "https://github.com/login/device"
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
export async function listModels() {
|
|
72
|
-
|
|
72
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
73
|
+
const impl = fetchImpl || fetch
|
|
74
|
+
const fallback = modelCatalog(id, (cfg && cfg.models) || COPILOT_MODELS)
|
|
75
|
+
if (!blob) return fallback
|
|
76
|
+
const githubToken = blob.refreshToken || blob.githubToken
|
|
77
|
+
let copilotToken = blob.accessToken
|
|
78
|
+
try {
|
|
79
|
+
if ((!copilotToken || !blob.expiresAt || blob.expiresAt < Date.now() + 60000) && githubToken) {
|
|
80
|
+
const exchange = await exchangeCopilotToken(githubToken, impl)
|
|
81
|
+
if (exchange && exchange.token) {
|
|
82
|
+
copilotToken = exchange.token
|
|
83
|
+
blob.accessToken = exchange.token
|
|
84
|
+
if (exchange.expires_at) blob.expiresAt = exchange.expires_at * 1000
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!copilotToken) return fallback
|
|
88
|
+
const res = await fetchWithTimeout(impl, "https://api.githubcopilot.com/models", {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
91
|
+
Accept: "application/json",
|
|
92
|
+
"User-Agent": "GitHubCopilotChat/0.24.0",
|
|
93
|
+
},
|
|
94
|
+
}, { timeoutMs: 15000 })
|
|
95
|
+
if (!res.ok) return fallback
|
|
96
|
+
const json = await readJson(res)
|
|
97
|
+
const items = json && (json.data || json.models || json)
|
|
98
|
+
if (!Array.isArray(items) || !items.length) return fallback
|
|
99
|
+
const rows = []
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const modelId = item.id || item.name
|
|
102
|
+
if (!modelId) continue
|
|
103
|
+
if (item.capabilities && item.capabilities.type && item.capabilities.type !== "chat") continue
|
|
104
|
+
rows.push({
|
|
105
|
+
id: modelId,
|
|
106
|
+
name: item.name || modelId,
|
|
107
|
+
...(item.capabilities && item.capabilities.limits && item.capabilities.limits.max_prompt_tokens ? { contextWindow: item.capabilities.limits.max_prompt_tokens } : {}),
|
|
108
|
+
...(item.capabilities && item.capabilities.supports && item.capabilities.supports.vision ? { inputModalities: ["text", "image"] } : {}),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
if (!rows.length) return fallback
|
|
112
|
+
return modelCatalog(id, rows)
|
|
113
|
+
} catch {
|
|
114
|
+
return fallback
|
|
115
|
+
}
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
export function getTelemetryHeaders(sessionId) {
|
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",
|