@goodandready/dsh-subscriptions 0.6.40 → 0.6.43
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 +88 -53
- package/lib/adapter-manager.js +3 -2
- package/lib/adapter.js +90 -41
- package/lib/backoff.js +13 -1
- package/lib/client.js +64 -11
- package/lib/code-assist.js +1 -0
- package/lib/google-validation.js +1 -1
- package/lib/history.js +12 -0
- package/lib/http.js +90 -6
- package/lib/import-auth.js +91 -44
- package/lib/oauth.js +12 -0
- package/lib/routes/accounts.js +5 -4
- package/lib/routes/status.js +6 -8
- package/lib/runaway-guard.js +13 -1
- package/lib/session-pin.js +9 -4
- package/lib/sse.js +6 -1
- package/lib/stream-rotate.js +13 -2
- package/lib/vendor-factory.js +9 -7
- package/lib/vendors/claude-cli.js +2 -1
- package/lib/vendors/codex.js +4 -2
- package/lib/vendors/cody.js +1 -4
- package/lib/vendors/copilot.js +9 -4
- package/lib/vendors/cursor.js +1 -0
- package/lib/vendors/ernie.js +1 -5
- package/lib/vendors/glm.js +1 -0
- package/lib/vendors/jetbrains.js +1 -5
- package/lib/vendors/kimi.js +1 -0
- package/lib/vendors/kiro.js +1 -0
- package/lib/vendors/perplexity.js +1 -4
- package/lib/vendors/qwen.js +2 -5
- package/lib/vendors/replit.js +1 -4
- package/lib/vendors/spark.js +1 -5
- package/lib/wire.js +12 -11
- package/package.json +13 -8
package/lib/accounts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure, STATUS_QUARANTINE, STATUS_PROBING } from "./quarantine.js"
|
|
2
|
-
import { fetchWithTimeout } from "./http.js"
|
|
2
|
+
import { fetchWithTimeout, isTimeoutError } from "./http.js"
|
|
3
3
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
4
4
|
import { oauthRef, isProvider } from './refs.js'
|
|
5
5
|
import { parseBlob, serializeBlob } from './blob.js'
|
|
@@ -75,7 +75,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
75
75
|
const doFetch = fetchImpl || fetch
|
|
76
76
|
// #88: per-account proxy fetch for token refresh / usage (falls back to doFetch).
|
|
77
77
|
const rawFetchFor = (ref) => (typeof fetchForRef === 'function' && fetchForRef(ref)) || doFetch
|
|
78
|
-
const fetchFor = (ref) => (url, init, opts) => fetchWithTimeout(rawFetchFor(ref), url, init, { timeoutMs:
|
|
78
|
+
const fetchFor = (ref, defaultTimeoutMs = 25000) => (url, init, opts) => fetchWithTimeout(rawFetchFor(ref), url, init, { timeoutMs: defaultTimeoutMs, ...(opts || {}) })
|
|
79
79
|
|
|
80
80
|
async function resolveRaw(ref) {
|
|
81
81
|
try {
|
|
@@ -89,7 +89,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
89
89
|
async function loadBlob(ref) {
|
|
90
90
|
const raw = await resolveRaw(ref)
|
|
91
91
|
if (!raw) {
|
|
92
|
-
const err = new Error(`no token for ${ref}`)
|
|
92
|
+
const err = /** @type {Error & { code?: string }} */ (new Error(`no token for ${ref}`))
|
|
93
93
|
err.code = 'AUTH'
|
|
94
94
|
throw err
|
|
95
95
|
}
|
|
@@ -103,7 +103,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
103
103
|
try {
|
|
104
104
|
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
105
105
|
} catch (err) {
|
|
106
|
-
const wrapErr = new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`)
|
|
106
|
+
const wrapErr = /** @type {Error & { code?: string, cause?: any }} */ (new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`))
|
|
107
107
|
wrapErr.code = "CREDENTIAL_SAVE_FAILED"
|
|
108
108
|
wrapErr.cause = err
|
|
109
109
|
throw wrapErr
|
|
@@ -171,6 +171,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
171
171
|
healthBadge: healthBadge(computeHealthScore(health.get(ref) || null)),
|
|
172
172
|
usagePercent: usage.has(ref) ? usage.get(ref) : null,
|
|
173
173
|
refreshError: refreshFailures.get(ref)?.error || '',
|
|
174
|
+
isTimeout: !!refreshFailures.get(ref)?.isTimeout,
|
|
174
175
|
validationUrl: base.validationUrl || '',
|
|
175
176
|
validationMessage: base.validationMessage || '',
|
|
176
177
|
accountNotice: base.accountNotice || '',
|
|
@@ -227,7 +228,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
227
228
|
const curConfig = getConfig()
|
|
228
229
|
const cfg = vendorConfig(provider, curConfig)
|
|
229
230
|
const next = await executeWithRetry(
|
|
230
|
-
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref)),
|
|
231
|
+
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref, 30000)),
|
|
231
232
|
{
|
|
232
233
|
maxRetries: 2,
|
|
233
234
|
initialDelayMs: 300,
|
|
@@ -250,7 +251,11 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
250
251
|
if (ref) refreshFailures.delete(ref)
|
|
251
252
|
return merged
|
|
252
253
|
} catch (e) {
|
|
253
|
-
|
|
254
|
+
const isTimeout = isTimeoutError(e)
|
|
255
|
+
const errMsg = isTimeout
|
|
256
|
+
? `Network timeout refreshing token: ${e.message || 'timed out'}`
|
|
257
|
+
: String(e && e.message || e)
|
|
258
|
+
if (ref) refreshFailures.set(ref, { at: Date.now(), error: errMsg, isTimeout })
|
|
254
259
|
throw e
|
|
255
260
|
} finally {
|
|
256
261
|
refreshLocks.delete(lockKey)
|
|
@@ -260,59 +265,89 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
260
265
|
return promise
|
|
261
266
|
}
|
|
262
267
|
|
|
268
|
+
const providerUsageLocks = new Map()
|
|
269
|
+
const slotUsageLocks = new Map()
|
|
270
|
+
|
|
263
271
|
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))
|
|
272
|
+
if (providerUsageLocks.has(provider)) {
|
|
273
|
+
return providerUsageLocks.get(provider)
|
|
274
|
+
}
|
|
275
|
+
const flight = (async () => {
|
|
276
|
+
const curConfig = getConfig()
|
|
277
|
+
const cfg = vendorConfig(provider, curConfig)
|
|
278
|
+
const vendor = getVendor(provider)
|
|
279
|
+
const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
|
|
280
|
+
for (const slot of slots) {
|
|
281
|
+
if (slotUsageLocks.has(slot.ref)) {
|
|
282
|
+
await slotUsageLocks.get(slot.ref).catch(() => {})
|
|
283
|
+
continue
|
|
281
284
|
}
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
const last = usageFetched.get(slot.ref) || 0
|
|
286
|
+
if (Date.now() - last < USAGE_TTL_MS) continue
|
|
287
|
+
let raw = ''
|
|
288
|
+
try { raw = await resolveRaw(slot.ref) } catch { continue }
|
|
289
|
+
if (!raw) continue
|
|
290
|
+
|
|
291
|
+
const slotFlight = (async () => {
|
|
292
|
+
try {
|
|
293
|
+
usageFetched.set(slot.ref, Date.now())
|
|
294
|
+
const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
|
|
295
|
+
const usageTimeout = Number(curConfig?.usageTimeoutMs) || 15000
|
|
296
|
+
const snap = await vendor.usage(blob, cfg, fetchFor(slot.ref, usageTimeout))
|
|
297
|
+
if (!snap) return
|
|
298
|
+
if (Number.isFinite(Number(snap.usedPercent))) {
|
|
299
|
+
usage.set(slot.ref, Number(snap.usedPercent))
|
|
300
|
+
}
|
|
301
|
+
// Notification thresholds: 70/90/100% — fire once per window until reset.
|
|
302
|
+
if (Array.isArray(snap.windows)) {
|
|
303
|
+
for (const win of snap.windows) {
|
|
304
|
+
if (!win || !Number.isFinite(Number(win.usedPercent))) continue
|
|
305
|
+
const pct = Number(win.usedPercent)
|
|
306
|
+
const key = slot.ref + ':' + win.id
|
|
307
|
+
const prev = notifyThresholds.get(key) || 0
|
|
308
|
+
const THRESHOLDS = [70, 90, 100]
|
|
309
|
+
for (const th of THRESHOLDS) {
|
|
310
|
+
if (pct >= th && prev < th) {
|
|
311
|
+
notifyThresholds.set(key, th)
|
|
312
|
+
try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch { /* ignore notification failure */ }
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
}
|
|
295
316
|
}
|
|
296
317
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
const list = Array.isArray(snap.windows) ? snap.windows : null
|
|
319
|
+
if (list) {
|
|
320
|
+
windows.set(slot.ref, list)
|
|
321
|
+
// persist last known windows so the card survives restarts
|
|
322
|
+
try {
|
|
323
|
+
const prev = Array.isArray(blob.usage) ? JSON.stringify(blob.usage) : ''
|
|
324
|
+
if (prev !== JSON.stringify(list)) {
|
|
325
|
+
await saveBlob(slot.ref, { ...blob, usage: list, usageAt: Date.now() })
|
|
326
|
+
} else if (!blob.usageAt) {
|
|
327
|
+
await saveBlob(slot.ref, { ...blob, usageAt: Date.now() })
|
|
328
|
+
}
|
|
329
|
+
} catch { /* persistence best-effort */ }
|
|
309
330
|
}
|
|
310
|
-
} catch {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
331
|
+
} catch (err) {
|
|
332
|
+
usageFetched.set(slot.ref, Date.now())
|
|
333
|
+
const isTimeout = isTimeoutError(err)
|
|
334
|
+
if (isTimeout && !refreshFailures.has(slot.ref)) {
|
|
335
|
+
refreshFailures.set(slot.ref, { at: Date.now(), error: `Network timeout fetching usage: ${err.message || 'timed out'}`, isTimeout: true })
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
slotUsageLocks.delete(slot.ref)
|
|
339
|
+
}
|
|
340
|
+
})()
|
|
341
|
+
|
|
342
|
+
slotUsageLocks.set(slot.ref, slotFlight)
|
|
343
|
+
await slotFlight.catch(() => {})
|
|
314
344
|
}
|
|
315
|
-
}
|
|
345
|
+
})().finally(() => {
|
|
346
|
+
providerUsageLocks.delete(provider)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
providerUsageLocks.set(provider, flight)
|
|
350
|
+
return flight
|
|
316
351
|
}
|
|
317
352
|
|
|
318
353
|
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
|
@@ -53,14 +53,16 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
53
53
|
let models = null
|
|
54
54
|
if (targetAccount) {
|
|
55
55
|
const cfg = this.deps.vendorConfig(provider)
|
|
56
|
-
const
|
|
56
|
+
const baseFetch = pickFetch(this.deps, targetAccount.ref)
|
|
57
|
+
const listModelsFetch = (url, init, opts) =>
|
|
58
|
+
fetchWithTimeout(baseFetch, url, init, { timeoutMs: 15000, ...(opts || {}) })
|
|
57
59
|
try {
|
|
58
60
|
const blob = await this.deps.ensureFresh(
|
|
59
61
|
provider,
|
|
60
62
|
await this.deps.loadBlob(targetAccount.ref),
|
|
61
63
|
targetAccount.ref,
|
|
62
64
|
)
|
|
63
|
-
const live = await getVendor(provider).listModels(blob, cfg,
|
|
65
|
+
const live = await getVendor(provider).listModels(blob, cfg, listModelsFetch)
|
|
64
66
|
if (Array.isArray(live) && live.length) models = live
|
|
65
67
|
} catch { /* use built-in catalog */ }
|
|
66
68
|
}
|
|
@@ -73,7 +75,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
73
75
|
models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
|
|
74
76
|
}
|
|
75
77
|
// Defensive: ensure every model has the provider field set
|
|
76
|
-
models = models.map((m) => (m
|
|
78
|
+
models = models.map((m) => (m ? { ...m, provider: (!m.provider || m.provider === "custom") ? provider : m.provider } : m))
|
|
77
79
|
return models
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -136,12 +138,71 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
136
138
|
let cacheReadTokens = 0
|
|
137
139
|
let cacheWriteTokens = 0
|
|
138
140
|
let approxChars = 0
|
|
139
|
-
|
|
141
|
+
let completed = false
|
|
142
|
+
let sawFinish = false
|
|
143
|
+
let finishReason = null
|
|
144
|
+
let errorCaught = null
|
|
145
|
+
let finalized = false
|
|
146
|
+
let blob = null
|
|
147
|
+
|
|
148
|
+
const finalizeOnce = (outcome, extraStatus) => {
|
|
149
|
+
if (finalized) return
|
|
150
|
+
finalized = true
|
|
151
|
+
|
|
152
|
+
if (!outputTokens && approxChars > 0) {
|
|
153
|
+
outputTokens = Math.max(1, Math.round(approxChars / 4))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalMs = Date.now() - t0
|
|
157
|
+
const ttftMs = firstChunkTime !== null ? Math.max(0, firstChunkTime - t0) : null
|
|
158
|
+
const genMs = (firstChunkTime !== null && totalMs > ttftMs) ? (totalMs - ttftMs) : 0
|
|
159
|
+
const tps = (outputTokens > 0 && genMs > 0)
|
|
160
|
+
? Math.round((outputTokens / (genMs / 1000)) * 10) / 10
|
|
161
|
+
: null
|
|
162
|
+
|
|
163
|
+
let status = 200
|
|
164
|
+
if (outcome === 'error') {
|
|
165
|
+
status = extraStatus || (errorCaught && (errorCaught.status || errorCaught.statusCode)) || (errorCaught && errorCaught.code === 'QUOTA' ? 429 : 500)
|
|
166
|
+
} else if (outcome === 'cancel') {
|
|
167
|
+
status = 499
|
|
168
|
+
} else if (outcome === 'runaway') {
|
|
169
|
+
status = 200
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (outcome === 'success') {
|
|
173
|
+
if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof deps.recordHistory === 'function') {
|
|
177
|
+
try {
|
|
178
|
+
deps.recordHistory({
|
|
179
|
+
provider,
|
|
180
|
+
ref: account.ref,
|
|
181
|
+
model: (opts && opts.model) || null,
|
|
182
|
+
path: '/responses',
|
|
183
|
+
method: 'POST',
|
|
184
|
+
status,
|
|
185
|
+
outcome,
|
|
186
|
+
ms: totalMs,
|
|
187
|
+
ttftMs,
|
|
188
|
+
tps,
|
|
189
|
+
inputTokens: inputTokens || undefined,
|
|
190
|
+
outputTokens: outputTokens || undefined,
|
|
191
|
+
cacheReadTokens: cacheReadTokens || undefined,
|
|
192
|
+
cacheWriteTokens: cacheWriteTokens || undefined,
|
|
193
|
+
kind: 'stream',
|
|
194
|
+
...(outcome === 'error' && errorCaught ? { error: errorCaught.message || String(errorCaught) } : {}),
|
|
195
|
+
...(outcome === 'runaway' ? { reason: finishReason || (opts && opts._runawayState && opts._runawayState.reason) || 'runaway_loop_detected' } : {}),
|
|
196
|
+
})
|
|
197
|
+
} catch { /* ignore quota capture failure */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
140
201
|
const vendor = getVendor(provider)
|
|
141
202
|
// ponytail: capture x-ratelimit headers without touching vendors
|
|
142
203
|
const baseFetch = pickFetch(deps, account.ref)
|
|
143
204
|
const quotaFetch = async (url, init) => {
|
|
144
|
-
const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs:
|
|
205
|
+
const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs: 60000, stream: true })
|
|
145
206
|
try {
|
|
146
207
|
const snap = quotaSnapshot(provider, res.headers, null, Date.now())
|
|
147
208
|
if (snap && typeof deps.rememberQuota === "function") {
|
|
@@ -162,7 +223,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
162
223
|
} catch { /* ignore quota capture failure */ }
|
|
163
224
|
if (!res.ok && res.clone) {
|
|
164
225
|
try {
|
|
165
|
-
const
|
|
226
|
+
const textPromise = res.clone().text()
|
|
227
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
|
|
228
|
+
const txt = await Promise.race([textPromise, timeoutPromise])
|
|
166
229
|
let j=null; try { j = JSON.parse(txt) } catch { j = null }
|
|
167
230
|
if (j) {
|
|
168
231
|
const b = parseBody(provider, j, Date.now())
|
|
@@ -189,7 +252,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
189
252
|
}
|
|
190
253
|
return res
|
|
191
254
|
}
|
|
255
|
+
|
|
192
256
|
try {
|
|
257
|
+
blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
193
258
|
if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
|
|
194
259
|
for await (const chunk of vendor.streamOnce({
|
|
195
260
|
blob,
|
|
@@ -204,7 +269,10 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
204
269
|
firstChunkTime = Date.now()
|
|
205
270
|
}
|
|
206
271
|
if (chunk && typeof chunk === 'object') {
|
|
207
|
-
if (chunk.type === '
|
|
272
|
+
if (chunk.type === 'finish') {
|
|
273
|
+
sawFinish = true
|
|
274
|
+
finishReason = chunk.reason
|
|
275
|
+
} else if (chunk.type === 'usage' && chunk.usage) {
|
|
208
276
|
const u = toTokenUsage(chunk.usage)
|
|
209
277
|
if (u && typeof u.outputTokens === 'number' && u.outputTokens > 0) {
|
|
210
278
|
outputTokens = u.outputTokens
|
|
@@ -224,46 +292,15 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
224
292
|
}
|
|
225
293
|
yield chunk
|
|
226
294
|
}
|
|
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
|
-
}
|
|
295
|
+
completed = true
|
|
260
296
|
} catch (err) {
|
|
297
|
+
errorCaught = err
|
|
261
298
|
if (err && (err.code === 'QUOTA' || err.code === 'QUOTA_EXCEEDED' || err.status === 429)) {
|
|
262
299
|
if (typeof deps.recordExhaust === 'function') deps.recordExhaust(account.ref)
|
|
263
300
|
} else if (err && (err.status === 401 || err.status === 403 || err.code === 'TOKEN_REVOKED' || err.code === 'BROKEN')) {
|
|
264
301
|
if (typeof deps.recordBroken === 'function') deps.recordBroken(account.ref)
|
|
265
302
|
}
|
|
266
|
-
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
303
|
+
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl && blob) {
|
|
267
304
|
await deps.saveBlob(account.ref, {
|
|
268
305
|
...blob,
|
|
269
306
|
validationUrl: err.validationUrl,
|
|
@@ -271,6 +308,18 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
271
308
|
})
|
|
272
309
|
}
|
|
273
310
|
throw err
|
|
311
|
+
} finally {
|
|
312
|
+
if (errorCaught) {
|
|
313
|
+
finalizeOnce('error')
|
|
314
|
+
} else if (opts && opts._runawayState && opts._runawayState.tripped) {
|
|
315
|
+
finalizeOnce('runaway')
|
|
316
|
+
} else if (completed || sawFinish) {
|
|
317
|
+
finalizeOnce('success')
|
|
318
|
+
} else if (opts && opts.signal && opts.signal.aborted) {
|
|
319
|
+
finalizeOnce('cancel', 499)
|
|
320
|
+
} else {
|
|
321
|
+
finalizeOnce('cancel', 499)
|
|
322
|
+
}
|
|
274
323
|
}
|
|
275
324
|
},
|
|
276
325
|
})) {
|
package/lib/backoff.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @param {() => Promise<T>} fn
|
|
4
|
+
* @param {{
|
|
5
|
+
* maxRetries?: number,
|
|
6
|
+
* initialDelayMs?: number,
|
|
7
|
+
* maxDelayMs?: number,
|
|
8
|
+
* factor?: number,
|
|
9
|
+
* isRetryable?: (err: any) => boolean,
|
|
10
|
+
* }} [options]
|
|
11
|
+
* @returns {Promise<T>}
|
|
12
|
+
*/
|
|
1
13
|
export async function executeWithRetry(fn, {
|
|
2
14
|
maxRetries = 3,
|
|
3
15
|
initialDelayMs = 200,
|
|
4
16
|
maxDelayMs = 3000,
|
|
5
17
|
factor = 2,
|
|
6
|
-
isRetryable = () => true
|
|
18
|
+
isRetryable = (_err) => true
|
|
7
19
|
} = {}) {
|
|
8
20
|
let attempt = 0
|
|
9
21
|
let delay = initialDelayMs
|
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/code-assist.js
CHANGED
|
@@ -204,6 +204,7 @@ export function streamEnvelope({ projectId, model, request, userAgent, sessionId
|
|
|
204
204
|
const session = sessionId || randomUUID()
|
|
205
205
|
const body = { ...(request || {}) }
|
|
206
206
|
if (!body.session_id) body.session_id = session
|
|
207
|
+
/** @type {Record<string, any>} */
|
|
207
208
|
const envelope = {
|
|
208
209
|
...(projectId ? { project: projectId } : {}),
|
|
209
210
|
model,
|
package/lib/google-validation.js
CHANGED
|
@@ -98,7 +98,7 @@ export function validationFromHttpError(status, bodyText) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function validationRequiredError(details) {
|
|
101
|
-
const err = new Error(`${details.message} Verify at ${details.validationUrl}`)
|
|
101
|
+
const err = /** @type {Error & { code?: string, validationUrl?: string, learnMoreUrl?: string }} */ (new Error(`${details.message} Verify at ${details.validationUrl}`))
|
|
102
102
|
err.code = 'VALIDATION_REQUIRED'
|
|
103
103
|
err.validationUrl = details.validationUrl
|
|
104
104
|
if (details.learnMoreUrl) err.learnMoreUrl = details.learnMoreUrl
|
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
|
}
|