@goodandready/dsh-subscriptions 0.7.49 → 0.7.51
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/analytics.js +22 -0
- package/lib/client.js +12 -0
- package/lib/client.panels.js +25 -6
- package/lib/index.js +1 -0
- package/lib/media-video.js +48 -0
- package/lib/native-tools.js +48 -12
- package/package.json +1 -1
package/lib/analytics.js
CHANGED
|
@@ -342,6 +342,27 @@ export function calculateQuotaForecast(accounts = [], entries = [], nowMs = Date
|
|
|
342
342
|
return forecast
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/** #789: 24h request heatmap grouped by account ref and model. */
|
|
346
|
+
export function calculateAccountModelHeatmap(entries = [], nowMs = Date.now()) {
|
|
347
|
+
const ONE_HOUR = 3600000
|
|
348
|
+
const cutoff = nowMs - 24 * ONE_HOUR
|
|
349
|
+
const rows = new Map()
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (!entry || !entry.ts || entry.ts < cutoff || entry.ts > nowMs) continue
|
|
352
|
+
const ref = String(entry.ref || entry.accountRef || entry.provider || 'unknown')
|
|
353
|
+
const model = String(entry.model || 'unknown')
|
|
354
|
+
const key = ref + '\u0000' + model
|
|
355
|
+
let row = rows.get(key)
|
|
356
|
+
if (!row) {
|
|
357
|
+
row = { ref, provider: entry.provider || '', model, hours: Array(24).fill(0) }
|
|
358
|
+
rows.set(key, row)
|
|
359
|
+
}
|
|
360
|
+
const bucket = 23 - Math.floor((nowMs - entry.ts) / ONE_HOUR)
|
|
361
|
+
if (bucket >= 0 && bucket < 24) row.hours[bucket]++
|
|
362
|
+
}
|
|
363
|
+
return [...rows.values()]
|
|
364
|
+
}
|
|
365
|
+
|
|
345
366
|
export function calculateAnalytics(entries = [], accounts = [], nowMs = Date.now()) {
|
|
346
367
|
let totalPrompt = 0, totalCompletion = 0, totalCacheRead = 0
|
|
347
368
|
for (const r of entries) {
|
|
@@ -360,6 +381,7 @@ export function calculateAnalytics(entries = [], accounts = [], nowMs = Date.now
|
|
|
360
381
|
cache: calculateCacheMetrics(entries),
|
|
361
382
|
performance: calculatePerformanceMetrics(entries),
|
|
362
383
|
hourly: calculateHourlyBuckets(entries, nowMs),
|
|
384
|
+
heatmap: calculateAccountModelHeatmap(entries, nowMs),
|
|
363
385
|
accounts: calculateAccountBreakdown(entries, accounts),
|
|
364
386
|
forecast: calculateQuotaForecast(accounts, entries, nowMs),
|
|
365
387
|
valueDelivered,
|
package/lib/client.js
CHANGED
|
@@ -1090,6 +1090,7 @@ window.__ModuleLoader__.load({
|
|
|
1090
1090
|
const SourcesPanel = lazyPanel('SourcesPanel')
|
|
1091
1091
|
const AccountsDashboardPanel = lazyPanel('AccountsDashboardPanel')
|
|
1092
1092
|
const SpeedToggle = lazyPanel('SpeedToggle')
|
|
1093
|
+
const GeneratedMediaRow = lazyPanel('GeneratedMediaRow')
|
|
1093
1094
|
|
|
1094
1095
|
// #839 model-list helpers
|
|
1095
1096
|
function modelListSearchText(model) {
|
|
@@ -3912,6 +3913,17 @@ h('div', { className: 'dsub-row' },
|
|
|
3912
3913
|
}, SpeedToggle))
|
|
3913
3914
|
}, 'dsh-subscriptions: composer speed toggle')
|
|
3914
3915
|
|
|
3916
|
+
ctx.effect(() => {
|
|
3917
|
+
if (!ctx.slots || typeof ctx.slots.inject !== 'function') return () => {}
|
|
3918
|
+
const offs = ['image_generate', 'video_generate'].map((key) => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
3919
|
+
name: 'tool.call.toolview',
|
|
3920
|
+
key,
|
|
3921
|
+
locale: NS,
|
|
3922
|
+
children: { 'tool.call.images': { kind: 'single', scope: 'session' } },
|
|
3923
|
+
}, GeneratedMediaRow)))
|
|
3924
|
+
return () => { for (const off of offs) if (typeof off === 'function') off() }
|
|
3925
|
+
}, 'dsh-subscriptions: generated media rows')
|
|
3926
|
+
|
|
3915
3927
|
ctx.effect(() => {
|
|
3916
3928
|
const cleanups = []
|
|
3917
3929
|
const registerLegacySeats = () => {
|
package/lib/client.panels.js
CHANGED
|
@@ -291,12 +291,13 @@ window.__ModuleLoader__.load({
|
|
|
291
291
|
h('label', { className: 'dsub-sub' }, h('input', { type: 'checkbox', checked: compact, onChange: (e) => setCompact(e.target.checked) }), ' ' + t('compact')),
|
|
292
292
|
),
|
|
293
293
|
h('div', { className: 'dsub-h' }, t('heatmap')),
|
|
294
|
-
h('div', { className: 'dsub-row', style: { gap: 2 } },
|
|
295
|
-
(
|
|
294
|
+
((analytics && analytics.heatmap) || []).filter((row) => row.provider === selected).slice(0, 12).map((row) => h('div', { key: row.ref + ':' + row.model, className: 'dsub-row', style: { gap: 2 } },
|
|
295
|
+
h('span', { className: 'dsub-dim', style: { width: '130px', minWidth: '130px', overflow: 'hidden', textOverflow: 'ellipsis' } }, row.ref + ' · ' + row.model),
|
|
296
|
+
(row.hours || []).map((count, i) => h('span', {
|
|
296
297
|
key: i,
|
|
297
|
-
title:
|
|
298
|
-
style: { display: 'inline-block', width: 8, height: 18, background: 'rgba(88,166,255,' + Math.min(1, (
|
|
299
|
-
}))),
|
|
298
|
+
title: count + ' req',
|
|
299
|
+
style: { display: 'inline-block', width: 8, height: 18, background: 'rgba(88,166,255,' + Math.min(1, (count || 0) / 10 + 0.1) + ')' },
|
|
300
|
+
})))),
|
|
300
301
|
h('div', { className: 'dsub-row' },
|
|
301
302
|
h('span', { className: 'dsub-h' }, t('modelEditor') + ' · ' + t('discovered') + ' ' + models.length + ' · ' + t('shown') + ' ' + shown.length),
|
|
302
303
|
h('button', { type: 'button', className: 'dsub-mini', onClick: () => setEditing(!editing) }, t('editModels')),
|
|
@@ -364,6 +365,24 @@ window.__ModuleLoader__.load({
|
|
|
364
365
|
}, mode === 'fast' ? '⚡ FAST' : 'Speed')
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
|
|
368
|
+
// #801: inline image/video result row for generated media.
|
|
369
|
+
function GeneratedMediaRow(props) {
|
|
370
|
+
const block = (props && props.block) || {}
|
|
371
|
+
const content = Array.isArray(block.content) ? block.content : []
|
|
372
|
+
const text = content.map((part) => (part && part.text) || '').join('\n')
|
|
373
|
+
const refs = []
|
|
374
|
+
for (const match of text.matchAll(/Attachment:\s*(\S+)/g)) refs.push(match[1])
|
|
375
|
+
const urls = []
|
|
376
|
+
for (const match of text.matchAll(/https?:\/\/\S+\.(?:mp4|webm|mov)\S*/g)) urls.push(match[0])
|
|
377
|
+
return h('div', { className: 'dsub-section-card' },
|
|
378
|
+
h('div', { className: 'dsub-sub' }, text.split('\n').filter((line) => !line.startsWith('Attachment:')).join('\n').slice(0, 500)),
|
|
379
|
+
refs.length && props.renderSlot
|
|
380
|
+
? props.renderSlot('tool.call.images', { images: refs.map((attachment) => ({ attachment })) })
|
|
381
|
+
: null,
|
|
382
|
+
urls.map((url, i) => h('video', { key: i, src: url, controls: true, style: { maxWidth: '100%', marginTop: 8 } })),
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel, AccountsDashboardPanel, SpeedToggle, GeneratedMediaRow }
|
|
368
387
|
}
|
|
369
388
|
})
|
package/lib/index.js
CHANGED
|
@@ -481,6 +481,7 @@ export function apply(ctx, rawConfig) {
|
|
|
481
481
|
ctx.effect(() => registerSubscriptionTools(ctx, {
|
|
482
482
|
subscriptions,
|
|
483
483
|
images: () => ctx.subscriptionImages,
|
|
484
|
+
attachments: () => (typeof ctx.get === 'function' ? ctx.get('attachments') : null),
|
|
484
485
|
}, { enabled: live().nativeTools || process.env.DSH_SUBSCRIPTIONS_NATIVE_TOOLS === '1' }), 'dsh-subscriptions: native tools')
|
|
485
486
|
ctx.effect(() => () => clearSessionPins(), 'dsh-subscriptions: session pins cleanup')
|
|
486
487
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const ASPECTS = new Set(['16:9', '9:16', '1:1', '4:3', '3:4', '3:2', '2:3'])
|
|
2
|
+
const RESOLUTIONS = new Set(['480p', '720p', '1080p'])
|
|
3
|
+
|
|
4
|
+
/** #801: validate video generation parameters before dispatch. */
|
|
5
|
+
export function validateVideoRequest({ duration, aspectRatio, resolution, imageUrl } = {}) {
|
|
6
|
+
const seconds = duration == null ? 5 : Number(duration)
|
|
7
|
+
if (!Number.isFinite(seconds) || seconds < 1 || seconds > 15) {
|
|
8
|
+
const err = new Error('duration must be between 1 and 15 seconds')
|
|
9
|
+
err.code = 'INVALID_VIDEO_PARAMS'
|
|
10
|
+
throw err
|
|
11
|
+
}
|
|
12
|
+
if (aspectRatio != null && !ASPECTS.has(String(aspectRatio))) {
|
|
13
|
+
const err = new Error('unsupported aspect ratio')
|
|
14
|
+
err.code = 'INVALID_VIDEO_PARAMS'
|
|
15
|
+
throw err
|
|
16
|
+
}
|
|
17
|
+
if (resolution != null && !RESOLUTIONS.has(String(resolution))) {
|
|
18
|
+
const err = new Error('unsupported resolution')
|
|
19
|
+
err.code = 'INVALID_VIDEO_PARAMS'
|
|
20
|
+
throw err
|
|
21
|
+
}
|
|
22
|
+
if (imageUrl != null && !/^https?:\/\//i.test(String(imageUrl))) {
|
|
23
|
+
const err = new Error('imageUrl must be http(s)')
|
|
24
|
+
err.code = 'INVALID_VIDEO_PARAMS'
|
|
25
|
+
throw err
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
duration: seconds,
|
|
29
|
+
...(aspectRatio ? { aspectRatio: String(aspectRatio) } : {}),
|
|
30
|
+
...(resolution ? { resolution: String(resolution) } : {}),
|
|
31
|
+
...(imageUrl ? { imageUrl: String(imageUrl) } : {}),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function videoPollDelay(attempt) {
|
|
36
|
+
const n = Number(attempt) || 0
|
|
37
|
+
return Math.min(4000, 500 * 2 ** n)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extractVideoResult(payload) {
|
|
41
|
+
const body = payload && typeof payload === 'object' ? payload : {}
|
|
42
|
+
const data = body.data && typeof body.data === 'object' ? body.data : body
|
|
43
|
+
return {
|
|
44
|
+
id: data.id || data.video_id || data.request_id || null,
|
|
45
|
+
status: String(data.status || '').toLowerCase(),
|
|
46
|
+
url: data.url || data.video_url || (Array.isArray(data.output) ? data.output[0] : null),
|
|
47
|
+
}
|
|
48
|
+
}
|
package/lib/native-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { validateReferenceImages } from './media-reference.js'
|
|
4
|
+
import { validateVideoRequest, videoPollDelay, extractVideoResult } from './media-video.js'
|
|
4
5
|
|
|
5
6
|
const TOOL_OUTPUT = {
|
|
6
7
|
schema: {
|
|
@@ -44,18 +45,26 @@ function outputDir(exec) {
|
|
|
44
45
|
return path.join(cwd, 'generated', 'subscriptions')
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
async function saveImages(images, exec) {
|
|
48
|
+
async function saveImages(images, exec, attachments) {
|
|
48
49
|
const dir = outputDir(exec)
|
|
49
50
|
await mkdir(dir, { recursive: true })
|
|
50
51
|
const out = []
|
|
52
|
+
const stored = []
|
|
51
53
|
for (let i = 0; i < images.length; i += 1) {
|
|
52
54
|
const image = images[i]
|
|
53
55
|
if (!image || typeof image.b64_json !== 'string') continue
|
|
56
|
+
const data = Buffer.from(image.b64_json, 'base64')
|
|
54
57
|
const file = path.join(dir, `image-${Date.now()}-${i}.png`)
|
|
55
|
-
await writeFile(file,
|
|
58
|
+
await writeFile(file, data)
|
|
56
59
|
out.push(file)
|
|
60
|
+
if (attachments && typeof attachments.saveImages === 'function') {
|
|
61
|
+
try {
|
|
62
|
+
const refs = await attachments.saveImages([{ data, mediaType: 'image/png', name: `image-${i + 1}.png` }])
|
|
63
|
+
for (const ref of refs || []) stored.push(ref)
|
|
64
|
+
} catch { /* file output still works */ }
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
|
-
return out
|
|
67
|
+
return { files: out, attachments: stored }
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
@@ -109,9 +118,11 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
|
109
118
|
referenceImages: references.refs,
|
|
110
119
|
signal: exec && exec.signal,
|
|
111
120
|
})
|
|
112
|
-
const
|
|
113
|
-
if (!files.length) throw new Error('image response contained no images')
|
|
114
|
-
|
|
121
|
+
const saved = await saveImages(rows, exec, deps.attachments && deps.attachments())
|
|
122
|
+
if (!saved.files.length) throw new Error('image response contained no images')
|
|
123
|
+
const lines = saved.files.map((file) => `Generated image: ${file}`)
|
|
124
|
+
for (const ref of saved.attachments) lines.push(`Attachment: ${ref}`)
|
|
125
|
+
return textResult(lines.join('\n'))
|
|
115
126
|
} catch (err) {
|
|
116
127
|
lastError = err
|
|
117
128
|
}
|
|
@@ -194,11 +205,18 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
|
194
205
|
prompt: { type: 'string', description: 'Video prompt' },
|
|
195
206
|
duration: { type: 'number', description: 'Duration in seconds (1-15)' },
|
|
196
207
|
aspect_ratio: { type: 'string', description: 'e.g. 16:9, 1:1, 9:16' },
|
|
197
|
-
|
|
208
|
+
resolution: { type: 'string', enum: ['480p', '720p', '1080p'] },
|
|
209
|
+
image_url: { type: 'string', description: 'Optional source image URL (image-to-video)' },
|
|
198
210
|
},
|
|
199
211
|
required: ['prompt'],
|
|
200
212
|
},
|
|
201
213
|
async execute(args, exec) {
|
|
214
|
+
const params = validateVideoRequest({
|
|
215
|
+
duration: args && args.duration,
|
|
216
|
+
aspectRatio: args && args.aspect_ratio,
|
|
217
|
+
resolution: args && args.resolution,
|
|
218
|
+
imageUrl: args && args.image_url,
|
|
219
|
+
})
|
|
202
220
|
const res = await deps.subscriptions.request({
|
|
203
221
|
provider: 'grok',
|
|
204
222
|
path: '/v1/videos',
|
|
@@ -207,15 +225,33 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
|
207
225
|
body: {
|
|
208
226
|
model: 'grok-imagine-video-1.5',
|
|
209
227
|
prompt: args && args.prompt,
|
|
210
|
-
duration:
|
|
211
|
-
aspect_ratio:
|
|
212
|
-
|
|
228
|
+
duration: params.duration,
|
|
229
|
+
...(params.aspectRatio ? { aspect_ratio: params.aspectRatio } : {}),
|
|
230
|
+
...(params.resolution ? { resolution: params.resolution } : {}),
|
|
231
|
+
...(params.imageUrl ? { image_url: params.imageUrl } : {}),
|
|
213
232
|
},
|
|
214
233
|
signal: exec && exec.signal,
|
|
215
234
|
})
|
|
216
|
-
|
|
235
|
+
let data = await res.json().catch(() => ({}))
|
|
217
236
|
if (!res.ok) throw new Error(`video_generate HTTP ${res.status}: ${extractResponseText(data)}`)
|
|
218
|
-
|
|
237
|
+
let result = extractVideoResult(data)
|
|
238
|
+
for (let attempt = 0; result.id && !result.url && result.status !== 'failed' && attempt < 5; attempt += 1) {
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, videoPollDelay(attempt)))
|
|
240
|
+
const poll = await deps.subscriptions.request({
|
|
241
|
+
provider: 'grok',
|
|
242
|
+
path: '/v1/videos/' + encodeURIComponent(result.id),
|
|
243
|
+
method: 'GET',
|
|
244
|
+
headers: { Accept: 'application/json' },
|
|
245
|
+
signal: exec && exec.signal,
|
|
246
|
+
})
|
|
247
|
+
data = await poll.json().catch(() => ({}))
|
|
248
|
+
if (!poll.ok) break
|
|
249
|
+
result = extractVideoResult(data)
|
|
250
|
+
}
|
|
251
|
+
if (result.status === 'failed') throw new Error('video generation failed')
|
|
252
|
+
return textResult(result.url
|
|
253
|
+
? 'Video: ' + result.url
|
|
254
|
+
: `Video request submitted: ${JSON.stringify(data).slice(0, 1000)}`)
|
|
219
255
|
},
|
|
220
256
|
})
|
|
221
257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.51",
|
|
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",
|