@goodandready/dsh-subscriptions 0.6.91 → 0.7.3

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,65 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ export function parseArgs(argv) {
6
+ const args = Array.isArray(argv) ? argv.slice() : []
7
+ const command = args.shift() || 'status'
8
+ const options = { command, csv: false, type: 'accounts', sessionId: '', mode: 'fast', help: false }
9
+ for (let i = 0; i < args.length; i += 1) {
10
+ const arg = args[i]
11
+ if (arg === '--csv') options.csv = true
12
+ else if (arg === '--help' || arg === '-h') options.help = true
13
+ else if (arg === '--type') options.type = args[++i] || 'accounts'
14
+ else if (arg === '--session') options.sessionId = args[++i] || ''
15
+ else if (command === 'fast' && !options.sessionId) options.sessionId = arg
16
+ else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
17
+ }
18
+ return options
19
+ }
20
+
21
+ export function buildRequest(options, env = process.env) {
22
+ const base = String(env.DSH_SUBS_URL || 'http://127.0.0.1:3080/dsh-subscriptions').replace(/\/$/, '')
23
+ const headers = { Accept: 'application/json' }
24
+ if (env.DSH_AUTH_TOKEN) headers.Authorization = 'Bearer ' + env.DSH_AUTH_TOKEN
25
+ if (options.command === 'status') return { url: base + '/status', init: { method: 'GET', headers } }
26
+ if (options.command === 'usage') {
27
+ const params = new URLSearchParams({ type: options.type || 'accounts', format: options.csv ? 'csv' : 'json' })
28
+ return { url: base + '/usage?' + params.toString(), init: { method: 'GET', headers } }
29
+ }
30
+ if (options.command === 'fast') {
31
+ if (!options.sessionId) throw new Error('fast requires a session id: dsh-subscriptions fast <session> [fast|standard]')
32
+ headers['Content-Type'] = 'application/json'
33
+ return {
34
+ url: base + '/speed',
35
+ init: { method: 'POST', headers, body: JSON.stringify({ sessionId: options.sessionId, mode: options.mode || 'fast' }) },
36
+ }
37
+ }
38
+ throw new Error('unknown command: ' + options.command)
39
+ }
40
+
41
+ export async function run(argv = process.argv.slice(2), env = process.env, fetchImpl = fetch) {
42
+ const options = parseArgs(argv)
43
+ if (options.help) return { help: true }
44
+ const request = buildRequest(options, env)
45
+ const res = await fetchImpl(request.url, request.init)
46
+ const text = await res.text()
47
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
48
+ return { text, contentType: res.headers.get('content-type') || '' }
49
+ }
50
+
51
+ async function main() {
52
+ const result = await run()
53
+ if (result.help) {
54
+ process.stdout.write('Usage: dsh-subscriptions <status|usage|fast> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n')
55
+ return
56
+ }
57
+ process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
58
+ }
59
+
60
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
61
+ main().catch((err) => {
62
+ process.stderr.write(String(err && err.message || err) + '\n')
63
+ process.exitCode = 1
64
+ })
65
+ }
@@ -36,6 +36,7 @@ export function createAdapterManager({
36
36
  }
37
37
 
38
38
  const adapter = new SubscriptionAdapter({
39
+ live: () => live(),
39
40
  listAccounts: (provider) => store.listAccounts(provider),
40
41
  loadBlob: (ref) => store.loadBlob(ref),
41
42
  ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
@@ -46,6 +47,8 @@ export function createAdapterManager({
46
47
  drainSafetyPercent: () => live().drainSafetyPercent,
47
48
  switchAtRemaining: () => live().switchAtRemaining,
48
49
  rotationStrategy: () => live().rotationStrategy,
50
+ rateLimitWait: () => live().rateLimitWait,
51
+ rateLimitMaxWaitMs: () => live().rateLimitMaxWaitMs,
49
52
  enableSoftFailureSentinel: () => live().enableSoftFailureSentinel,
50
53
  rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
51
54
  rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
package/lib/adapter.js CHANGED
@@ -3,6 +3,8 @@ import { inferModelCapabilities, isDeprecatedId } from './models-catalog.js'
3
3
  import { fetchWithTimeout } from "./http.js"
4
4
  import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
5
5
  import { displayName } from './refs.js'
6
+ import { applyModelSettings, bestAccountPlan } from './model-settings.js'
7
+ import { getSpeedMode } from './speed-mode.js'
6
8
  import { getVendor } from './vendors/index.js'
7
9
  import { modelCatalog } from './messages.js'
8
10
  import { streamWithRotation } from './stream-rotate.js'
@@ -21,6 +23,9 @@ function asLlmError(err) {
21
23
  try {
22
24
  const out = new LlmError(message, code)
23
25
  if (err && err.validationUrl) out.validationUrl = err.validationUrl
26
+ if (err && err.retryAfterMs != null) out.retryAfterMs = err.retryAfterMs
27
+ if (err && err.resetAt != null) out.resetAt = err.resetAt
28
+ if (err && err.retryAfter != null) out.retryAfter = err.retryAfter
24
29
  return out
25
30
  } catch {
26
31
  return err
@@ -86,7 +91,7 @@ export class SubscriptionAdapter extends LlmAdapter {
86
91
  capabilities: Array.isArray(m.capabilities) && m.capabilities.length ? m.capabilities : caps.capabilities,
87
92
  }
88
93
  })
89
- return models
94
+ return applyModelSettings(provider, models, this.deps.live ? this.deps.live() : {}, bestAccountPlan(accounts))
90
95
  }
91
96
 
92
97
  imageRequestPricing() {
@@ -112,7 +117,7 @@ export class SubscriptionAdapter extends LlmAdapter {
112
117
  ...(reasoningMeta ? { reasoning: reasoningMeta } : {}),
113
118
  }
114
119
  }
115
- return { provider, id: model, name: model, ...(reasoningMeta ? { reasoning: reasoningMeta } : {}) }
120
+ return { provider, id: model, name: (found && found.name) || model, ...(reasoningMeta ? { reasoning: reasoningMeta } : {}), ...(found && found.contextWindow ? { context: { contextWindow: found.contextWindow } } : {}) }
116
121
  }
117
122
 
118
123
  async prepareCall(provider, model, signal) {
@@ -153,6 +158,7 @@ export class SubscriptionAdapter extends LlmAdapter {
153
158
  const decisionTrace = {}
154
159
  const streamOptions = {
155
160
  ...options,
161
+ ...(provider === 'codex' && getSpeedMode(sessionId) === 'fast' ? { service_tier: 'priority' } : {}),
156
162
  ...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
157
163
  ...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
158
164
  _decisionTrace: decisionTrace,
@@ -161,6 +167,8 @@ export class SubscriptionAdapter extends LlmAdapter {
161
167
  drainSafetyPercent: effectiveDrainSafetyPercent,
162
168
  rotationStrategy: effectiveRotationStrategy,
163
169
  enableSoftFailureSentinel: effectiveSoftFailureSentinel,
170
+ rateLimitWait: typeof deps.rateLimitWait === 'function' ? deps.rateLimitWait() : deps.rateLimitWait,
171
+ rateLimitMaxWaitMs: typeof deps.rateLimitMaxWaitMs === 'function' ? deps.rateLimitMaxWaitMs() : deps.rateLimitMaxWaitMs,
164
172
  }
165
173
  for await (const event of streamWithRotation({
166
174
  accounts: await deps.listAccounts(provider),
package/lib/alerts.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs'
2
+ import { createHmac } from 'node:crypto'
2
3
 
3
4
  let pkgVersion = ''
4
5
  try {
@@ -59,35 +60,66 @@ export function getRecentAlerts(limit = 50) {
59
60
  return alertBuffer.slice(-Math.max(1, Math.min(limit, MAX_ALERTS))).reverse()
60
61
  }
61
62
 
62
- /**
63
- * Dispatches an alert asynchronously to an external webhookUrl if provided.
64
- */
65
- export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch) {
63
+ // #696: signed, retrying webhook delivery plus a digest endpoint payload.
64
+ export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
66
65
  if (!alert || !webhookUrl || typeof webhookUrl !== 'string' || !webhookUrl.startsWith('http')) {
67
66
  return false
68
67
  }
68
+ return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
69
+ }
69
70
 
70
- const payload = {
71
- event: 'subscription_alert',
72
- alert,
73
- source: 'dsh-subscriptions',
74
- sentAt: new Date().toISOString(),
71
+ export function buildNotificationDigest(alerts, { windowMs = 24 * 60 * 60 * 1000, nowMs = Date.now() } = {}) {
72
+ const since = nowMs - Math.max(0, Number(windowMs) || 0)
73
+ const recent = (Array.isArray(alerts) ? alerts : []).filter((a) => a && Number(a.timestamp) >= since)
74
+ const bySeverity = {}
75
+ const byType = {}
76
+ for (const a of recent) {
77
+ bySeverity[a.severity || 'warn'] = (bySeverity[a.severity || 'warn'] || 0) + 1
78
+ byType[a.type || 'unknown'] = (byType[a.type || 'unknown'] || 0) + 1
75
79
  }
80
+ return {
81
+ total: recent.length,
82
+ bySeverity,
83
+ byType,
84
+ since: new Date(since).toISOString(),
85
+ until: new Date(nowMs).toISOString(),
86
+ }
87
+ }
76
88
 
77
- try {
78
- const res = await fetchImpl(webhookUrl, {
79
- method: 'POST',
80
- headers: {
81
- 'content-type': 'application/json',
82
- 'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
83
- },
84
- body: JSON.stringify(payload),
85
- })
86
- return res.ok
87
- } catch {
88
- // Non-blocking fire-and-forget
89
- return false
89
+ export async function dispatchNotificationDigest(alerts, webhookUrl, fetchImpl = fetch, options = {}) {
90
+ if (!webhookUrl) return false
91
+ const digest = buildNotificationDigest(alerts, options)
92
+ return dispatchNotification({ event: 'subscription_digest', digest }, webhookUrl, fetchImpl, options)
93
+ }
94
+
95
+ async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
96
+ const { secret = '', retries = 1, timeoutMs = 5000, sleep } = options || {}
97
+ const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
98
+ const raw = JSON.stringify(payload)
99
+ const headers = {
100
+ 'content-type': 'application/json',
101
+ 'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
102
+ }
103
+ if (secret) headers['x-dsh-signature'] = 'sha256=' + createHmac('sha256', String(secret)).update(raw).digest('hex')
104
+ headers['x-dsh-timestamp'] = payload.sentAt
105
+ const wait = typeof sleep === 'function' ? sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms))
106
+ const attempts = Math.max(0, Number(retries) || 0) + 1
107
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
108
+ try {
109
+ const init = { method: 'POST', headers, body: raw }
110
+ if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
111
+ init.signal = AbortSignal.timeout(timeoutMs)
112
+ }
113
+ const res = await fetchImpl(webhookUrl, init)
114
+ if (res && res.ok) return true
115
+ const status = Number(res && res.status) || 0
116
+ if (status && status < 500) return false
117
+ } catch {
118
+ // retry network errors
119
+ }
120
+ if (attempt + 1 < attempts) await wait(250 * (2 ** attempt))
90
121
  }
122
+ return false
91
123
  }
92
124
 
93
125
  /**