@goodandready/dsh-subscriptions 0.7.31 → 0.7.33
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/account-reliability.js +68 -0
- package/lib/accounts-view.js +2 -0
- package/lib/accounts.js +28 -0
- package/lib/index.js +3 -3
- package/lib/rotate.js +15 -1
- package/lib/routing-reliability.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function text(value) {
|
|
2
|
+
return value == null ? '' : String(value).trim()
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #827: decide whether a stored credential is usable without exposing it.
|
|
7
|
+
* Empty access/refresh tokens and non-finite expiry are treated as corrupt.
|
|
8
|
+
*/
|
|
9
|
+
export function credentialState(blob) {
|
|
10
|
+
if (!blob || typeof blob !== 'object') return { usable: false, reason: 'invalid_blob' }
|
|
11
|
+
const accessToken = text(blob.accessToken)
|
|
12
|
+
const refreshToken = text(blob.refreshToken)
|
|
13
|
+
if (!accessToken && !refreshToken && !text(blob.apiKey)) {
|
|
14
|
+
return { usable: false, reason: 'empty_credentials' }
|
|
15
|
+
}
|
|
16
|
+
if (blob.expiresAt != null && blob.expiresAt !== '' && !Number.isFinite(Number(blob.expiresAt))) {
|
|
17
|
+
return { usable: false, reason: 'invalid_expiry' }
|
|
18
|
+
}
|
|
19
|
+
return { usable: true, reason: null }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* #831: stable provider identity used for duplicate detection. Unknown
|
|
24
|
+
* identities return null and must never be merged automatically.
|
|
25
|
+
*/
|
|
26
|
+
export function accountIdentityKey(provider, blob) {
|
|
27
|
+
if (!blob || typeof blob !== 'object') return null
|
|
28
|
+
const id = text(blob.email || blob.login || blob.accountId || blob.userId)
|
|
29
|
+
if (!id) return null
|
|
30
|
+
return `${text(provider).toLowerCase()}:${id.toLowerCase()}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* #818: aliases are display-only. Resolve either a stable ref or a legacy
|
|
35
|
+
* label within one provider back to the stable credential ref.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveAccountRef(slots, provider, value) {
|
|
38
|
+
const wanted = text(value)
|
|
39
|
+
if (!wanted) return null
|
|
40
|
+
const list = (Array.isArray(slots) ? slots : []).filter((slot) => slot && slot.provider === provider)
|
|
41
|
+
const byIndex = Number(wanted)
|
|
42
|
+
if (Number.isInteger(byIndex) && byIndex > 0) {
|
|
43
|
+
return list.find((slot) => Number(slot.index) === byIndex)?.ref || null
|
|
44
|
+
}
|
|
45
|
+
return list.find((slot) => slot.ref === wanted || text(slot.label) === wanted)?.ref || null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* #806: a delayed refresh may only write when its credential lineage is
|
|
50
|
+
* still current. A newer save/revoke bumps the version.
|
|
51
|
+
*/
|
|
52
|
+
export function refreshIsCurrent(currentVersion, expectedVersion) {
|
|
53
|
+
return Number(currentVersion) === Number(expectedVersion)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* #833: deterministic host-env vs plugin-proxy precedence.
|
|
58
|
+
* @param {{ hostProxy?: string, pluginProxy?: string, prefer?: 'host' | 'plugin' }} [options]
|
|
59
|
+
*/
|
|
60
|
+
export function proxyPrecedence(options = {}) {
|
|
61
|
+
const prefer = options.prefer || 'host'
|
|
62
|
+
const host = text(options.hostProxy)
|
|
63
|
+
const plugin = text(options.pluginProxy)
|
|
64
|
+
if (host && plugin && prefer === 'plugin') return { proxy: plugin, source: 'plugin', conflict: true }
|
|
65
|
+
if (host) return { proxy: host, source: 'host', conflict: Boolean(plugin) }
|
|
66
|
+
if (plugin) return { proxy: plugin, source: 'plugin', conflict: false }
|
|
67
|
+
return { proxy: '', source: 'none', conflict: false }
|
|
68
|
+
}
|
package/lib/accounts-view.js
CHANGED
|
@@ -169,6 +169,8 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
169
169
|
poolEnabled: slot.poolEnabled !== false,
|
|
170
170
|
isDefault: !!slot.isDefault,
|
|
171
171
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
172
|
+
credentialError: info.credentialError || null,
|
|
173
|
+
duplicateOf: info.duplicateOf || null,
|
|
172
174
|
capabilities,
|
|
173
175
|
billing,
|
|
174
176
|
renewsAt: billing.renewsAt,
|
package/lib/accounts.js
CHANGED
|
@@ -46,6 +46,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
|
46
46
|
import { oauthRef, isProvider } from './refs.js'
|
|
47
47
|
import { parseBlob, serializeBlob } from './blob.js'
|
|
48
48
|
import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
|
|
49
|
+
import { accountIdentityKey, credentialState } from './account-reliability.js'
|
|
49
50
|
import { recordSwitch, recordExhaust, recordBroken, computeHealthScore, healthBadge } from './health.js'
|
|
50
51
|
import { executeWithRetry } from './backoff.js'
|
|
51
52
|
import { getVendor } from './vendors/index.js'
|
|
@@ -308,6 +309,26 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
308
309
|
if (base.configured) {
|
|
309
310
|
try {
|
|
310
311
|
const blob = parseBlob(await resolveRaw(ref))
|
|
312
|
+
const state = credentialState(blob)
|
|
313
|
+
if (!state.usable) {
|
|
314
|
+
base.configured = false
|
|
315
|
+
base.credentialError = state.reason
|
|
316
|
+
base.label = blob.label || blob.email || ''
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
cooldownUntil: 0,
|
|
320
|
+
quarantineUntil: 0,
|
|
321
|
+
cooldownFamilies: null,
|
|
322
|
+
cooldownModels: null,
|
|
323
|
+
quota: null,
|
|
324
|
+
isRevoked: false,
|
|
325
|
+
authWarning: '',
|
|
326
|
+
validationUrl: '',
|
|
327
|
+
validationMessage: '',
|
|
328
|
+
accountNotice: '',
|
|
329
|
+
paidTierName: '',
|
|
330
|
+
}
|
|
331
|
+
}
|
|
311
332
|
base.label = blob.label || blob.email || ''
|
|
312
333
|
base.email = blob.email || ''
|
|
313
334
|
base.plan = blob.plan || blob.paidTierName || ''
|
|
@@ -367,6 +388,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
367
388
|
latencyGrade: pings.get(ref)?.latencyGrade ?? null,
|
|
368
389
|
lastDoctorVerdict: doctorVerdicts.get(ref) || null,
|
|
369
390
|
availableModels: dynamicModels.get(ref)?.models || null,
|
|
391
|
+
credentialError: base.credentialError || null,
|
|
370
392
|
}
|
|
371
393
|
}
|
|
372
394
|
|
|
@@ -374,8 +396,12 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
374
396
|
const curConfig = getConfig()
|
|
375
397
|
const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
|
|
376
398
|
const out = []
|
|
399
|
+
const identities = new Map()
|
|
377
400
|
for (const slot of slots) {
|
|
378
401
|
const info = await describeRef(slot.ref, curConfig)
|
|
402
|
+
const identity = accountIdentityKey(provider, { email: info.email, accountId: info.accountId })
|
|
403
|
+
const duplicateOf = identity ? (identities.get(identity) || null) : null
|
|
404
|
+
if (identity && !duplicateOf) identities.set(identity, slot.ref)
|
|
379
405
|
const q = quarantines.get(slot.ref)
|
|
380
406
|
out.push({
|
|
381
407
|
ref: slot.ref,
|
|
@@ -400,6 +426,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
400
426
|
poolEnabled: slot.poolEnabled !== false,
|
|
401
427
|
isDefault: !!slot.isDefault,
|
|
402
428
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
429
|
+
credentialError: info.credentialError || null,
|
|
430
|
+
duplicateOf,
|
|
403
431
|
})
|
|
404
432
|
}
|
|
405
433
|
return out
|
package/lib/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createSubscriptionsService } from './subscriptions.js'
|
|
|
18
18
|
import { registerSubscriptionTools } from './native-tools.js'
|
|
19
19
|
import { detectConflicts, readProfilePluginNames, setActiveConflicts } from './coexistence.js'
|
|
20
20
|
import { createAccountStore, defaultAccountRef, normalizeSlots, vendorConfig } from './accounts.js'
|
|
21
|
+
import { resolveAccountRef } from './account-reliability.js'
|
|
21
22
|
import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
|
|
22
23
|
import { createResetCreditService } from './reset-credits.js'
|
|
23
24
|
import { maskEmail, maskLabel } from './mask.js'
|
|
@@ -276,9 +277,8 @@ export function apply(ctx, rawConfig) {
|
|
|
276
277
|
|
|
277
278
|
// #85: host-only reset credit service for Codex accounts.
|
|
278
279
|
const resetCredits = createResetCreditService({ loadBlob: (ref) => store.loadBlob(ref) })
|
|
279
|
-
function refForSlot(provider,
|
|
280
|
-
|
|
281
|
-
return slot ? slot.ref : null
|
|
280
|
+
function refForSlot(provider, indexOrAlias) {
|
|
281
|
+
return resolveAccountRef(normalizeSlots(live().slots), provider, indexOrAlias)
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
function stripLegacySlots(slots) {
|
package/lib/rotate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computePacingRisk, calculateBurnRatePerHour, calculateBurnUrgency } from "./forecast.js"
|
|
2
2
|
import { isQuarantined } from './quarantine.js'
|
|
3
3
|
import { getPinnedAccountRef, pinSession, touchSessionPin, unpinSession, isCacheAwareProvider } from './session-pin.js'
|
|
4
|
+
import { accountCatalogHasModel, proactiveQuotaDecision } from './routing-reliability.js'
|
|
4
5
|
|
|
5
6
|
const SWITCH_CODES = new Set([
|
|
6
7
|
'RATE_LIMIT',
|
|
@@ -159,7 +160,9 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
159
160
|
const pinnedRef = getPinnedAccountRef(targetSession, now)
|
|
160
161
|
if (pinnedRef) {
|
|
161
162
|
const pinned = list.find((a) => (a.ref || a.id) === pinnedRef)
|
|
162
|
-
const pinnedBlock = pinned
|
|
163
|
+
const pinnedBlock = pinned
|
|
164
|
+
? (poolBlockReason(pinned, opts) || (!accountCatalogHasModel(pinned, opts && opts.model) ? 'model_not_in_account_catalog' : null))
|
|
165
|
+
: null
|
|
163
166
|
if (pinned && pinnedBlock) {
|
|
164
167
|
unpinSession(targetSession, pinnedBlock)
|
|
165
168
|
evaluations.push({
|
|
@@ -248,6 +251,17 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
248
251
|
evaluations.push({ ref, eligible: false, tier: null, reason: blockedByPool })
|
|
249
252
|
continue
|
|
250
253
|
}
|
|
254
|
+
if (!accountCatalogHasModel(acc, opts && opts.model)) {
|
|
255
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'model_not_in_account_catalog', model: opts && opts.model })
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
if (opts && opts.proactiveGuard === true) {
|
|
259
|
+
const proactive = proactiveQuotaDecision(acc.quota, now, opts.proactiveMinRemaining)
|
|
260
|
+
if (proactive.block) {
|
|
261
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: proactive.reason, until: proactive.until })
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
}
|
|
251
265
|
|
|
252
266
|
if (!opts || opts.autoPacing !== false) {
|
|
253
267
|
if (acc.pacingRisk === undefined) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROMPT_CACHE_TTL_MS = 5 * 60 * 1000
|
|
4
|
+
export const MIN_CONNECT_ATTEMPT_TIMEOUT_MS = 1500
|
|
5
|
+
|
|
6
|
+
function text(value) {
|
|
7
|
+
return value == null ? '' : String(value).trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** #810: cache usefulness score for an account (higher = warmer cache). */
|
|
11
|
+
export function promptCacheAffinity(account, nowMs = Date.now(), ttlMs = DEFAULT_PROMPT_CACHE_TTL_MS) {
|
|
12
|
+
if (!account) return { score: 0, state: 'cache_expired' }
|
|
13
|
+
const at = Number(account.lastUsedAt || account.cacheAt || 0)
|
|
14
|
+
if (!at) return { score: 0, state: 'cache_expired' }
|
|
15
|
+
const ttl = Number(ttlMs) > 0 ? Number(ttlMs) : 0
|
|
16
|
+
if (!ttl) return { score: 0, state: 'cache_off' }
|
|
17
|
+
const age = Math.max(0, Number(nowMs) - at)
|
|
18
|
+
if (age >= ttl) return { score: 0, state: 'cache_expired', ageMs: age }
|
|
19
|
+
return { score: 1 - age / ttl, state: 'cache_affinity', ageMs: age }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** #813: a model may only route to accounts whose catalog contains it. */
|
|
23
|
+
export function accountCatalogHasModel(account, model) {
|
|
24
|
+
if (!Array.isArray(account?.availableModels) || !account.availableModels.length) return true
|
|
25
|
+
const wanted = text(model).toLowerCase()
|
|
26
|
+
if (!wanted) return true
|
|
27
|
+
return account.availableModels.some((entry) => {
|
|
28
|
+
const id = typeof entry === 'string' ? entry : (entry && (entry.id || entry.slug || entry.name))
|
|
29
|
+
return text(id).toLowerCase() === wanted
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** #820: never lower a larger host value; raise tiny per-address attempts. */
|
|
34
|
+
export function effectiveConnectAttemptTimeout(hostValue, minimum = MIN_CONNECT_ATTEMPT_TIMEOUT_MS) {
|
|
35
|
+
const host = Number(hostValue)
|
|
36
|
+
const min = Number(minimum) > 0 ? Number(minimum) : MIN_CONNECT_ATTEMPT_TIMEOUT_MS
|
|
37
|
+
if (!Number.isFinite(host) || host <= 0) return min
|
|
38
|
+
return Math.max(host, min)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** #824: bind opaque provider replay state to provider/account/model. */
|
|
42
|
+
export function replayStateKey(provider, accountRef, model) {
|
|
43
|
+
return createHash('sha256')
|
|
44
|
+
.update(`${text(provider).toLowerCase()}|${text(accountRef)}|${text(model)}`)
|
|
45
|
+
.digest('hex')
|
|
46
|
+
.slice(0, 24)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function wrapReplayState(state, key, version = 1) {
|
|
50
|
+
if (state == null || !key) return state
|
|
51
|
+
return { kind: 'dsh-subscriptions.replay', version, key, state }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function unwrapReplayState(envelope, key) {
|
|
55
|
+
if (!envelope || typeof envelope !== 'object' || envelope.kind !== 'dsh-subscriptions.replay') {
|
|
56
|
+
return { state: envelope, legacy: true, matched: true }
|
|
57
|
+
}
|
|
58
|
+
return { state: envelope.key === key ? envelope.state : undefined, legacy: false, matched: envelope.key === key }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** #792: proactive guard for near-zero remaining quota. */
|
|
62
|
+
export function proactiveQuotaDecision(quota, nowMs = Date.now(), minRemaining = 1) {
|
|
63
|
+
if (!quota) return { block: false, reason: null }
|
|
64
|
+
const resetAt = Number(quota.resetAt || 0)
|
|
65
|
+
if (resetAt && resetAt <= nowMs) return { block: false, reason: null }
|
|
66
|
+
const remaining = quota.remaining != null ? Number(quota.remaining) : null
|
|
67
|
+
const remainingPercent = quota.remainingPercent != null ? Number(quota.remainingPercent) : null
|
|
68
|
+
if (remaining != null && remaining <= Number(minRemaining)) return { block: true, reason: 'remaining_low', until: resetAt || null }
|
|
69
|
+
if (remainingPercent != null && remainingPercent <= 0) return { block: true, reason: 'remaining_zero', until: resetAt || null }
|
|
70
|
+
return { block: false, reason: null }
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.33",
|
|
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",
|