@goodandready/dsh-subscriptions 0.6.39 → 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 {
@@ -63,7 +63,8 @@ export function createAdapterManager({
63
63
  const cfg = live()
64
64
  if (!cfg.cascadingFallback) throw err
65
65
  const logged = await store.loggedInProviders()
66
- const available = Object.keys(logged || {}).filter((p) => logged[p] && p !== provider)
66
+ const loggedList = Array.isArray(logged) ? logged : Object.keys(logged || {}).filter((p) => logged[p])
67
+ const available = loggedList.filter((p) => p !== provider)
67
68
  visited.add(provider)
68
69
  const nextVendor = resolveFallbackVendor(provider, available, cfg.cascadingChain, visited)
69
70
  if (!nextVendor) throw err
@@ -280,8 +281,9 @@ export function createAdapterManager({
280
281
  // #75: eager usage refresh at startup so the windows (5h/7d) land in the blob immediately
281
282
  const eager = async () => {
282
283
  try {
283
- for (const slot of normalizeSlots(live().slots)) {
284
- 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) => {
285
287
  if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
286
288
  })
287
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
 
@@ -132,8 +132,70 @@ export class SubscriptionAdapter extends LlmAdapter {
132
132
  const t0 = Date.now()
133
133
  let firstChunkTime = null
134
134
  let outputTokens = 0
135
+ let inputTokens = 0
136
+ let cacheReadTokens = 0
137
+ let cacheWriteTokens = 0
135
138
  let approxChars = 0
136
- 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
+
137
199
  const vendor = getVendor(provider)
138
200
  // ponytail: capture x-ratelimit headers without touching vendors
139
201
  const baseFetch = pickFetch(deps, account.ref)
@@ -186,7 +248,9 @@ export class SubscriptionAdapter extends LlmAdapter {
186
248
  }
187
249
  return res
188
250
  }
251
+
189
252
  try {
253
+ blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
190
254
  if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
191
255
  for await (const chunk of vendor.streamOnce({
192
256
  blob,
@@ -201,54 +265,38 @@ export class SubscriptionAdapter extends LlmAdapter {
201
265
  firstChunkTime = Date.now()
202
266
  }
203
267
  if (chunk && typeof chunk === 'object') {
204
- 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) {
205
272
  const u = toTokenUsage(chunk.usage)
206
273
  if (u && typeof u.outputTokens === 'number' && u.outputTokens > 0) {
207
274
  outputTokens = u.outputTokens
208
275
  }
276
+ if (u && typeof u.inputTokens === 'number') {
277
+ inputTokens = u.inputTokens
278
+ }
279
+ if (u && typeof u.cacheReadTokens === 'number') {
280
+ cacheReadTokens = u.cacheReadTokens
281
+ }
282
+ if (u && typeof u.cacheWriteTokens === 'number') {
283
+ cacheWriteTokens = u.cacheWriteTokens
284
+ }
209
285
  } else if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
210
286
  approxChars += chunk.text.length
211
287
  }
212
288
  }
213
289
  yield chunk
214
290
  }
215
- if (!outputTokens && approxChars > 0) {
216
- outputTokens = Math.max(1, Math.round(approxChars / 4))
217
- }
218
- // stream completed successfully: clear cooldown and health penalties
219
- if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
220
- // #65: record in the request and cost history.
221
- if (typeof deps.recordHistory === 'function') {
222
- try {
223
- const totalMs = Date.now() - t0
224
- const ttftMs = firstChunkTime !== null ? Math.max(0, firstChunkTime - t0) : null
225
- const genMs = (firstChunkTime !== null && totalMs > ttftMs) ? (totalMs - ttftMs) : 0
226
- const tps = (outputTokens > 0 && genMs > 0)
227
- ? Math.round((outputTokens / (genMs / 1000)) * 10) / 10
228
- : null
229
-
230
- deps.recordHistory({
231
- provider,
232
- ref: account.ref,
233
- model: (opts && opts.model) || null,
234
- path: '/responses',
235
- method: 'POST',
236
- status: 200,
237
- ms: totalMs,
238
- ttftMs,
239
- tps,
240
- outputTokens: outputTokens || undefined,
241
- kind: 'stream',
242
- })
243
- } catch { /* ignore quota capture failure */ }
244
- }
291
+ completed = true
245
292
  } catch (err) {
293
+ errorCaught = err
246
294
  if (err && (err.code === 'QUOTA' || err.code === 'QUOTA_EXCEEDED' || err.status === 429)) {
247
295
  if (typeof deps.recordExhaust === 'function') deps.recordExhaust(account.ref)
248
296
  } else if (err && (err.status === 401 || err.status === 403 || err.code === 'TOKEN_REVOKED' || err.code === 'BROKEN')) {
249
297
  if (typeof deps.recordBroken === 'function') deps.recordBroken(account.ref)
250
298
  }
251
- if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
299
+ if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl && blob) {
252
300
  await deps.saveBlob(account.ref, {
253
301
  ...blob,
254
302
  validationUrl: err.validationUrl,
@@ -256,6 +304,18 @@ export class SubscriptionAdapter extends LlmAdapter {
256
304
  })
257
305
  }
258
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
+ }
259
319
  }
260
320
  },
261
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
  }
package/lib/images.js CHANGED
@@ -205,25 +205,48 @@ export function toOpenAiImage(norm) {
205
205
  }
206
206
 
207
207
  export function toAnthropicImage(norm) {
208
- if (!norm.base64) return null
209
- return {
210
- type: 'image',
211
- source: {
212
- type: 'base64',
213
- media_type: norm.mimeType || 'image/jpeg',
214
- data: norm.base64,
215
- },
208
+ if (!norm) return null
209
+ if (norm.base64) {
210
+ return {
211
+ type: 'image',
212
+ source: {
213
+ type: 'base64',
214
+ media_type: norm.mimeType || 'image/jpeg',
215
+ data: norm.base64,
216
+ },
217
+ }
216
218
  }
219
+ if (norm.url) {
220
+ return {
221
+ type: 'image',
222
+ source: {
223
+ type: 'url',
224
+ url: norm.url,
225
+ },
226
+ }
227
+ }
228
+ return null
217
229
  }
218
230
 
219
231
  export function toGeminiImage(norm) {
220
- if (!norm.base64) return null
221
- return {
222
- inlineData: {
223
- mimeType: norm.mimeType || 'image/jpeg',
224
- data: norm.base64,
225
- },
232
+ if (!norm) return null
233
+ if (norm.base64) {
234
+ return {
235
+ inlineData: {
236
+ mimeType: norm.mimeType || 'image/jpeg',
237
+ data: norm.base64,
238
+ },
239
+ }
226
240
  }
241
+ if (norm.url) {
242
+ return {
243
+ fileData: {
244
+ mimeType: norm.mimeType || 'image/jpeg',
245
+ fileUri: norm.url,
246
+ },
247
+ }
248
+ }
249
+ return null
227
250
  }
228
251
 
229
252
  export function toCodexImage(norm) {
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@ import { HistoryStore, resolveHistoryDir } from './history.js'
22
22
  import { createDiagnosticsReport } from './diagnostics.js'
23
23
  import { createAccountsView } from './accounts-view.js'
24
24
  import { createAdapterManager } from './adapter-manager.js'
25
+ import { pickAccount } from './rotate.js'
25
26
 
26
27
  export { observeForecast, estimateForecast } from './forecast.js'
27
28
  export { formatRelativeReset } from './relative-time.js'
@@ -328,7 +329,8 @@ export function apply(ctx, rawConfig) {
328
329
  ctx.effect(() => ctx.provide('subscriptionImages', {
329
330
  async available() {
330
331
  const logged = await store.loggedInProviders()
331
- return ['codex', 'grok'].filter((name) => logged && logged[name])
332
+ const loggedList = Array.isArray(logged) ? logged : Object.keys(logged || {}).filter((name) => logged[name])
333
+ return ['codex', 'grok'].filter((name) => loggedList.includes(name))
332
334
  },
333
335
  sizes: IMAGE_SIZES,
334
336
  async generate(request) {
@@ -337,18 +339,20 @@ export function apply(ctx, rawConfig) {
337
339
  throw new Error(`unknown subscription provider: ${provider}`)
338
340
  }
339
341
  const accounts = await store.listAccounts(provider)
340
- const slot = (accounts || []).find((row) => row && row.ref)
342
+ const eligible = pickAccount(accounts || [], Date.now(), { family: provider === 'codex' ? 'gpt' : 'grok' })
343
+ const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken)
341
344
  if (!slot) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
342
345
  const raw = await store.resolveRaw(slot.ref)
343
346
  if (!raw) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
344
347
  const session = await store.ensureFresh(provider, parseBlob(raw), slot.ref)
348
+ const fetchFn = (typeof fetchForRef === 'function' && fetchForRef(slot.ref)) || fetch
345
349
  return generateOnce({
346
350
  provider,
347
351
  prompt: request.prompt,
348
352
  size: request.size,
349
353
  quality: request.quality,
350
354
  session,
351
- fetchImpl: fetch,
355
+ fetchImpl: fetchFn,
352
356
  signal: request.signal,
353
357
  })
354
358
  },
@@ -170,6 +170,6 @@ export async function* streamResponses(body) {
170
170
  if (translator.terminated) return
171
171
  }
172
172
  if (!translator.terminated) {
173
- yield { type: 'finish', reason: { kind: 'stop' } }
173
+ yield { type: 'finish', reason: { kind: 'error', failure: { code: 'INCOMPLETE', message: 'stream terminated before response.completed' } } }
174
174
  }
175
175
  }
package/lib/rotate.js CHANGED
@@ -44,7 +44,7 @@ export function cooldownBlocks(acc, family) {
44
44
  export function pickAccount(accounts, nowMs, opts) {
45
45
  const list = Array.isArray(accounts) ? accounts : []
46
46
  const now = Number(nowMs) || 0
47
- const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0.01
47
+ const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0
48
48
  const thr = Number.isFinite(thrRaw) ? thrRaw : 0
49
49
  const isVipRequest = Boolean(opts && opts.vip)
50
50
  const targetTag = opts && opts.tag
@@ -115,8 +115,7 @@ export function pickAccount(accounts, nowMs, opts) {
115
115
 
116
116
  const isCooldown = acc.cooldownUntil && Number(acc.cooldownUntil) > now && cooldownBlocks(acc, opts && opts.family)
117
117
  const inQuarantine = isQuarantined(acc, now)
118
- const isHardExhausted = (acc.quota && acc.quota.remaining != null && Number(acc.quota.remaining) <= 0 && acc.quota.resetAt && Number(acc.quota.resetAt) > now) ||
119
- (acc.usagePercent != null && Number(acc.usagePercent) >= 100 && (!acc.quota || !acc.quota.resetAt || Number(acc.quota.resetAt) > now))
118
+ const isHardExhausted = (acc.usagePercent != null && Number(acc.usagePercent) >= 100 && (!acc.quota || !acc.quota.resetAt || Number(acc.quota.resetAt) > now))
120
119
 
121
120
  if (isCooldown || inQuarantine || isHardExhausted) {
122
121
  continue
@@ -53,7 +53,7 @@ export function registerProxyRoutes(ctx, state) {
53
53
  const url = new URL(req.url || '/', 'http://localhost')
54
54
  const parts = url.pathname.replace(/^\/dsh-subscriptions\/proxy\//, '').split('/').filter(Boolean)
55
55
  const provider = parts[0]
56
- const restPath = '/' + parts.slice(1).join('/')
56
+ const restPath = '/' + parts.slice(1).join('/') + (url.search || '')
57
57
  if (!isProvider(provider)) {
58
58
  writeJson(res, 404, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
59
59
  return