@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 +72 -47
- package/lib/adapter-manager.js +5 -3
- package/lib/adapter.js +94 -34
- package/lib/client.js +64 -11
- package/lib/history.js +12 -0
- package/lib/images.js +37 -14
- package/lib/index.js +7 -3
- package/lib/responses-stream.js +1 -1
- package/lib/rotate.js +2 -3
- package/lib/routes/proxy.js +1 -1
- package/lib/runaway-guard.js +8 -1
- package/lib/sse.js +25 -7
- package/lib/stream-rotate.js +12 -1
- package/lib/tools-normalizer.js +54 -27
- 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/lib/wire.js +103 -14
- 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
|
@@ -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
|
|
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
|
-
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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/images.js
CHANGED
|
@@ -205,25 +205,48 @@ export function toOpenAiImage(norm) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
export function toAnthropicImage(norm) {
|
|
208
|
-
if (!norm
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
355
|
+
fetchImpl: fetchFn,
|
|
352
356
|
signal: request.signal,
|
|
353
357
|
})
|
|
354
358
|
},
|
package/lib/responses-stream.js
CHANGED
|
@@ -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: '
|
|
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
|
|
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.
|
|
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
|
package/lib/routes/proxy.js
CHANGED
|
@@ -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
|