@goodandready/dsh-subscriptions 0.7.48 → 0.7.50

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.
@@ -69,6 +69,10 @@ export function createUsageLoader({ load } = {}) {
69
69
  }
70
70
  }
71
71
 
72
+ export function supportsFastTier(model) {
73
+ return /(?:^|[-_.])(gpt-5|gpt-6|codex|o3|o4)(?:[-_.]|$)/i.test(String(model || ''))
74
+ }
75
+
72
76
  /** #802: explicit child choice > subagent default > session choice > global. */
73
77
  export function resolveSpeed({ childSpeed, sessionSpeed, subagentSpeed, modelSupportsFast = true } = {}) {
74
78
  const pick = [childSpeed, subagentSpeed, sessionSpeed].find((value) => value === 'standard' || value === 'fast')
package/lib/adapter.js CHANGED
@@ -6,7 +6,7 @@ import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
6
6
  import { displayName } from './refs.js'
7
7
  import { applyModelSettings, bestAccountPlan } from './model-settings.js'
8
8
  import { getSpeedMode } from './speed-mode.js'
9
- import { resolveSpeed } from './account-features.js'
9
+ import { resolveSpeed, supportsFastTier } from './account-features.js'
10
10
  import { getVendor } from './vendors/index.js'
11
11
  import { modelCatalog } from './messages.js'
12
12
  import { streamWithRotation } from './stream-rotate.js'
@@ -204,13 +204,17 @@ export class SubscriptionAdapter extends LlmAdapter {
204
204
  }
205
205
  const poolEnabled = typeof deps.poolEnabled === 'function' ? deps.poolEnabled() !== false : true
206
206
  const defaultRef = typeof deps.defaultAccountRef === 'function' ? deps.defaultAccountRef(provider) : null
207
- const streamOptions = {
208
- ...hydratedOptions,
209
- ...(provider === 'codex' && resolveSpeed({
207
+ const effectiveSpeed = provider === 'codex'
208
+ ? resolveSpeed({
210
209
  childSpeed: options.speed,
211
210
  sessionSpeed: getSpeedMode(sessionId),
212
211
  subagentSpeed: (options.subagent || options.parentSessionId) && typeof deps.subagentSpeed === 'function' ? deps.subagentSpeed() : undefined,
213
- }) === 'fast' ? { service_tier: 'priority' } : {}),
212
+ modelSupportsFast: supportsFastTier(options.model),
213
+ })
214
+ : 'standard'
215
+ const streamOptions = {
216
+ ...hydratedOptions,
217
+ ...(provider === 'codex' && effectiveSpeed === 'fast' ? { service_tier: 'priority' } : {}),
214
218
  ...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
215
219
  ...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
216
220
  // #830: kill switch. With no default account, use a sentinel that
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
@@ -1089,6 +1089,7 @@ window.__ModuleLoader__.load({
1089
1089
  const ProviderPacksPanel = lazyPanel('ProviderPacksPanel')
1090
1090
  const SourcesPanel = lazyPanel('SourcesPanel')
1091
1091
  const AccountsDashboardPanel = lazyPanel('AccountsDashboardPanel')
1092
+ const SpeedToggle = lazyPanel('SpeedToggle')
1092
1093
 
1093
1094
  // #839 model-list helpers
1094
1095
  function modelListSearchText(model) {
@@ -3900,6 +3901,17 @@ h('div', { className: 'dsub-row' },
3900
3901
  }, 'dsh-subscriptions: locales')
3901
3902
  setT(ctx.locale.bind(NS))
3902
3903
 
3904
+ ctx.effect(() => {
3905
+ if (!ctx.slots || typeof ctx.slots.inject !== 'function') return () => {}
3906
+ return ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
3907
+ name: 'conversation.input.right',
3908
+ id: 'subscriptions-speed',
3909
+ order: 50,
3910
+ locale: NS,
3911
+ inject: () => ({}),
3912
+ }, SpeedToggle))
3913
+ }, 'dsh-subscriptions: composer speed toggle')
3914
+
3903
3915
  ctx.effect(() => {
3904
3916
  const cleanups = []
3905
3917
  const registerLegacySeats = () => {
@@ -280,8 +280,8 @@ window.__ModuleLoader__.load({
280
280
  h('div', { className: 'dsub-row' },
281
281
  h('span', { className: 'dsub-h' }, '#' + row.index + ' ' + (row.label || row.email || row.ref)),
282
282
  h('span', { className: 'dsub-pillTag' }, row.quarantineUntil > Date.now() ? t('statusQuarantined') : (row.cooldownUntil > Date.now() ? t('statusCooling') : t('statusHealthy')))),
283
- h('div', { className: 'dsub-sub' }, t('quotaUsed') + ': ' + (row.usagePercent == null ? '—' : row.usagePercent + '%') + ' · ' + t('requests') + ' ' + (row.requests || 0) + ' · ' + t('errors') + ' ' + (row.errors || 0)),
284
- h('div', { className: 'dsub-row' },
283
+ compact ? null : h('div', { className: 'dsub-sub' }, t('quotaUsed') + ': ' + (row.usagePercent == null ? '—' : row.usagePercent + '%') + ' · ' + t('requests') + ' ' + (row.requests || 0) + ' · ' + t('errors') + ' ' + (row.errors || 0)),
284
+ compact ? null : h('div', { className: 'dsub-row' },
285
285
  h('button', { type: 'button', className: 'dsub-mini', onClick: () => loadUsage(row) }, u && u.loading ? '…' : t('loadUsage')),
286
286
  u && u.error ? h('span', { className: 'dsub-bad' }, u.error) : null,
287
287
  u && u.value ? h('span', { className: 'dsub-dim' }, JSON.stringify(u.value).slice(0, 180)) : null),
@@ -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
- ((analytics && analytics.hourly) || []).slice(-24).map((hour, i) => h('span', {
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: (hour && hour.requests || 0) + ' req',
298
- style: { display: 'inline-block', width: 8, height: 18, background: 'rgba(88,166,255,' + Math.min(1, ((hour && hour.requests) || 0) / 10 + 0.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')),
@@ -337,6 +338,33 @@ window.__ModuleLoader__.load({
337
338
  )
338
339
  }
339
340
 
340
- return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel, AccountsDashboardPanel }
341
+ // #802: composer Speed toggle, scoped to the current session.
342
+ function SpeedToggle(props) {
343
+ const sessionId = (props && (props.sessionId || (props.session && props.session.id))) || 'default'
344
+ const [mode, setMode] = React.useState('standard')
345
+ React.useEffect(() => {
346
+ fetch('/dsh-subscriptions/speed?sessionId=' + encodeURIComponent(sessionId), { cache: 'no-store' })
347
+ .then((res) => res.json())
348
+ .then((out) => setMode((out && out.mode) || 'standard'))
349
+ .catch(() => {})
350
+ }, [sessionId])
351
+ const toggle = () => {
352
+ const next = mode === 'fast' ? 'standard' : 'fast'
353
+ setMode(next)
354
+ fetch('/dsh-subscriptions/speed', {
355
+ method: 'POST',
356
+ headers: { 'content-type': 'application/json' },
357
+ body: JSON.stringify({ sessionId, mode: next }),
358
+ }).then((res) => res.json()).then((out) => { if (out && out.mode) setMode(out.mode) }).catch(() => {})
359
+ }
360
+ return h('button', {
361
+ type: 'button',
362
+ className: 'dsub-mini',
363
+ title: 'Codex Speed',
364
+ onClick: toggle,
365
+ }, mode === 'fast' ? '⚡ FAST' : 'Speed')
366
+ }
367
+
368
+ return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel, AccountsDashboardPanel, SpeedToggle }
341
369
  }
342
370
  })
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
 
@@ -44,18 +44,26 @@ function outputDir(exec) {
44
44
  return path.join(cwd, 'generated', 'subscriptions')
45
45
  }
46
46
 
47
- async function saveImages(images, exec) {
47
+ async function saveImages(images, exec, attachments) {
48
48
  const dir = outputDir(exec)
49
49
  await mkdir(dir, { recursive: true })
50
50
  const out = []
51
+ const stored = []
51
52
  for (let i = 0; i < images.length; i += 1) {
52
53
  const image = images[i]
53
54
  if (!image || typeof image.b64_json !== 'string') continue
55
+ const data = Buffer.from(image.b64_json, 'base64')
54
56
  const file = path.join(dir, `image-${Date.now()}-${i}.png`)
55
- await writeFile(file, Buffer.from(image.b64_json, 'base64'))
57
+ await writeFile(file, data)
56
58
  out.push(file)
59
+ if (attachments && typeof attachments.saveImages === 'function') {
60
+ try {
61
+ const refs = await attachments.saveImages([{ data, mediaType: 'image/png', name: `image-${i + 1}.png` }])
62
+ for (const ref of refs || []) stored.push(ref)
63
+ } catch { /* file output still works */ }
64
+ }
57
65
  }
58
- return out
66
+ return { files: out, attachments: stored }
59
67
  }
60
68
 
61
69
  export function registerSubscriptionTools(ctx, deps, options = {}) {
@@ -109,9 +117,11 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
109
117
  referenceImages: references.refs,
110
118
  signal: exec && exec.signal,
111
119
  })
112
- const files = await saveImages(rows, exec)
113
- if (!files.length) throw new Error('image response contained no images')
114
- return textResult(files.map((file) => `Generated image: ${file}`).join('\n'))
120
+ const saved = await saveImages(rows, exec, deps.attachments && deps.attachments())
121
+ if (!saved.files.length) throw new Error('image response contained no images')
122
+ const lines = saved.files.map((file) => `Generated image: ${file}`)
123
+ for (const ref of saved.attachments) lines.push(`Attachment: ${ref}`)
124
+ return textResult(lines.join('\n'))
115
125
  } catch (err) {
116
126
  lastError = err
117
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.48",
3
+ "version": "0.7.50",
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",