@goodandready/dsh-subscriptions 0.6.40 → 0.6.41

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/accounts.js CHANGED
@@ -260,59 +260,84 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
260
260
  return promise
261
261
  }
262
262
 
263
+ const providerUsageLocks = new Map()
264
+ const slotUsageLocks = new Map()
265
+
263
266
  async function refreshUsage(provider) {
264
- const curConfig = getConfig()
265
- const cfg = vendorConfig(provider, curConfig)
266
- const vendor = getVendor(provider)
267
- const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
268
- for (const slot of slots) {
269
- const last = usageFetched.get(slot.ref) || 0
270
- if (Date.now() - last < USAGE_TTL_MS) continue
271
- let raw = ''
272
- try { raw = await resolveRaw(slot.ref) } catch { continue }
273
- if (!raw) continue
274
- try {
275
- const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
276
- const snap = await vendor.usage(blob, cfg, fetchFor(slot.ref))
277
- usageFetched.set(slot.ref, Date.now())
278
- if (!snap) continue
279
- if (Number.isFinite(Number(snap.usedPercent))) {
280
- usage.set(slot.ref, Number(snap.usedPercent))
267
+ if (providerUsageLocks.has(provider)) {
268
+ return providerUsageLocks.get(provider)
269
+ }
270
+ const flight = (async () => {
271
+ const curConfig = getConfig()
272
+ const cfg = vendorConfig(provider, curConfig)
273
+ const vendor = getVendor(provider)
274
+ const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
275
+ for (const slot of slots) {
276
+ if (slotUsageLocks.has(slot.ref)) {
277
+ await slotUsageLocks.get(slot.ref).catch(() => {})
278
+ continue
281
279
  }
282
- // Notification thresholds: 70/90/100% — fire once per window until reset.
283
- if (Array.isArray(snap.windows)) {
284
- for (const win of snap.windows) {
285
- if (!win || !Number.isFinite(Number(win.usedPercent))) continue
286
- const pct = Number(win.usedPercent)
287
- const key = slot.ref + ':' + win.id
288
- const prev = notifyThresholds.get(key) || 0
289
- const THRESHOLDS = [70, 90, 100]
290
- for (const th of THRESHOLDS) {
291
- if (pct >= th && prev < th) {
292
- notifyThresholds.set(key, th)
293
- try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch { /* ignore notification failure */ }
294
- break
280
+ const last = usageFetched.get(slot.ref) || 0
281
+ if (Date.now() - last < USAGE_TTL_MS) continue
282
+ let raw = ''
283
+ try { raw = await resolveRaw(slot.ref) } catch { continue }
284
+ if (!raw) continue
285
+
286
+ const slotFlight = (async () => {
287
+ try {
288
+ usageFetched.set(slot.ref, Date.now())
289
+ const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
290
+ const snap = await vendor.usage(blob, cfg, fetchFor(slot.ref))
291
+ if (!snap) return
292
+ if (Number.isFinite(Number(snap.usedPercent))) {
293
+ usage.set(slot.ref, Number(snap.usedPercent))
294
+ }
295
+ // Notification thresholds: 70/90/100% — fire once per window until reset.
296
+ if (Array.isArray(snap.windows)) {
297
+ for (const win of snap.windows) {
298
+ if (!win || !Number.isFinite(Number(win.usedPercent))) continue
299
+ const pct = Number(win.usedPercent)
300
+ const key = slot.ref + ':' + win.id
301
+ const prev = notifyThresholds.get(key) || 0
302
+ const THRESHOLDS = [70, 90, 100]
303
+ for (const th of THRESHOLDS) {
304
+ if (pct >= th && prev < th) {
305
+ notifyThresholds.set(key, th)
306
+ try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch { /* ignore notification failure */ }
307
+ break
308
+ }
309
+ }
295
310
  }
296
311
  }
297
- }
298
- }
299
- const list = Array.isArray(snap.windows) ? snap.windows : null
300
- if (list) {
301
- windows.set(slot.ref, list)
302
- // persist last known windows so the card survives restarts
303
- try {
304
- const prev = Array.isArray(blob.usage) ? JSON.stringify(blob.usage) : ''
305
- if (prev !== JSON.stringify(list)) {
306
- await saveBlob(slot.ref, { ...blob, usage: list, usageAt: Date.now() })
307
- } else if (!blob.usageAt) {
308
- await saveBlob(slot.ref, { ...blob, usageAt: Date.now() })
312
+ const list = Array.isArray(snap.windows) ? snap.windows : null
313
+ if (list) {
314
+ windows.set(slot.ref, list)
315
+ // persist last known windows so the card survives restarts
316
+ try {
317
+ const prev = Array.isArray(blob.usage) ? JSON.stringify(blob.usage) : ''
318
+ if (prev !== JSON.stringify(list)) {
319
+ await saveBlob(slot.ref, { ...blob, usage: list, usageAt: Date.now() })
320
+ } else if (!blob.usageAt) {
321
+ await saveBlob(slot.ref, { ...blob, usageAt: Date.now() })
322
+ }
323
+ } catch { /* persistence best-effort */ }
309
324
  }
310
- } catch { /* persistence best-effort */ }
311
- }
312
- } catch {
313
- usageFetched.set(slot.ref, Date.now())
325
+ } catch {
326
+ usageFetched.set(slot.ref, Date.now())
327
+ } finally {
328
+ slotUsageLocks.delete(slot.ref)
329
+ }
330
+ })()
331
+
332
+ slotUsageLocks.set(slot.ref, slotFlight)
333
+ await slotFlight.catch(() => {})
314
334
  }
315
- }
335
+ })().finally(() => {
336
+ providerUsageLocks.delete(provider)
337
+ })
338
+
339
+ providerUsageLocks.set(provider, flight)
340
+ return flight
316
341
  }
317
342
 
318
343
  return {
@@ -281,8 +281,9 @@ export function createAdapterManager({
281
281
  // #75: eager usage refresh at startup so the windows (5h/7d) land in the blob immediately
282
282
  const eager = async () => {
283
283
  try {
284
- for (const slot of normalizeSlots(live().slots)) {
285
- store.refreshUsage(slot.provider).catch((err) => {
284
+ const uniqueProviders = new Set(normalizeSlots(live().slots).map((s) => s.provider).filter(Boolean))
285
+ for (const provider of uniqueProviders) {
286
+ store.refreshUsage(provider).catch((err) => {
286
287
  if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
287
288
  })
288
289
  }
package/lib/adapter.js CHANGED
@@ -73,7 +73,7 @@ export class SubscriptionAdapter extends LlmAdapter {
73
73
  models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
74
74
  }
75
75
  // Defensive: ensure every model has the provider field set
76
- models = models.map((m) => (m && !m.provider ? { ...m, provider } : m))
76
+ models = models.map((m) => (m ? { ...m, provider: (!m.provider || m.provider === "custom") ? provider : m.provider } : m))
77
77
  return models
78
78
  }
79
79
 
@@ -136,7 +136,66 @@ export class SubscriptionAdapter extends LlmAdapter {
136
136
  let cacheReadTokens = 0
137
137
  let cacheWriteTokens = 0
138
138
  let approxChars = 0
139
- const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
139
+ let completed = false
140
+ let sawFinish = false
141
+ let finishReason = null
142
+ let errorCaught = null
143
+ let finalized = false
144
+ let blob = null
145
+
146
+ const finalizeOnce = (outcome, extraStatus) => {
147
+ if (finalized) return
148
+ finalized = true
149
+
150
+ if (!outputTokens && approxChars > 0) {
151
+ outputTokens = Math.max(1, Math.round(approxChars / 4))
152
+ }
153
+
154
+ const totalMs = Date.now() - t0
155
+ const ttftMs = firstChunkTime !== null ? Math.max(0, firstChunkTime - t0) : null
156
+ const genMs = (firstChunkTime !== null && totalMs > ttftMs) ? (totalMs - ttftMs) : 0
157
+ const tps = (outputTokens > 0 && genMs > 0)
158
+ ? Math.round((outputTokens / (genMs / 1000)) * 10) / 10
159
+ : null
160
+
161
+ let status = 200
162
+ if (outcome === 'error') {
163
+ status = extraStatus || (errorCaught && (errorCaught.status || errorCaught.statusCode)) || (errorCaught && errorCaught.code === 'QUOTA' ? 429 : 500)
164
+ } else if (outcome === 'cancel') {
165
+ status = 499
166
+ } else if (outcome === 'runaway') {
167
+ status = 200
168
+ }
169
+
170
+ if (outcome === 'success') {
171
+ if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
172
+ }
173
+
174
+ if (typeof deps.recordHistory === 'function') {
175
+ try {
176
+ deps.recordHistory({
177
+ provider,
178
+ ref: account.ref,
179
+ model: (opts && opts.model) || null,
180
+ path: '/responses',
181
+ method: 'POST',
182
+ status,
183
+ outcome,
184
+ ms: totalMs,
185
+ ttftMs,
186
+ tps,
187
+ inputTokens: inputTokens || undefined,
188
+ outputTokens: outputTokens || undefined,
189
+ cacheReadTokens: cacheReadTokens || undefined,
190
+ cacheWriteTokens: cacheWriteTokens || undefined,
191
+ kind: 'stream',
192
+ ...(outcome === 'error' && errorCaught ? { error: errorCaught.message || String(errorCaught) } : {}),
193
+ ...(outcome === 'runaway' ? { reason: finishReason || (opts && opts._runawayState && opts._runawayState.reason) || 'runaway_loop_detected' } : {}),
194
+ })
195
+ } catch { /* ignore quota capture failure */ }
196
+ }
197
+ }
198
+
140
199
  const vendor = getVendor(provider)
141
200
  // ponytail: capture x-ratelimit headers without touching vendors
142
201
  const baseFetch = pickFetch(deps, account.ref)
@@ -189,7 +248,9 @@ export class SubscriptionAdapter extends LlmAdapter {
189
248
  }
190
249
  return res
191
250
  }
251
+
192
252
  try {
253
+ blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
193
254
  if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
194
255
  for await (const chunk of vendor.streamOnce({
195
256
  blob,
@@ -204,7 +265,10 @@ export class SubscriptionAdapter extends LlmAdapter {
204
265
  firstChunkTime = Date.now()
205
266
  }
206
267
  if (chunk && typeof chunk === 'object') {
207
- if (chunk.type === 'usage' && chunk.usage) {
268
+ if (chunk.type === 'finish') {
269
+ sawFinish = true
270
+ finishReason = chunk.reason
271
+ } else if (chunk.type === 'usage' && chunk.usage) {
208
272
  const u = toTokenUsage(chunk.usage)
209
273
  if (u && typeof u.outputTokens === 'number' && u.outputTokens > 0) {
210
274
  outputTokens = u.outputTokens
@@ -224,46 +288,15 @@ export class SubscriptionAdapter extends LlmAdapter {
224
288
  }
225
289
  yield chunk
226
290
  }
227
- if (!outputTokens && approxChars > 0) {
228
- outputTokens = Math.max(1, Math.round(approxChars / 4))
229
- }
230
- // stream completed successfully: clear cooldown and health penalties
231
- if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
232
- // #65: record in the request and cost history.
233
- if (typeof deps.recordHistory === 'function') {
234
- try {
235
- const totalMs = Date.now() - t0
236
- const ttftMs = firstChunkTime !== null ? Math.max(0, firstChunkTime - t0) : null
237
- const genMs = (firstChunkTime !== null && totalMs > ttftMs) ? (totalMs - ttftMs) : 0
238
- const tps = (outputTokens > 0 && genMs > 0)
239
- ? Math.round((outputTokens / (genMs / 1000)) * 10) / 10
240
- : null
241
-
242
- deps.recordHistory({
243
- provider,
244
- ref: account.ref,
245
- model: (opts && opts.model) || null,
246
- path: '/responses',
247
- method: 'POST',
248
- status: 200,
249
- ms: totalMs,
250
- ttftMs,
251
- tps,
252
- inputTokens: inputTokens || undefined,
253
- outputTokens: outputTokens || undefined,
254
- cacheReadTokens: cacheReadTokens || undefined,
255
- cacheWriteTokens: cacheWriteTokens || undefined,
256
- kind: 'stream',
257
- })
258
- } catch { /* ignore quota capture failure */ }
259
- }
291
+ completed = true
260
292
  } catch (err) {
293
+ errorCaught = err
261
294
  if (err && (err.code === 'QUOTA' || err.code === 'QUOTA_EXCEEDED' || err.status === 429)) {
262
295
  if (typeof deps.recordExhaust === 'function') deps.recordExhaust(account.ref)
263
296
  } else if (err && (err.status === 401 || err.status === 403 || err.code === 'TOKEN_REVOKED' || err.code === 'BROKEN')) {
264
297
  if (typeof deps.recordBroken === 'function') deps.recordBroken(account.ref)
265
298
  }
266
- if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
299
+ if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl && blob) {
267
300
  await deps.saveBlob(account.ref, {
268
301
  ...blob,
269
302
  validationUrl: err.validationUrl,
@@ -271,6 +304,18 @@ export class SubscriptionAdapter extends LlmAdapter {
271
304
  })
272
305
  }
273
306
  throw err
307
+ } finally {
308
+ if (errorCaught) {
309
+ finalizeOnce('error')
310
+ } else if (opts && opts._runawayState && opts._runawayState.tripped) {
311
+ finalizeOnce('runaway')
312
+ } else if (completed || sawFinish) {
313
+ finalizeOnce('success')
314
+ } else if (opts && opts.signal && opts.signal.aborted) {
315
+ finalizeOnce('cancel', 499)
316
+ } else {
317
+ finalizeOnce('cancel', 499)
318
+ }
274
319
  }
275
320
  },
276
321
  })) {
package/lib/client.js CHANGED
@@ -919,9 +919,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
919
919
  const items = (h && h.items) || []
920
920
  const events = items.map((it) => ({
921
921
  usage: {
922
- promptTokens: it.promptTokens || (it.tokens && it.tokens.prompt) || 0,
923
- cachedTokens: it.cachedTokens || (it.tokens && it.tokens.cached) || 0,
924
- completionTokens: it.completionTokens || (it.tokens && it.tokens.completion) || 0,
922
+ inputTokens: it.inputTokens ?? it.promptTokens ?? (it.tokens && it.tokens.prompt) ?? 0,
923
+ cacheReadTokens: it.cacheReadTokens ?? it.cachedTokens ?? (it.tokens && it.tokens.cached) ?? 0,
924
+ outputTokens: it.outputTokens ?? it.completionTokens ?? (it.tokens && it.tokens.completion) ?? 0,
925
+ promptTokens: it.inputTokens ?? it.promptTokens ?? (it.tokens && it.tokens.prompt) ?? 0,
926
+ cachedTokens: it.cacheReadTokens ?? it.cachedTokens ?? (it.tokens && it.tokens.cached) ?? 0,
927
+ completionTokens: it.outputTokens ?? it.completionTokens ?? (it.tokens && it.tokens.completion) ?? 0,
925
928
  },
926
929
  }))
927
930
  return fetch('/dsh-subscriptions/analyze-session', {
@@ -1396,6 +1399,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1396
1399
  React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.successRate || 100) + '%'),
1397
1400
  React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsSuccessRate')),
1398
1401
  ),
1402
+ React.createElement('div', { className: 'dsub-stat-box' },
1403
+ React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.cacheHitPercent || 0) + '%'),
1404
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('cacheHitRate')),
1405
+ ),
1399
1406
  React.createElement('div', { className: 'dsub-stat-box' },
1400
1407
  React.createElement('div', { className: 'dsub-stat-val' }, telemetry.lastRequestAt ? Math.round((Date.now() - telemetry.lastRequestAt) / 60000) + 'm ago' : '—'),
1401
1408
  React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLastActivity')),
@@ -2169,17 +2176,31 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2169
2176
  ))
2170
2177
  if (typeof un1 === 'function') cleanups.push(un1)
2171
2178
 
2172
- const un2 = ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
2179
+ // DSH 0.2 bundle config seat: rendered in PackageDetail when viewing package
2180
+ const unBundle = ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register(
2173
2181
  {
2174
- name: 'plugins.row.config',
2175
- key: ROW_CONFIG_KEY,
2182
+ name: 'plugins.bundle.config',
2183
+ key: PKG,
2176
2184
  locale: NS,
2177
2185
  inject: () => ({ ctx: ctx }),
2178
2186
  },
2179
2187
  PluginCard,
2180
2188
  ))
2181
- if (typeof un2 === 'function') cleanups.push(un2)
2182
-
2189
+ if (typeof unBundle === 'function') cleanups.push(unBundle)
2190
+
2191
+ // DSH 0.1/0.2 row config seats
2192
+ for (const key of [ROW_CONFIG_KEY, ROW_ID + '#' + ROW_ID, ROW_ID]) {
2193
+ const unRow = ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
2194
+ {
2195
+ name: 'plugins.row.config',
2196
+ key,
2197
+ locale: NS,
2198
+ inject: () => ({ ctx: ctx }),
2199
+ },
2200
+ PluginCard,
2201
+ ))
2202
+ if (typeof unRow === 'function') cleanups.push(unRow)
2203
+ }
2183
2204
  }
2184
2205
  } catch {
2185
2206
  // Slot system not available — expected on headless / non-web profiles.
@@ -2536,8 +2557,35 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2536
2557
  e.preventDefault()
2537
2558
  setState((s) => Object.assign({}, s, { open: false }))
2538
2559
  try {
2539
- const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
2540
- if (btn) btn.click()
2560
+ const nav = (props.ctx && props.ctx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
2561
+ if (nav && typeof nav.openBundle === 'function') {
2562
+ nav.openBundle(PKG)
2563
+ return
2564
+ }
2565
+ } catch { /* best-effort */ }
2566
+
2567
+ try {
2568
+ if (props.ctx && props.ctx.layout && typeof props.ctx.layout.selectPanel === 'function') {
2569
+ props.ctx.layout.selectPanel('plugins')
2570
+ }
2571
+ } catch { /* best-effort */ }
2572
+
2573
+ try {
2574
+ const panelBtn = document.querySelector('[data-panel-id="plugins"]') ||
2575
+ document.querySelector('button[aria-label*="Plugin" i]') ||
2576
+ document.querySelector('button[title*="Plugin" i]')
2577
+ if (panelBtn) panelBtn.click()
2578
+
2579
+ setTimeout(() => {
2580
+ try {
2581
+ const item = document.querySelector(`[data-plugin-name="${PKG}"]`) ||
2582
+ document.querySelector(`[data-plugin-item="${ROW_ID}"]`) ||
2583
+ Array.from(document.querySelectorAll('button, a, div')).find((el) =>
2584
+ el.textContent && (el.textContent.includes('dsh-subscriptions') || el.textContent.includes(t('title')))
2585
+ )
2586
+ if (item) item.click()
2587
+ } catch { /* best-effort */ }
2588
+ }, 150)
2541
2589
  } catch { /* best-effort */ }
2542
2590
  },
2543
2591
  }, t('subsOpenSettings')),
@@ -2557,7 +2605,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2557
2605
  order: 15,
2558
2606
  locale: NS,
2559
2607
  },
2560
- () => React.createElement(ErrorBoundary, null, React.createElement(SubsPill, { t })),
2608
+ () => React.createElement(ErrorBoundary, null, React.createElement(SubsPill, { t, ctx })),
2561
2609
  ),
2562
2610
  )
2563
2611
  }, 'dsh-subscriptions: subs pill')
@@ -2643,6 +2691,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2643
2691
  exports.inject = ['slots', 'locale', 'sessions']
2644
2692
  exports.apply = function apply(ctx) {
2645
2693
  setT(ctx.locale.bind(NS))
2694
+ try {
2695
+ ctx.inject(['pluginNavigation'], (navCtx) => {
2696
+ ctx.pluginNavigation = navCtx.pluginNavigation
2697
+ })
2698
+ } catch { /* optional */ }
2646
2699
  registerSettings(ctx)
2647
2700
  registerSubsPill(ctx)
2648
2701
  registerComposerQuota(ctx)
package/lib/history.js CHANGED
@@ -242,6 +242,9 @@ export class HistoryStore {
242
242
  const now = Date.now()
243
243
  const last24h = now - 24 * 60 * 60 * 1000
244
244
  let count24h = 0
245
+ let totalPromptTokens = 0
246
+ let totalCompletionTokens = 0
247
+ let totalCacheReadTokens = 0
245
248
 
246
249
  for (const r of this.rows) {
247
250
  if (r.ts >= last24h) count24h++
@@ -252,6 +255,9 @@ export class HistoryStore {
252
255
  totalMs += r.ms
253
256
  latencyCount++
254
257
  }
258
+ if (typeof r.inputTokens === 'number') totalPromptTokens += r.inputTokens
259
+ if (typeof r.outputTokens === 'number') totalCompletionTokens += r.outputTokens
260
+ if (typeof r.cacheReadTokens === 'number') totalCacheReadTokens += r.cacheReadTokens
255
261
  }
256
262
 
257
263
  const first = this.rows[0] // newest first
@@ -266,6 +272,12 @@ export class HistoryStore {
266
272
  lastRequestAt: (first && first.ts) || null,
267
273
  lastProvider: (first && first.provider) || null,
268
274
  lastModel: (first && first.model) || null,
275
+ totalPromptTokens,
276
+ totalCompletionTokens,
277
+ totalCacheReadTokens,
278
+ cacheHitPercent: totalPromptTokens > 0
279
+ ? Math.round((totalCacheReadTokens / totalPromptTokens) * 1000) / 10
280
+ : 0,
269
281
  accounts: this.accountsTelemetry(),
270
282
  }
271
283
  }
@@ -79,7 +79,7 @@ export async function* withRunawayGuard(stream, options = {}) {
79
79
  for await (const chunk of stream) {
80
80
  if (chunk && chunk.type === 'finish') {
81
81
  yield chunk
82
- return
82
+ continue
83
83
  }
84
84
 
85
85
  const text = (chunk && typeof chunk === 'object')
@@ -88,6 +88,13 @@ export async function* withRunawayGuard(stream, options = {}) {
88
88
 
89
89
  if (text && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta' || !chunk.type)) {
90
90
  if (detector.feed(text)) {
91
+ if (options && typeof options === 'object') {
92
+ options.tripped = true
93
+ options.trippedReason = detector.getReason()
94
+ if (typeof options.onRunaway === 'function') {
95
+ options.onRunaway(detector.getReason())
96
+ }
97
+ }
91
98
  yield {
92
99
  type: 'finish',
93
100
  reason: {
@@ -53,7 +53,18 @@ export async function* streamWithRotation({
53
53
 
54
54
  while (true) {
55
55
  try {
56
- const guardedStream = withRunawayGuard(streamOnce(account, options), options && options.runawayGuard)
56
+ const runawayState = options && options._runawayState ? options._runawayState : { tripped: false, reason: null }
57
+ const runawayOpts = {
58
+ ...(options && options.runawayGuard),
59
+ onRunaway: (reason) => {
60
+ runawayState.tripped = true
61
+ runawayState.reason = reason
62
+ },
63
+ }
64
+ const guardedStream = withRunawayGuard(
65
+ streamOnce(account, { ...options, _runawayState: runawayState }),
66
+ runawayOpts
67
+ )
57
68
  for await (const chunk of guardedStream) {
58
69
  firstChunkDelivered = true
59
70
  yield chunk
@@ -13,8 +13,8 @@ import { codexResponsesBody, modelCatalog } from "./messages.js"
13
13
  import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from "./wire.js"
14
14
  import { emailFromToken } from "./jwt.js"
15
15
 
16
- function defaultModels(models) {
17
- return modelCatalog("custom", (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
16
+ function defaultModels(providerId, models) {
17
+ return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
18
18
  }
19
19
 
20
20
  /**
@@ -84,7 +84,7 @@ export function createVendorFromProfile(profile) {
84
84
  return tokenBlobFromOAuth(json, { label: blob.label, email: blob.email })
85
85
  },
86
86
  async listModels(blob, cfg, fetchImpl) {
87
- const catalog = defaultModels(cfg.models || profile.models || [])
87
+ const catalog = defaultModels(id, cfg.models || profile.models || [])
88
88
  if (!profile.modelsPath) return catalog
89
89
  try {
90
90
  const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'cody'
7
6
  const CODY_API_BASE = 'https://sourcegraph.com/.api'
@@ -42,9 +41,7 @@ export async function listModels() {
42
41
 
43
42
  export async function usage(blob) {
44
43
  if (blob && (blob.accessToken || blob.token)) {
45
- const snap = asUsageSnapshot(15)
46
- if (snap) snap.plan = 'Cody Pro'
47
- return snap
44
+ return { usedPercent: null, plan: 'Cody Pro', timestamp: Date.now() }
48
45
  }
49
46
  return null
50
47
  }
@@ -2,7 +2,6 @@ import { LlmError } from "@deepseek-ai/dsh-llm"
2
2
  import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
3
3
  import { fetchWithTimeout } from "../http.js"
4
4
  import { openaiChatStream, readJson, httpError } from "../wire.js"
5
- import { asUsageSnapshot } from "../usage.js"
6
5
 
7
6
  export const id = "copilot"
8
7
 
@@ -268,9 +267,14 @@ export async function usage(blob, config, fetchImpl) {
268
267
  })
269
268
  if (!res.ok) return null
270
269
  const user = await readJson(res)
271
- const snap = asUsageSnapshot(10)
272
- if (snap) snap.plan = user && user.plan ? user.plan.name : "Copilot"
273
- return snap
270
+ const usedPercent = user && typeof user.used_percent === "number" && Number.isFinite(user.used_percent)
271
+ ? user.used_percent
272
+ : null
273
+ return {
274
+ usedPercent,
275
+ plan: user && user.plan ? (user.plan.name || String(user.plan)) : "Copilot",
276
+ timestamp: Date.now(),
277
+ }
274
278
  } catch {
275
279
  return null
276
280
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, readJson, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'ernie'
7
6
  export const ERNIE_OAUTH_URL = 'https://aip.baidubce.com/oauth/2.0/token'
@@ -51,10 +50,7 @@ async function refreshErnieToken(apiKey, secretKey, fetchImpl) {
51
50
  return readJson(res)
52
51
  }
53
52
 
54
- export async function usage(blob) {
55
- if (blob && (blob.accessToken || blob.apiKey)) {
56
- return asUsageSnapshot(20)
57
- }
53
+ export async function usage() {
58
54
  return null
59
55
  }
60
56
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'jetbrains'
7
6
  const JETBRAINS_API_BASE = 'https://api.jetbrains.ai'
@@ -42,10 +41,7 @@ export async function listModels() {
42
41
  return JETBRAINS_MODELS
43
42
  }
44
43
 
45
- export async function usage(blob) {
46
- if (blob && (blob.accessToken || blob.token)) {
47
- return asUsageSnapshot(30)
48
- }
44
+ export async function usage() {
49
45
  return null
50
46
  }
51
47
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'perplexity'
7
6
  const PERPLEXITY_API_BASE = 'https://api.perplexity.ai'
@@ -44,9 +43,7 @@ export async function listModels() {
44
43
 
45
44
  export async function usage(blob) {
46
45
  if (blob && (blob.accessToken || blob.apiKey)) {
47
- const snap = asUsageSnapshot(18)
48
- if (snap) snap.plan = 'Perplexity Pro'
49
- return snap
46
+ return { usedPercent: null, plan: 'Perplexity Pro', timestamp: Date.now() }
50
47
  }
51
48
  return null
52
49
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages, openaiTools } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'qwen'
7
6
  const QWEN_API_BASE = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
@@ -49,10 +48,7 @@ export async function listModels() {
49
48
  return QWEN_MODELS
50
49
  }
51
50
 
52
- export async function usage(blob) {
53
- if (blob && (blob.accessToken || blob.apiKey)) {
54
- return asUsageSnapshot(15)
55
- }
51
+ export async function usage() {
56
52
  return null
57
53
  }
58
54
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'replit'
7
6
  const REPLIT_API_BASE = 'https://replit.com/api/v1/ai'
@@ -37,9 +36,7 @@ export async function listModels() {
37
36
 
38
37
  export async function usage(blob) {
39
38
  if (blob && (blob.accessToken || blob.token)) {
40
- const snap = asUsageSnapshot(25)
41
- if (snap) snap.plan = 'Replit Core'
42
- return snap
39
+ return { usedPercent: null, plan: 'Replit Core', timestamp: Date.now() }
43
40
  }
44
41
  return null
45
42
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'spark'
7
6
  const SPARK_API_BASE = 'https://spark-api-open.xf-yun.com/v1'
@@ -42,10 +41,7 @@ export async function listModels() {
42
41
  return SPARK_MODELS
43
42
  }
44
43
 
45
- export async function usage(blob) {
46
- if (blob && (blob.accessToken || blob.apiKey)) {
47
- return asUsageSnapshot(12)
48
- }
44
+ export async function usage() {
49
45
  return null
50
46
  }
51
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.40",
3
+ "version": "0.6.41",
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",