@goodandready/dsh-subscriptions 0.6.61 → 0.6.63

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/README.md CHANGED
@@ -100,6 +100,8 @@ graph LR
100
100
  * **Dynamic Cooldown Calculation**: Parses upstream headers (`Retry-After`, `x-ratelimit-reset`, ISO dates, epoch timestamps) and auto-restores cooled-down accounts when their window resets.
101
101
  * **Synchronous Decision Tracing & Routing Explanation (#517)**: Real-time decision traces capture every account selection, rotation step, failure reason, and cascade fallback without post-hoc reconstruction, exposed in WebUI telemetry and diagnostics.
102
102
  * **Dynamic Model Catalog & Live Quota Visualizer (#507)**: Dynamic model catalog with pool-level health calculation (individual quarantine degrades rather than disables the pool), context window badges, capability tags (`vision`, `tools`, `reasoning`, `coding`, `fast`), and live reset countdowns.
103
+ * **Cache-Aware Session Pinning (#520)**: Retains session affinity for prompt-caching providers (Claude, Codex, Antigravity, Grok) to maximize token cache reuse while strictly releasing pins when an account enters cooldown, quarantine, or quota exhaustion.
104
+ * **Guided Diagnostic Wizard (#519)**: Interactive step-by-step checklist verifying credentials, network/proxy connectivity, token freshness, and model catalog reachability without paid inference and with full secret redaction.
103
105
 
104
106
  ---
105
107
 
@@ -244,6 +246,10 @@ dsh-subscriptions:
244
246
  codexFastMode: false # v0.4.17: service_tier priority (1.5x speed tier)
245
247
  composerQuota: 'off' # v0.4.18: composer indicator: off | percent | bar | forecast
246
248
  enableToasts: true # v0.6.59: real-time in-app toast alerts on quarantine and quota events
249
+ enableQueue: true # v0.6.62: bounded concurrency queue to prevent bursts of 429 errors
250
+ accountMaxConcurrency: 2 # v0.6.62: max concurrent active streams per account
251
+ maxQueueSize: 20 # v0.6.62: max queue depth before load shedding / reject
252
+ queueTimeoutMs: 30000 # v0.6.62: max queue wait duration in ms before timeout
247
253
  # Per-slot fields (v0.4.9): expiresAt (ms epoch), proxyUrl (http/https/socks5://)
248
254
  accounts:
249
255
  codex:
package/README.ru.md CHANGED
@@ -100,6 +100,8 @@ graph LR
100
100
  * **Динамический расчёт Cooldown**: парсинг заголовков `Retry-After`, `x-ratelimit-reset`, дат ISO и миллисекундных меток с автоматическим возвратом остывших аккаунтов в строй.
101
101
  * **Трассировка решений ротации и объяснение маршрутизации (#517)**: Синхронная фиксация причин выбора аккаунтов, отказов и каскадных переходов в реальном времени с кодами причин без ретроспективной реконструкции; визуализация в телеметрии настроек и диагностике.
102
102
  * **Динамический каталог моделей с пуловой доступностью и квотами (#507)**: Обогащенная витрина моделей с расчетом здоровья пула (карантин одного аккаунта не блокирует модель при наличии живых), тегами возможностей (`vision`, `tools`, `reasoning`, `coding`, `fast`), размером контекста и таймером сброса квот.
103
+ * **Cache-Aware Session Pinning (#520)**: Сохранение привязки сессии к подходящему аккаунту для провайдеров с поддержкой кэширования промптов (Claude, Codex, Antigravity, Grok) с гарантированным немедленным освобождением при кулдауне, карантине или исчерпании квоты.
104
+ * **Мастер интерактивной диагностики (#519)**: Пошаговый чек-лист состояния аккаунтов (валидность ключей, прокси, токен, доступность каталога) без платного инференса и с автоматическим маскированием секретов.
103
105
 
104
106
  ---
105
107
 
@@ -217,6 +219,10 @@ dsh-subscriptions:
217
219
  codexFastMode: false # v0.4.17: service_tier priority (скоростной тир 1.5x)
218
220
  composerQuota: 'off' # v0.4.18: индикатор в строке ввода: off | percent | bar | forecast
219
221
  enableToasts: true # v0.6.59: всплывающие уведомления при событиях карантина и квот
222
+ enableQueue: true # v0.6.62: ограниченная очередь параллельных запросов к аккаунту
223
+ accountMaxConcurrency: 2 # v0.6.62: макс. число параллельных стримов на аккаунт
224
+ maxQueueSize: 20 # v0.6.62: макс. глубина очереди до сброса нагрузки
225
+ queueTimeoutMs: 30000 # v0.6.62: таймаут ожидания слота в очереди в мс
220
226
  # Поля слота (v0.4.9): expiresAt (ms), proxyUrl (http/https/socks5://)
221
227
  accounts:
222
228
  codex:
package/README.zh.md CHANGED
@@ -126,6 +126,10 @@ graph LR
126
126
  * **手动隔离管理(暂停与预热探测)**:支持在设置面板的账户卡片中一键暂停隔离活跃账户(`POST /dsh-subscriptions/quarantine/park`),或立即触发真实上游预热微探测(`probeWarmup`)并在验证成功后即时解除隔离(`POST /dsh-subscriptions/quarantine/probe`)。
127
127
  * **动态额度重置倒计时**:在顶部会话栏 `SubsPill` 和账户卡片中秒级动态更新,精准展示滚动配额窗口(`five_hour`、`seven_day`、`primary_window`)的剩余解锁时间。
128
128
  * **应用内即时弹窗提醒(Toast)**:当账户进入隔离、健康恢复解除隔离或额度达到预警线时,在聊天界面弹出轻量级浮动提醒。通过 `enableToasts: true` 控制。
129
+ enableQueue: true # v0.6.62: 账户并发请求排队与限流
130
+ accountMaxConcurrency: 2 # v0.6.62: 单账户最大并行活跃流
131
+ maxQueueSize: 20 # v0.6.62: 队列最大等待深度
132
+ queueTimeoutMs: 30000 # v0.6.62: 队列最大等待超时时间(毫秒)
129
133
  * **自动化 Webhook 告警集成**:通过 `notifyQuarantineEntered` 与 `notifyQuarantineReleased` 自动向外部通知系统派发隔离生命周期事件。
130
134
 
131
135
  ### Antigravity 客户端密钥架构与无感 CLI 令牌刷新 (v0.6.60)
@@ -0,0 +1,338 @@
1
+ // Subscription and cache analytics, performance aggregates, and quota forecasting.
2
+ // Enforces 0 Cyrillic characters.
3
+
4
+ export function calculatePercentile(values, percentile) {
5
+ if (!Array.isArray(values) || values.length === 0) return null
6
+ const sorted = [...values].sort((a, b) => a - b)
7
+ const index = Math.min(
8
+ sorted.length - 1,
9
+ Math.max(0, Math.ceil((percentile / 100) * sorted.length) - 1)
10
+ )
11
+ return sorted[index]
12
+ }
13
+
14
+ export function calculateCacheMetrics(entries = []) {
15
+ let promptTokens = 0
16
+ let completionTokens = 0
17
+ let cacheReadTokens = 0
18
+ let cacheCreationTokens = 0
19
+ let requestsWithCacheRead = 0
20
+
21
+ for (const r of entries) {
22
+ if (!r) continue
23
+ const prompt = typeof r.inputTokens === 'number'
24
+ ? r.inputTokens
25
+ : (r.tokens && typeof r.tokens.prompt === 'number' ? r.tokens.prompt : 0)
26
+ const completion = typeof r.outputTokens === 'number'
27
+ ? r.outputTokens
28
+ : (r.tokens && typeof r.tokens.completion === 'number' ? r.tokens.completion : 0)
29
+ const read = typeof r.cacheReadTokens === 'number'
30
+ ? r.cacheReadTokens
31
+ : (r.tokens && typeof r.tokens.cacheRead === 'number' ? r.tokens.cacheRead : 0)
32
+ const creation = typeof r.cacheCreationTokens === 'number'
33
+ ? r.cacheCreationTokens
34
+ : (r.tokens && typeof r.tokens.cacheCreation === 'number' ? r.tokens.cacheCreation : 0)
35
+
36
+ promptTokens += prompt
37
+ completionTokens += completion
38
+ cacheReadTokens += read
39
+ cacheCreationTokens += creation
40
+ if (read > 0) requestsWithCacheRead++
41
+ }
42
+
43
+ const totalTokens = promptTokens + completionTokens
44
+ const cacheTotal = promptTokens + cacheReadTokens
45
+ const cacheHitRate = cacheTotal > 0
46
+ ? Math.round((cacheReadTokens / cacheTotal) * 1000) / 10
47
+ : 0
48
+
49
+ return {
50
+ promptTokens,
51
+ completionTokens,
52
+ totalTokens,
53
+ cacheReadTokens,
54
+ cacheCreationTokens,
55
+ cacheHitRate,
56
+ requestsWithCacheRead,
57
+ cacheHitCount: requestsWithCacheRead,
58
+ }
59
+ }
60
+
61
+ export function calculatePerformanceMetrics(entries = []) {
62
+ const ttftList = []
63
+ const tpsList = []
64
+ const latencyList = []
65
+
66
+ let ttftSum = 0
67
+ let tpsSum = 0
68
+ let latencySum = 0
69
+
70
+ for (const r of entries) {
71
+ if (!r) continue
72
+ if (typeof r.ttftMs === 'number' && r.ttftMs >= 0) {
73
+ ttftList.push(r.ttftMs)
74
+ ttftSum += r.ttftMs
75
+ }
76
+ if (typeof r.tps === 'number' && r.tps > 0) {
77
+ tpsList.push(r.tps)
78
+ tpsSum += r.tps
79
+ }
80
+ if (typeof r.ms === 'number' && r.ms > 0) {
81
+ latencyList.push(r.ms)
82
+ latencySum += r.ms
83
+ }
84
+ }
85
+
86
+ const count = entries.length
87
+ return {
88
+ ttft: {
89
+ count: ttftList.length,
90
+ avgMs: ttftList.length > 0 ? Math.round(ttftSum / ttftList.length) : null,
91
+ p95Ms: calculatePercentile(ttftList, 95),
92
+ minMs: ttftList.length > 0 ? Math.min(...ttftList) : null,
93
+ maxMs: ttftList.length > 0 ? Math.max(...ttftList) : null,
94
+ },
95
+ tps: {
96
+ count: tpsList.length,
97
+ avg: tpsList.length > 0 ? Math.round((tpsSum / tpsList.length) * 10) / 10 : null,
98
+ p95: calculatePercentile(tpsList, 95),
99
+ min: tpsList.length > 0 ? Math.min(...tpsList) : null,
100
+ max: tpsList.length > 0 ? Math.max(...tpsList) : null,
101
+ },
102
+ latency: {
103
+ count: latencyList.length,
104
+ avgMs: latencyList.length > 0 ? Math.round(latencySum / latencyList.length) : 0,
105
+ p95Ms: calculatePercentile(latencyList, 95),
106
+ minMs: latencyList.length > 0 ? Math.min(...latencyList) : 0,
107
+ maxMs: latencyList.length > 0 ? Math.max(...latencyList) : 0,
108
+ totalRequests: count,
109
+ },
110
+ }
111
+ }
112
+
113
+ export function calculateHourlyBuckets(entries = [], nowMs = Date.now()) {
114
+ const buckets = []
115
+ const ONE_HOUR = 3600000
116
+
117
+ for (let i = 23; i >= 0; i--) {
118
+ const end = nowMs - (i * ONE_HOUR)
119
+ const start = end - ONE_HOUR
120
+ const dateObj = new Date(start)
121
+ const hourNum = dateObj.getHours()
122
+ const hourStr = String(hourNum).padStart(2, '0') + ':00'
123
+
124
+ buckets.push({
125
+ hour: hourNum,
126
+ label: hourStr,
127
+ start,
128
+ end,
129
+ requests: 0,
130
+ successCount: 0,
131
+ errorCount: 0,
132
+ promptTokens: 0,
133
+ completionTokens: 0,
134
+ cacheReadTokens: 0,
135
+ totalTokens: 0,
136
+ })
137
+ }
138
+
139
+ const cutoff = nowMs - (24 * ONE_HOUR)
140
+ for (const r of entries) {
141
+ if (!r || !r.ts || r.ts < cutoff || r.ts > nowMs) continue
142
+ const age = nowMs - r.ts
143
+ const bucketIndex = 23 - Math.floor(age / ONE_HOUR)
144
+ if (bucketIndex >= 0 && bucketIndex < 24) {
145
+ const b = buckets[bucketIndex]
146
+ b.requests++
147
+ const st = Number(r.status || 0)
148
+ if (st >= 200 && st < 400) b.successCount++
149
+ else if (st >= 400) b.errorCount++
150
+
151
+ const prompt = Number(r.inputTokens || (r.tokens && r.tokens.prompt) || 0)
152
+ const completion = Number(r.outputTokens || (r.tokens && r.tokens.completion) || 0)
153
+ const cacheRead = Number(r.cacheReadTokens || (r.tokens && r.tokens.cacheRead) || 0)
154
+
155
+ b.promptTokens += prompt
156
+ b.completionTokens += completion
157
+ b.cacheReadTokens += cacheRead
158
+ b.totalTokens += (prompt + completion)
159
+ }
160
+ }
161
+
162
+ return buckets
163
+ }
164
+
165
+ export function calculateAccountBreakdown(entries = [], accounts = []) {
166
+ const map = {}
167
+ const labels = {}
168
+ for (const a of accounts) {
169
+ if (a && a.ref) {
170
+ labels[a.ref] = a.label || a.ref
171
+ }
172
+ }
173
+
174
+ for (const r of entries) {
175
+ if (!r || !r.ref) continue
176
+ const ref = r.ref
177
+ if (!map[ref]) {
178
+ map[ref] = {
179
+ ref,
180
+ provider: r.provider || 'unknown',
181
+ label: labels[ref] || r.accountLabel || ref,
182
+ totalRequests: 0,
183
+ successRequests: 0,
184
+ errorRequests: 0,
185
+ promptTokens: 0,
186
+ completionTokens: 0,
187
+ cacheReadTokens: 0,
188
+ cacheCreationTokens: 0,
189
+ ttftSum: 0,
190
+ ttftCount: 0,
191
+ tpsSum: 0,
192
+ tpsCount: 0,
193
+ latencySum: 0,
194
+ }
195
+ }
196
+ const acc = map[ref]
197
+ acc.totalRequests++
198
+ const st = Number(r.status || 0)
199
+ if (st >= 200 && st < 400) acc.successRequests++
200
+ else if (st >= 400) acc.errorRequests++
201
+
202
+ const prompt = Number(r.inputTokens || (r.tokens && r.tokens.prompt) || 0)
203
+ const completion = Number(r.outputTokens || (r.tokens && r.tokens.completion) || 0)
204
+ const cacheRead = Number(r.cacheReadTokens || (r.tokens && r.tokens.cacheRead) || 0)
205
+ const cacheCreation = Number(r.cacheCreationTokens || (r.tokens && r.tokens.cacheCreation) || 0)
206
+
207
+ acc.promptTokens += prompt
208
+ acc.completionTokens += completion
209
+ acc.cacheReadTokens += cacheRead
210
+ acc.cacheCreationTokens += cacheCreation
211
+
212
+ if (typeof r.ttftMs === 'number' && r.ttftMs >= 0) {
213
+ acc.ttftSum += r.ttftMs
214
+ acc.ttftCount++
215
+ }
216
+ if (typeof r.tps === 'number' && r.tps > 0) {
217
+ acc.tpsSum += r.tps
218
+ acc.tpsCount++
219
+ }
220
+ if (typeof r.ms === 'number' && r.ms > 0) {
221
+ acc.latencySum += r.ms
222
+ }
223
+ }
224
+
225
+ const result = {}
226
+ for (const [ref, acc] of Object.entries(map)) {
227
+ const totalCache = acc.promptTokens + acc.cacheReadTokens
228
+ result[ref] = {
229
+ ref,
230
+ provider: acc.provider,
231
+ label: acc.label,
232
+ totalRequests: acc.totalRequests,
233
+ successRequests: acc.successRequests,
234
+ errorRequests: acc.errorRequests,
235
+ promptTokens: acc.promptTokens,
236
+ completionTokens: acc.completionTokens,
237
+ cacheReadTokens: acc.cacheReadTokens,
238
+ cacheCreationTokens: acc.cacheCreationTokens,
239
+ cacheHitRate: totalCache > 0 ? Math.round((acc.cacheReadTokens / totalCache) * 1000) / 10 : 0,
240
+ avgTtftMs: acc.ttftCount > 0 ? Math.round(acc.ttftSum / acc.ttftCount) : null,
241
+ avgTps: acc.tpsCount > 0 ? Math.round((acc.tpsSum / acc.tpsCount) * 10) / 10 : null,
242
+ avgLatencyMs: acc.totalRequests > 0 ? Math.round(acc.latencySum / acc.totalRequests) : 0,
243
+ }
244
+ }
245
+ return result
246
+ }
247
+
248
+ export function calculateQuotaForecast(accounts = [], entries = [], nowMs = Date.now()) {
249
+ const ONE_HOUR = 3600000
250
+ const ONE_DAY = 24 * ONE_HOUR
251
+ const forecast = []
252
+
253
+ for (const acc of accounts) {
254
+ if (!acc || !acc.ref) continue
255
+ const quota = acc.quota
256
+ const ref = acc.ref
257
+
258
+ // Calculate recent usage over the past 24 hours for this account
259
+ let tokens24h = 0
260
+ let requests24h = 0
261
+ let tokens1h = 0
262
+ let requests1h = 0
263
+
264
+ for (const r of entries) {
265
+ if (!r || r.ref !== ref || !r.ts) continue
266
+ const age = nowMs - r.ts
267
+ if (age <= ONE_DAY) {
268
+ const total = Number(r.inputTokens || (r.tokens && r.tokens.prompt) || 0) +
269
+ Number(r.outputTokens || (r.tokens && r.tokens.completion) || 0)
270
+ tokens24h += total
271
+ requests24h++
272
+ if (age <= ONE_HOUR) {
273
+ tokens1h += total
274
+ requests1h++
275
+ }
276
+ }
277
+ }
278
+
279
+ const burnRateTokensPerHour = requests1h > 0 ? tokens1h : Math.round(tokens24h / 24)
280
+
281
+ let willExhaustBeforeReset = false
282
+ let projectedExhaustionAt = null
283
+ let hoursToExhaustion = null
284
+ let remainingMs = null
285
+
286
+ if (quota && typeof quota.percentage === 'number') {
287
+ const remainingPercent = Math.max(0, Math.min(100, quota.percentage))
288
+ const resetAt = typeof quota.resetAt === 'number' ? quota.resetAt : null
289
+
290
+ if (resetAt && resetAt > nowMs) {
291
+ remainingMs = resetAt - nowMs
292
+ }
293
+
294
+ // If quota is low and active burn rate exists
295
+ if (burnRateTokensPerHour > 0 && remainingPercent > 0) {
296
+ // Approximate burn rate percentage per hour based on 24h consumption
297
+ // If 24h consumed tokens24h and quota dropped, estimate burn % per hour
298
+ const estimatedPercentPerHour = tokens24h > 0 ? Math.max(0.5, (100 - remainingPercent) / 24) : 0
299
+ if (estimatedPercentPerHour > 0) {
300
+ hoursToExhaustion = Math.round((remainingPercent / estimatedPercentPerHour) * 10) / 10
301
+ const msToExhaustion = hoursToExhaustion * ONE_HOUR
302
+ if (remainingMs !== null && msToExhaustion < remainingMs) {
303
+ willExhaustBeforeReset = true
304
+ projectedExhaustionAt = nowMs + msToExhaustion
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ forecast.push({
311
+ ref,
312
+ provider: acc.provider,
313
+ label: acc.label || ref,
314
+ quotaPercent: quota && typeof quota.percentage === 'number' ? quota.percentage : null,
315
+ resetAt: quota && typeof quota.resetAt === 'number' ? quota.resetAt : null,
316
+ burnRateTokensPerHour,
317
+ requests24h,
318
+ tokens24h,
319
+ hoursToExhaustion,
320
+ willExhaustBeforeReset,
321
+ projectedExhaustionAt,
322
+ })
323
+ }
324
+
325
+ return forecast
326
+ }
327
+
328
+ export function calculateAnalytics(entries = [], accounts = [], nowMs = Date.now()) {
329
+ return {
330
+ generatedAt: new Date(nowMs).toISOString(),
331
+ totalRequests: entries.length,
332
+ cache: calculateCacheMetrics(entries),
333
+ performance: calculatePerformanceMetrics(entries),
334
+ hourly: calculateHourlyBuckets(entries, nowMs),
335
+ accounts: calculateAccountBreakdown(entries, accounts),
336
+ forecast: calculateQuotaForecast(accounts, entries, nowMs),
337
+ }
338
+ }