@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 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 ? poolBlockReason(pinned, opts) : null
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.32",
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",