@goodandready/dsh-subscriptions 0.7.32 → 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/rotate.js +15 -1
- package/lib/routing-reliability.js +71 -0
- package/package.json +1 -1
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",
|