@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 +72 -47
- package/lib/adapter-manager.js +3 -2
- package/lib/adapter.js +82 -37
- package/lib/client.js +64 -11
- package/lib/history.js +12 -0
- package/lib/runaway-guard.js +8 -1
- package/lib/stream-rotate.js +12 -1
- package/lib/vendor-factory.js +3 -3
- package/lib/vendors/cody.js +1 -4
- package/lib/vendors/copilot.js +8 -4
- package/lib/vendors/ernie.js +1 -5
- package/lib/vendors/jetbrains.js +1 -5
- package/lib/vendors/perplexity.js +1 -4
- package/lib/vendors/qwen.js +1 -5
- package/lib/vendors/replit.js +1 -4
- package/lib/vendors/spark.js +1 -5
- package/package.json +1 -1
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 {
|
package/lib/adapter-manager.js
CHANGED
|
@@ -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
|
-
|
|
285
|
-
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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.
|
|
2175
|
-
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
|
|
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
|
|
2540
|
-
if (
|
|
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/runaway-guard.js
CHANGED
|
@@ -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
|
-
|
|
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: {
|
package/lib/stream-rotate.js
CHANGED
|
@@ -53,7 +53,18 @@ export async function* streamWithRotation({
|
|
|
53
53
|
|
|
54
54
|
while (true) {
|
|
55
55
|
try {
|
|
56
|
-
const
|
|
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
|
package/lib/vendor-factory.js
CHANGED
|
@@ -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(
|
|
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}`, {
|
package/lib/vendors/cody.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
}
|
package/lib/vendors/ernie.js
CHANGED
|
@@ -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(
|
|
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
|
|
package/lib/vendors/jetbrains.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
}
|
package/lib/vendors/qwen.js
CHANGED
|
@@ -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(
|
|
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
|
|
package/lib/vendors/replit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/vendors/spark.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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",
|