@goodandready/dsh-subscriptions 0.7.43 → 0.7.44

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.
@@ -0,0 +1,133 @@
1
+ function percentile(values, p) {
2
+ const list = (Array.isArray(values) ? values : []).filter(Number.isFinite).sort((a, b) => a - b)
3
+ if (!list.length) return null
4
+ return list[Math.min(list.length - 1, Math.max(0, Math.ceil((p / 100) * list.length) - 1))]
5
+ }
6
+
7
+ /** #796: independent picker entries are bound to the stable credential ref. */
8
+ export function buildIndependentEntries(accounts, modelsByProvider = {}) {
9
+ const out = []
10
+ for (const account of Array.isArray(accounts) ? accounts : []) {
11
+ if (!account || account.independentEntry !== true || account.hasToken === false) continue
12
+ const alias = String(account.independentAlias || account.label || account.ref || '').trim()
13
+ for (const model of modelsByProvider[account.provider] || []) {
14
+ const id = typeof model === 'string' ? model : model && (model.id || model.slug)
15
+ if (!id) continue
16
+ out.push({
17
+ id: 'independent:' + account.ref + ':' + id,
18
+ model: id,
19
+ provider: account.provider,
20
+ accountRef: account.ref,
21
+ alias,
22
+ })
23
+ }
24
+ }
25
+ return out
26
+ }
27
+
28
+ export function resolveIndependentEntry(entry, accounts) {
29
+ if (!entry || entry.accountRef == null) return null
30
+ const account = (Array.isArray(accounts) ? accounts : []).find((row) => row && row.ref === entry.accountRef)
31
+ if (!account || account.hasToken === false) return null
32
+ return account
33
+ }
34
+
35
+ /** #812: per-account usage loading with in-flight dedupe, stale-drop and cleanup. */
36
+ export function createUsageLoader({ load } = {}) {
37
+ const inflight = new Map()
38
+ const generation = new Map()
39
+ const state = new Map()
40
+
41
+ return {
42
+ load(key, { force = false } = {}) {
43
+ if (!force && inflight.has(key)) return inflight.get(key)
44
+ const gen = (generation.get(key) || 0) + 1
45
+ generation.set(key, gen)
46
+ state.set(key, { ...(state.get(key) || {}), loading: true, error: null })
47
+ const promise = Promise.resolve()
48
+ .then(() => load(key))
49
+ .then((value) => {
50
+ if (generation.get(key) !== gen) return state.get(key)
51
+ state.set(key, { value, loading: false, error: null })
52
+ return state.get(key)
53
+ })
54
+ .catch((error) => {
55
+ if (generation.get(key) !== gen) return state.get(key)
56
+ state.set(key, { ...(state.get(key) || {}), loading: false, error: String(error && error.message || error) })
57
+ return state.get(key)
58
+ })
59
+ .finally(() => { if (inflight.get(key) === promise) inflight.delete(key) })
60
+ inflight.set(key, promise)
61
+ return promise
62
+ },
63
+ get(key) { return state.get(key) || null },
64
+ clearMissing(validKeys) {
65
+ const keep = new Set(validKeys || [])
66
+ for (const key of [...state.keys()]) if (!keep.has(key)) state.delete(key)
67
+ for (const key of [...inflight.keys()]) if (!keep.has(key)) inflight.delete(key)
68
+ },
69
+ }
70
+ }
71
+
72
+ /** #802: explicit child choice > subagent default > session choice > global. */
73
+ export function resolveSpeed({ childSpeed, sessionSpeed, subagentSpeed, modelSupportsFast = true } = {}) {
74
+ const pick = [childSpeed, subagentSpeed, sessionSpeed].find((value) => value === 'standard' || value === 'fast')
75
+ if (pick === 'fast' && !modelSupportsFast) {
76
+ const err = new Error('this model does not support fast speed')
77
+ err.code = 'FAST_UNSUPPORTED'
78
+ throw err
79
+ }
80
+ return pick || 'standard'
81
+ }
82
+
83
+ /** #789: per-provider account dashboard rows and 24h heatmap. */
84
+ export function accountDashboard(accounts, { now = Date.now(), sort = 'risk' } = {}) {
85
+ const rows = (Array.isArray(accounts) ? accounts : []).map((account) => {
86
+ const quota = account.usagePercent == null ? null : Number(account.usagePercent)
87
+ const burn = Number(account.burnRatePerHour) || 0
88
+ const runwayMinutes = quota != null && burn > 0 ? Math.max(0, Math.round(((100 - quota) / burn) * 60)) : null
89
+ const latencies = (account.latencies || []).filter(Number.isFinite)
90
+ return {
91
+ ref: account.ref,
92
+ provider: account.provider,
93
+ label: account.label || account.email || account.ref,
94
+ status: account.quarantineUntil && account.quarantineUntil > now ? 'quarantined'
95
+ : account.cooldownUntil && account.cooldownUntil > now ? 'cooling' : 'healthy',
96
+ quota,
97
+ runwayMinutes,
98
+ burnRatePerHour: burn,
99
+ requests: Number(account.requests) || 0,
100
+ errors: Number(account.errors) || 0,
101
+ p95LatencyMs: percentile(latencies, 95),
102
+ }
103
+ })
104
+ const risk = (row) => (row.quota == null ? 1000 : row.quota) + row.errors * 5
105
+ if (sort === 'name') rows.sort((a, b) => String(a.label).localeCompare(String(b.label)))
106
+ else if (sort === 'quota') rows.sort((a, b) => (a.quota ?? 1000) - (b.quota ?? 1000))
107
+ else rows.sort((a, b) => risk(b) - risk(a))
108
+ return rows
109
+ }
110
+
111
+ /** #807: searchable model editor with counters and bounded rendering. */
112
+ export function modelEditor(models, { query = '', overrides = {}, limit = 40 } = {}) {
113
+ const q = String(query || '').trim().toLowerCase()
114
+ const rows = (Array.isArray(models) ? models : []).map((model) => {
115
+ const id = typeof model === 'string' ? model : model && (model.id || model.slug)
116
+ const name = typeof model === 'string' ? model : model && model.name
117
+ const override = overrides[id] || {}
118
+ return {
119
+ id,
120
+ name: override.name || name || id,
121
+ hidden: override.hidden === true,
122
+ contextWindow: override.contextWindow || (model && model.contextWindow) || null,
123
+ reasoningEffort: override.reasoningEffort || null,
124
+ }
125
+ }).filter((row) => row.id && (!q || [row.id, row.name].some((value) => String(value || '').toLowerCase().includes(q))))
126
+ return {
127
+ rows: rows.slice(0, Math.max(0, Number(limit) || 0)),
128
+ total: rows.length,
129
+ discovered: Array.isArray(models) ? models.length : 0,
130
+ overridden: Object.keys(overrides || {}).length,
131
+ hidden: rows.filter((row) => row.hidden).length,
132
+ }
133
+ }
@@ -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
  proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
172
+ independentEntry: slot.independentEntry === true,
173
+ independentAlias: slot.independentAlias || '',
172
174
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
173
175
  credentialError: info.credentialError || null,
174
176
  duplicateOf: info.duplicateOf || null,
package/lib/accounts.js CHANGED
@@ -76,6 +76,8 @@ export function normalizeSlots(slots) {
76
76
  ...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
77
77
  ...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
78
78
  ...(Array.isArray(slot.proxyBypass) ? { proxyBypass: slot.proxyBypass.map((host) => String(host).trim()).filter(Boolean) } : {}),
79
+ ...(slot.independentEntry === true ? { independentEntry: true } : {}),
80
+ ...(slot.independentAlias ? { independentAlias: String(slot.independentAlias).trim() } : {}),
79
81
  ...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
80
82
  ...(slot.isDefault ? { isDefault: true } : {}),
81
83
  ...(Array.isArray(slot.poolModels)
@@ -433,6 +435,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
433
435
  poolEnabled: slot.poolEnabled !== false,
434
436
  isDefault: !!slot.isDefault,
435
437
  proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
438
+ independentEntry: slot.independentEntry === true,
439
+ independentAlias: slot.independentAlias || '',
436
440
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
437
441
  credentialError: info.credentialError || null,
438
442
  lastUsedAt: lastUsedAt.get(slot.ref) || 0,
@@ -22,6 +22,10 @@ export const Slot = z.object({
22
22
  .description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
23
23
  proxyBypass: z.array(z.string()).default([])
24
24
  .description('#803 Hosts/domains that bypass the per-account proxy.'),
25
+ independentEntry: z.boolean().default(false)
26
+ .description('#796 Expose this account as separate picker model rows with no fallback.'),
27
+ independentAlias: z.string().default('')
28
+ .description('#796 Display alias for independent model rows. Binding stays by credential ref.'),
25
29
  maxConcurrency: z.number().default(1)
26
30
  .description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
27
31
  monthlyCost: z.number().default(20)
@@ -101,6 +105,8 @@ export const Config = z.object({
101
105
  .description('#93 Response verbosity for Codex reasoning models: low, medium or high. Empty = protocol default.'),
102
106
  codexFastMode: z.boolean().volatile().default(false)
103
107
  .description('#92 Fast Mode for Codex: sends service_tier priority (1.5x speed billing tier) with every request.'),
108
+ codexSubagentSpeed: z.string().volatile().default('standard')
109
+ .description('#802 Default speed for subagents: standard or fast. Explicit child choice wins.'),
104
110
  composerQuota: z.string().volatile().default('off')
105
111
  .description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
106
112
  composerQuotaDisplay: z.string().volatile().default('current')
@@ -5,6 +5,7 @@ export function isDeprecatedId(id) {
5
5
  }
6
6
  import { getVendor } from './vendors/index.js'
7
7
  import { getAccountRemainingPercent } from './rotate.js'
8
+ import { buildIndependentEntries } from './account-features.js'
8
9
 
9
10
  export const MODEL_CATALOG_TTL_MS = 5 * 60 * 1000
10
11
 
@@ -303,6 +304,22 @@ export function buildEnrichedCatalog({
303
304
  pool,
304
305
  })
305
306
  }
307
+
308
+ const independent = buildIndependentEntries(
309
+ accounts.filter((account) => account && account.provider === provId),
310
+ { [provId]: rawList },
311
+ )
312
+ for (const entry of independent) {
313
+ result.push({
314
+ id: entry.id,
315
+ name: entry.alias + ' · ' + entry.model,
316
+ provider: provId,
317
+ providerName: displayName(provId),
318
+ accountRef: entry.accountRef,
319
+ independent: true,
320
+ pool,
321
+ })
322
+ }
306
323
  }
307
324
 
308
325
  return result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.43",
3
+ "version": "0.7.44",
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",