@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.
- package/bin/dsh-subscriptions.mjs +65 -0
- package/lib/adapter-manager.js +3 -0
- package/lib/adapter.js +10 -2
- package/lib/alerts.js +54 -22
- package/lib/client.js +283 -9
- package/lib/coexistence.js +45 -0
- package/lib/config-schema.js +22 -0
- package/lib/index.js +24 -1
- package/lib/messages.js +1 -1
- package/lib/model-settings.js +49 -0
- package/lib/native-tools.js +220 -0
- package/lib/reset-clock.js +16 -6
- package/lib/route-report.js +28 -0
- package/lib/routes/status.js +88 -2
- package/lib/speed-mode.js +17 -0
- package/lib/stream-rotate.js +10 -2
- package/lib/subscriptions.js +1 -1
- package/lib/usage-export.js +52 -0
- package/lib/vault-sync.js +53 -15
- package/lib/vendor-pack.js +36 -0
- package/overlay.example.yml +8 -0
- package/package.json +33 -2
|
@@ -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
|
+
}
|
package/lib/adapter-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
/**
|