@goodandready/dsh-subscriptions 0.6.60 → 0.6.62
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 +6 -0
- package/README.ru.md +6 -0
- package/README.zh.md +6 -0
- package/lib/adapter-manager.js +14 -0
- package/lib/adapter.js +18 -6
- package/lib/analytics.js +338 -0
- package/lib/client.js +267 -8
- package/lib/concurrency-queue.js +222 -0
- package/lib/config-schema.js +8 -0
- package/lib/diagnostics.js +17 -0
- package/lib/history.js +22 -0
- package/lib/models-catalog.js +240 -0
- package/lib/rotate.js +115 -14
- package/lib/routes/status.js +74 -1
- package/lib/session-pin.js +4 -0
- package/lib/stream-rotate.js +102 -6
- package/lib/subscriptions.js +23 -3
- package/package.json +1 -1
package/lib/history.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { calculateAnalytics } from './analytics.js'
|
|
1
2
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
3
|
import { join } from 'node:path'
|
|
3
4
|
import { homedir } from 'node:os'
|
|
@@ -261,6 +262,22 @@ export class HistoryStore {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
const first = this.rows[0] // newest first
|
|
265
|
+
const recentDecisions = []
|
|
266
|
+
for (const r of this.rows) {
|
|
267
|
+
if (r && r.decisionTrace) {
|
|
268
|
+
recentDecisions.push({
|
|
269
|
+
ts: r.ts,
|
|
270
|
+
provider: r.provider,
|
|
271
|
+
ref: r.ref,
|
|
272
|
+
model: r.model || null,
|
|
273
|
+
decisionReason: r.decisionTrace.finalOutcome || (r.decisionTrace.attempts && r.decisionTrace.attempts[0] && r.decisionTrace.attempts[0].decisionReason) || 'selected',
|
|
274
|
+
attemptsCount: Array.isArray(r.decisionTrace.attempts) ? r.decisionTrace.attempts.length : 1,
|
|
275
|
+
cascaded: r.decisionTrace.finalOutcome === 'cascade_fallback' || r.kind === 'cascade',
|
|
276
|
+
})
|
|
277
|
+
if (recentDecisions.length >= 10) break
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
264
281
|
return {
|
|
265
282
|
totalRequests: total,
|
|
266
283
|
requests24h: count24h,
|
|
@@ -279,6 +296,11 @@ export class HistoryStore {
|
|
|
279
296
|
? Math.round((totalCacheReadTokens / totalPromptTokens) * 1000) / 10
|
|
280
297
|
: 0,
|
|
281
298
|
accounts: this.accountsTelemetry(),
|
|
299
|
+
recentDecisions,
|
|
282
300
|
}
|
|
283
301
|
}
|
|
302
|
+
|
|
303
|
+
getAnalytics(accounts = [], nowMs = Date.now()) {
|
|
304
|
+
return calculateAnalytics(this.rows, accounts, nowMs)
|
|
305
|
+
}
|
|
284
306
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { PROVIDERS, displayName } from './refs.js'
|
|
2
|
+
|
|
3
|
+
export function isDeprecatedId(id) {
|
|
4
|
+
return /(^|[-_:])(test|preview|dev|alpha|beta|snapshot|experimental|legacy|deprecated)([-_:]|$)/i.test(String(id || ''))
|
|
5
|
+
}
|
|
6
|
+
import { getVendor } from './vendors/index.js'
|
|
7
|
+
import { getAccountRemainingPercent } from './rotate.js'
|
|
8
|
+
|
|
9
|
+
export const CAPABILITY_TAGS = {
|
|
10
|
+
VISION: 'vision',
|
|
11
|
+
TOOLS: 'tools',
|
|
12
|
+
REASONING: 'reasoning',
|
|
13
|
+
CODING: 'coding',
|
|
14
|
+
FAST: 'fast',
|
|
15
|
+
MATH: 'math',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Infer context window and capability tags for a given provider and model.
|
|
20
|
+
* @param {string} provider
|
|
21
|
+
* @param {string} modelId
|
|
22
|
+
* @returns {{ contextWindow: number, contextLabel: string, capabilities: string[] }}
|
|
23
|
+
*/
|
|
24
|
+
export function inferModelCapabilities(provider, modelId) {
|
|
25
|
+
const id = String(modelId || '').toLowerCase()
|
|
26
|
+
const p = String(provider || '').toLowerCase()
|
|
27
|
+
const caps = new Set([CAPABILITY_TAGS.TOOLS])
|
|
28
|
+
let contextWindow = 128000
|
|
29
|
+
let contextLabel = '128k'
|
|
30
|
+
|
|
31
|
+
// Vision detection
|
|
32
|
+
if (
|
|
33
|
+
id.includes('vision') ||
|
|
34
|
+
id.includes('gpt-4o') ||
|
|
35
|
+
id.includes('gpt-5') ||
|
|
36
|
+
id.includes('gpt-6') ||
|
|
37
|
+
id.includes('claude') ||
|
|
38
|
+
id.includes('gemini') ||
|
|
39
|
+
id.includes('grok-2') ||
|
|
40
|
+
id.includes('grok-3')
|
|
41
|
+
) {
|
|
42
|
+
caps.add(CAPABILITY_TAGS.VISION)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Reasoning detection
|
|
46
|
+
if (
|
|
47
|
+
id.includes('thinking') ||
|
|
48
|
+
id.includes('reasoning') ||
|
|
49
|
+
id.includes('o1') ||
|
|
50
|
+
id.includes('o3') ||
|
|
51
|
+
id.includes('gpt-5') ||
|
|
52
|
+
id.includes('gpt-6') ||
|
|
53
|
+
id.includes('claude-opus') ||
|
|
54
|
+
id.includes('claude-sonnet-5') ||
|
|
55
|
+
id.includes('claude-fable') ||
|
|
56
|
+
id.includes('claude-3-7') ||
|
|
57
|
+
id.includes('k1.5') ||
|
|
58
|
+
id.includes('r1')
|
|
59
|
+
) {
|
|
60
|
+
caps.add(CAPABILITY_TAGS.REASONING)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Coding detection
|
|
64
|
+
if (
|
|
65
|
+
id.includes('codex') ||
|
|
66
|
+
id.includes('coder') ||
|
|
67
|
+
id.includes('coding') ||
|
|
68
|
+
id.includes('sonnet') ||
|
|
69
|
+
id.includes('gpt-5') ||
|
|
70
|
+
id.includes('gpt-6') ||
|
|
71
|
+
id.includes('sol') ||
|
|
72
|
+
id.includes('terra') ||
|
|
73
|
+
id.includes('luna') ||
|
|
74
|
+
p === 'codex' ||
|
|
75
|
+
p === 'copilot' ||
|
|
76
|
+
p === 'cursor'
|
|
77
|
+
) {
|
|
78
|
+
caps.add(CAPABILITY_TAGS.CODING)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fast / Speed detection
|
|
82
|
+
if (id.includes('flash') || id.includes('mini') || id.includes('haiku') || id.includes('small') || id.includes('fast')) {
|
|
83
|
+
caps.add(CAPABILITY_TAGS.FAST)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Context window mapping
|
|
87
|
+
if (p === 'antigravity' && (id.includes('gemini-2.5') || id.includes('gemini-3'))) {
|
|
88
|
+
contextWindow = id.includes('pro') ? 2000000 : 1000000
|
|
89
|
+
contextLabel = id.includes('pro') ? '2M' : '1M'
|
|
90
|
+
} else if (id.includes('grok-3') || id.includes('1m')) {
|
|
91
|
+
contextWindow = 1000000
|
|
92
|
+
contextLabel = '1M'
|
|
93
|
+
} else if (
|
|
94
|
+
id.includes('claude-') ||
|
|
95
|
+
id.includes('o1') ||
|
|
96
|
+
id.includes('o3') ||
|
|
97
|
+
id.includes('gpt-5') ||
|
|
98
|
+
id.includes('gpt-6') ||
|
|
99
|
+
id.includes('kimi') ||
|
|
100
|
+
id.includes('200k')
|
|
101
|
+
) {
|
|
102
|
+
contextWindow = 200000
|
|
103
|
+
contextLabel = '200k'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
contextWindow,
|
|
108
|
+
contextLabel,
|
|
109
|
+
capabilities: Array.from(caps),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculates pool-wide status and quota for a provider from the account list.
|
|
115
|
+
* @param {string} provider
|
|
116
|
+
* @param {Array<object>} accounts
|
|
117
|
+
* @param {number} [nowMs]
|
|
118
|
+
* @returns {object}
|
|
119
|
+
*/
|
|
120
|
+
export function calculatePoolStatus(provider, accounts = [], nowMs = Date.now()) {
|
|
121
|
+
const p = String(provider || '').toLowerCase()
|
|
122
|
+
const matching = (accounts || []).filter((a) => a && String(a.provider).toLowerCase() === p)
|
|
123
|
+
|
|
124
|
+
if (!matching.length) {
|
|
125
|
+
return {
|
|
126
|
+
status: 'unconfigured',
|
|
127
|
+
totalAccounts: 0,
|
|
128
|
+
healthyAccounts: 0,
|
|
129
|
+
quarantinedAccounts: 0,
|
|
130
|
+
cooldownAccounts: 0,
|
|
131
|
+
remainingPercent: null,
|
|
132
|
+
resetAt: null,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let healthyCount = 0
|
|
137
|
+
let quarantinedCount = 0
|
|
138
|
+
let cooldownCount = 0
|
|
139
|
+
let maxRemainingPercent = null
|
|
140
|
+
let nearestResetAt = null
|
|
141
|
+
|
|
142
|
+
for (const acc of matching) {
|
|
143
|
+
const isQuarantined = Boolean(acc.quarantineUntil && Number(acc.quarantineUntil) > nowMs)
|
|
144
|
+
const isCooldown = Boolean(acc.cooldownUntil && Number(acc.cooldownUntil) > nowMs)
|
|
145
|
+
const rem = getAccountRemainingPercent(acc)
|
|
146
|
+
const isExhausted = (rem !== null && rem <= 0) || (acc.usagePercent != null && Number(acc.usagePercent) >= 100)
|
|
147
|
+
const hasToken = Boolean(acc.configured && acc.writable !== false)
|
|
148
|
+
|
|
149
|
+
if (isQuarantined) quarantinedCount++
|
|
150
|
+
if (isCooldown) cooldownCount++
|
|
151
|
+
|
|
152
|
+
if (hasToken && !isQuarantined && !isCooldown && !isExhausted) {
|
|
153
|
+
healthyCount++
|
|
154
|
+
if (rem !== null) {
|
|
155
|
+
maxRemainingPercent = maxRemainingPercent === null ? rem : Math.max(maxRemainingPercent, rem)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const resetAt = acc.quota?.resetAt ? Number(acc.quota.resetAt) : null
|
|
160
|
+
if (resetAt && resetAt > nowMs) {
|
|
161
|
+
if (!nearestResetAt || resetAt < nearestResetAt) {
|
|
162
|
+
nearestResetAt = resetAt
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let status = 'available'
|
|
168
|
+
if (healthyCount === 0) {
|
|
169
|
+
if (quarantinedCount === matching.length) {
|
|
170
|
+
status = 'quarantined'
|
|
171
|
+
} else if (cooldownCount === matching.length) {
|
|
172
|
+
status = 'cooldown'
|
|
173
|
+
} else {
|
|
174
|
+
status = 'exhausted'
|
|
175
|
+
}
|
|
176
|
+
} else if (healthyCount < matching.length) {
|
|
177
|
+
status = 'degraded'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
status,
|
|
182
|
+
totalAccounts: matching.length,
|
|
183
|
+
healthyAccounts: healthyCount,
|
|
184
|
+
quarantinedAccounts: quarantinedCount,
|
|
185
|
+
cooldownAccounts: cooldownCount,
|
|
186
|
+
remainingPercent: maxRemainingPercent !== null ? Math.round(maxRemainingPercent) : null,
|
|
187
|
+
resetAt: nearestResetAt,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds the enriched model catalog across all supported subscription vendors.
|
|
193
|
+
* @param {object} options
|
|
194
|
+
* @param {Array<object>} options.accounts
|
|
195
|
+
* @param {object} [options.liveConfig]
|
|
196
|
+
* @param {boolean} [options.hideDeprecated]
|
|
197
|
+
* @param {number} [options.nowMs]
|
|
198
|
+
* @returns {Array<object>}
|
|
199
|
+
*/
|
|
200
|
+
export function buildEnrichedCatalog({
|
|
201
|
+
accounts = [],
|
|
202
|
+
liveConfig = {},
|
|
203
|
+
hideDeprecated = false,
|
|
204
|
+
nowMs = Date.now(),
|
|
205
|
+
} = {}) {
|
|
206
|
+
const result = []
|
|
207
|
+
|
|
208
|
+
for (const provId of PROVIDERS) {
|
|
209
|
+
const vendor = getVendor(provId)
|
|
210
|
+
const pool = calculatePoolStatus(provId, accounts, nowMs)
|
|
211
|
+
const modelsKey = `${provId}Models`
|
|
212
|
+
let rawList = liveConfig && Array.isArray(liveConfig[modelsKey]) && liveConfig[modelsKey].length
|
|
213
|
+
? liveConfig[modelsKey]
|
|
214
|
+
: (vendor.defaults ? vendor.defaults().models : [])
|
|
215
|
+
|
|
216
|
+
if (!Array.isArray(rawList)) rawList = []
|
|
217
|
+
|
|
218
|
+
for (const item of rawList) {
|
|
219
|
+
const modelId = typeof item === 'string' ? item : (item.id || item.slug || item.name)
|
|
220
|
+
if (!modelId) continue
|
|
221
|
+
if (hideDeprecated && isDeprecatedId(modelId)) continue
|
|
222
|
+
|
|
223
|
+
const modelName = (typeof item === 'object' && (item.name || item.display_name)) ? (item.name || item.display_name) : modelId
|
|
224
|
+
const caps = inferModelCapabilities(provId, modelId)
|
|
225
|
+
|
|
226
|
+
result.push({
|
|
227
|
+
id: modelId,
|
|
228
|
+
name: modelName,
|
|
229
|
+
provider: provId,
|
|
230
|
+
providerName: displayName(provId),
|
|
231
|
+
contextWindow: caps.contextWindow,
|
|
232
|
+
contextLabel: caps.contextLabel,
|
|
233
|
+
capabilities: caps.capabilities,
|
|
234
|
+
pool,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
}
|
package/lib/rotate.js
CHANGED
|
@@ -63,7 +63,7 @@ export function getAccountRemainingPercent(acc) {
|
|
|
63
63
|
return null
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function
|
|
66
|
+
export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
67
67
|
const list = Array.isArray(accounts) ? accounts : []
|
|
68
68
|
const now = Number(nowMs) || 0
|
|
69
69
|
const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0
|
|
@@ -72,6 +72,8 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
72
72
|
const targetTag = opts && opts.tag
|
|
73
73
|
const targetSession = opts && opts.sessionId
|
|
74
74
|
|
|
75
|
+
const evaluations = []
|
|
76
|
+
|
|
75
77
|
function isQuotaExhausted(acc) {
|
|
76
78
|
if (!thr || thr <= 0) return false
|
|
77
79
|
const q = acc.quota
|
|
@@ -96,7 +98,7 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
96
98
|
return true
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
// Sticky Session Pinning (#170)
|
|
101
|
+
// Sticky Session Pinning (#170, #517)
|
|
100
102
|
if (targetSession) {
|
|
101
103
|
const pinnedRef = getPinnedAccountRef(targetSession)
|
|
102
104
|
if (pinnedRef) {
|
|
@@ -108,8 +110,42 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
108
110
|
(pinned.quota.remainingPercent != null && Number(pinned.quota.remainingPercent) <= 0)
|
|
109
111
|
) && (!pinned.quota.resetAt || Number(pinned.quota.resetAt) > now))
|
|
110
112
|
if (!isCooldown && !isPinnedZero && !isQuotaExhausted(pinned) && !isUsageExhausted(pinned)) {
|
|
111
|
-
|
|
113
|
+
evaluations.push({
|
|
114
|
+
ref: pinnedRef,
|
|
115
|
+
eligible: true,
|
|
116
|
+
tier: 0,
|
|
117
|
+
reason: 'session_pinned',
|
|
118
|
+
pinned: true,
|
|
119
|
+
})
|
|
120
|
+
return {
|
|
121
|
+
account: pinned,
|
|
122
|
+
trace: {
|
|
123
|
+
chosenRef: pinnedRef,
|
|
124
|
+
decisionReason: 'session_pinned',
|
|
125
|
+
tier: 0,
|
|
126
|
+
evaluations,
|
|
127
|
+
selectionMetrics: {
|
|
128
|
+
pinned: true,
|
|
129
|
+
remainingPercent: getAccountRemainingPercent(pinned),
|
|
130
|
+
healthScore: pinned.healthScore ?? 100,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
evaluations.push({
|
|
136
|
+
ref: pinnedRef,
|
|
137
|
+
eligible: false,
|
|
138
|
+
reason: isCooldown ? 'pinned_cooldown' : isPinnedZero ? 'pinned_zero_quota' : 'pinned_exhausted',
|
|
139
|
+
pinned: true,
|
|
140
|
+
})
|
|
112
141
|
}
|
|
142
|
+
} else if (pinned) {
|
|
143
|
+
evaluations.push({
|
|
144
|
+
ref: pinnedRef,
|
|
145
|
+
eligible: false,
|
|
146
|
+
reason: !pinned.hasToken ? 'pinned_no_token' : 'pinned_quarantine',
|
|
147
|
+
pinned: true,
|
|
148
|
+
})
|
|
113
149
|
}
|
|
114
150
|
}
|
|
115
151
|
}
|
|
@@ -121,7 +157,12 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
121
157
|
const tiers = [[], [], []]
|
|
122
158
|
|
|
123
159
|
for (const acc of list) {
|
|
124
|
-
if (!acc
|
|
160
|
+
if (!acc) continue
|
|
161
|
+
const ref = acc.ref || acc.id || 'unknown'
|
|
162
|
+
if (!acc.hasToken) {
|
|
163
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'no_token' })
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
125
166
|
|
|
126
167
|
if (!opts || opts.autoPacing !== false) {
|
|
127
168
|
if (acc.pacingRisk === undefined) {
|
|
@@ -143,34 +184,62 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
143
184
|
}
|
|
144
185
|
|
|
145
186
|
// VIP Slot Reservation (#175)
|
|
146
|
-
if (acc.vipOnly && !isVipRequest)
|
|
187
|
+
if (acc.vipOnly && !isVipRequest) {
|
|
188
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'vip_reserved' })
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
147
191
|
|
|
148
192
|
// Tag-based filtering (#173)
|
|
149
|
-
if (targetTag && Array.isArray(acc.tags) && !acc.tags.includes(targetTag))
|
|
193
|
+
if (targetTag && Array.isArray(acc.tags) && !acc.tags.includes(targetTag)) {
|
|
194
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'tag_mismatch' })
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
150
197
|
|
|
151
198
|
const isCooldown = acc.cooldownUntil && Number(acc.cooldownUntil) > now && cooldownBlocks(acc, opts && opts.family)
|
|
199
|
+
if (isCooldown) {
|
|
200
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'cooldown', until: acc.cooldownUntil })
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
152
204
|
const inQuarantine = isQuarantined(acc, now)
|
|
205
|
+
if (inQuarantine) {
|
|
206
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'quarantine', until: acc.quarantineUntil, quarantineReason: acc.quarantineReason })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
153
210
|
const isHardExhausted = (acc.usagePercent != null && Number(acc.usagePercent) >= 100 && (!acc.quota || !acc.quota.resetAt || Number(acc.quota.resetAt) > now))
|
|
211
|
+
if (isHardExhausted) {
|
|
212
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'hard_exhausted', usagePercent: acc.usagePercent })
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
154
216
|
const isZeroQuota = Boolean(acc.quota && (
|
|
155
217
|
(acc.quota.remaining != null && Number(acc.quota.remaining) <= 0) ||
|
|
156
218
|
(acc.quota.remainingPercent != null && Number(acc.quota.remainingPercent) <= 0)
|
|
157
219
|
) && (!acc.quota.resetAt || Number(acc.quota.resetAt) > now))
|
|
158
|
-
|
|
159
|
-
|
|
220
|
+
if (isZeroQuota) {
|
|
221
|
+
evaluations.push({ ref, eligible: false, tier: null, reason: 'zero_quota' })
|
|
160
222
|
continue
|
|
161
223
|
}
|
|
162
224
|
|
|
163
225
|
const softExhausted = isQuotaExhausted(acc) || isUsageExhausted(acc)
|
|
164
226
|
if (softExhausted) {
|
|
165
227
|
tiers[2].push(acc)
|
|
228
|
+
evaluations.push({ ref, eligible: true, tier: 2, reason: 'soft_exhausted', remainingPercent: getAccountRemainingPercent(acc) })
|
|
166
229
|
continue
|
|
167
230
|
}
|
|
168
231
|
|
|
169
|
-
if (!acc.quota)
|
|
170
|
-
|
|
232
|
+
if (!acc.quota) {
|
|
233
|
+
tiers[1].push(acc)
|
|
234
|
+
evaluations.push({ ref, eligible: true, tier: 1, reason: 'healthy_unknown_quota' })
|
|
235
|
+
} else {
|
|
236
|
+
tiers[0].push(acc)
|
|
237
|
+
evaluations.push({ ref, eligible: true, tier: 0, reason: 'healthy_known_quota', remainingPercent: getAccountRemainingPercent(acc) })
|
|
238
|
+
}
|
|
171
239
|
}
|
|
172
240
|
|
|
173
|
-
for (
|
|
241
|
+
for (let tierIdx = 0; tierIdx < tiers.length; tierIdx++) {
|
|
242
|
+
const tier = tiers[tierIdx]
|
|
174
243
|
if (tier.length) {
|
|
175
244
|
tier.sort((a, b) => {
|
|
176
245
|
// #485: Smart Quota Drainer - prioritize higher burn urgency first
|
|
@@ -206,14 +275,46 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
206
275
|
})
|
|
207
276
|
|
|
208
277
|
const chosen = tier[0]
|
|
278
|
+
const chosenRef = chosen.ref || chosen.id
|
|
209
279
|
if (targetSession && chosen) {
|
|
210
|
-
pinSession(targetSession,
|
|
280
|
+
pinSession(targetSession, chosenRef)
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
account: chosen,
|
|
284
|
+
trace: {
|
|
285
|
+
chosenRef,
|
|
286
|
+
decisionReason: tierIdx === 0 ? 'healthy_tier0' : tierIdx === 1 ? 'healthy_tier1' : 'tier2_fallback',
|
|
287
|
+
tier: tierIdx,
|
|
288
|
+
evaluations,
|
|
289
|
+
selectionMetrics: {
|
|
290
|
+
tier: tierIdx,
|
|
291
|
+
burnUrgency: chosen.burnUrgency != null ? Number(chosen.burnUrgency) : null,
|
|
292
|
+
pacingRisk: chosen.pacingRisk != null ? Number(chosen.pacingRisk) : null,
|
|
293
|
+
remainingPercent: getAccountRemainingPercent(chosen),
|
|
294
|
+
resetAt: chosen.quota?.resetAt || null,
|
|
295
|
+
weight: chosen.weight || (chosen.isPro ? 5 : 1),
|
|
296
|
+
healthScore: chosen.healthScore ?? 100,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
211
299
|
}
|
|
212
|
-
return chosen
|
|
213
300
|
}
|
|
214
301
|
}
|
|
215
302
|
|
|
216
|
-
return
|
|
303
|
+
return {
|
|
304
|
+
account: null,
|
|
305
|
+
trace: {
|
|
306
|
+
chosenRef: null,
|
|
307
|
+
decisionReason: 'no_usable_account',
|
|
308
|
+
tier: null,
|
|
309
|
+
evaluations,
|
|
310
|
+
selectionMetrics: null,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function pickAccount(accounts, nowMs, opts) {
|
|
316
|
+
const res = pickAccountWithTrace(accounts, nowMs, opts)
|
|
317
|
+
return res ? res.account : null
|
|
217
318
|
}
|
|
218
319
|
|
|
219
320
|
export function markCooldown(account, nowMs, cooldownMs, family) {
|
package/lib/routes/status.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { defaultConcurrencyQueue } from '../concurrency-queue.js'
|
|
2
|
+
import { calculateAnalytics } from '../analytics.js'
|
|
3
|
+
import { buildEnrichedCatalog } from '../models-catalog.js'
|
|
1
4
|
// #374: Sensitive GET routes protected by same-origin check
|
|
2
5
|
import { readFileSync } from 'node:fs'
|
|
3
6
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
@@ -36,12 +39,19 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
async function configResponse() {
|
|
42
|
+
const accts = await accountsView()
|
|
39
43
|
return {
|
|
40
44
|
ok: true,
|
|
41
45
|
revision: getCurrentRevision(),
|
|
42
46
|
config: publicConfig(live()),
|
|
43
|
-
accounts:
|
|
47
|
+
accounts: accts,
|
|
44
48
|
providers: PROVIDERS.map((id) => ({ id, name: displayName(id) })),
|
|
49
|
+
modelsCatalog: buildEnrichedCatalog({
|
|
50
|
+
accounts: accts,
|
|
51
|
+
liveConfig: live(),
|
|
52
|
+
hideDeprecated: !!live().hideDeprecatedModels,
|
|
53
|
+
}),
|
|
54
|
+
queue: defaultConcurrencyQueue.stats(),
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -239,6 +249,30 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
239
249
|
}), 'dsh-subscriptions: /status')
|
|
240
250
|
|
|
241
251
|
|
|
252
|
+
ctx.effect(() => ctx.webServer.register({
|
|
253
|
+
kind: 'exact',
|
|
254
|
+
path: '/dsh-subscriptions/models',
|
|
255
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
256
|
+
if (req.method !== 'GET') {
|
|
257
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
261
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
const accts = await accountsView()
|
|
265
|
+
writeJson(res, 200, {
|
|
266
|
+
ok: true,
|
|
267
|
+
models: buildEnrichedCatalog({
|
|
268
|
+
accounts: accts,
|
|
269
|
+
liveConfig: live(),
|
|
270
|
+
hideDeprecated: !!live().hideDeprecatedModels,
|
|
271
|
+
}),
|
|
272
|
+
})
|
|
273
|
+
}),
|
|
274
|
+
}), 'dsh-subscriptions: /models')
|
|
275
|
+
|
|
242
276
|
ctx.effect(() => ctx.webServer.register({
|
|
243
277
|
kind: 'exact',
|
|
244
278
|
path: '/dsh-subscriptions/diagnostics',
|
|
@@ -507,4 +541,43 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
507
541
|
}),
|
|
508
542
|
}), 'dsh-subscriptions: /alerts')
|
|
509
543
|
|
|
544
|
+
|
|
545
|
+
// #511: Detailed cache and subscription analytics endpoint
|
|
546
|
+
ctx.effect(() => ctx.webServer.register({
|
|
547
|
+
kind: 'exact',
|
|
548
|
+
path: '/dsh-subscriptions/analytics',
|
|
549
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
550
|
+
if (req.method !== 'GET') {
|
|
551
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
555
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
const accounts = await accountsView()
|
|
559
|
+
const analytics = typeof history.getAnalytics === 'function'
|
|
560
|
+
? history.getAnalytics(accounts)
|
|
561
|
+
: calculateAnalytics(history.all(), accounts)
|
|
562
|
+
writeJson(res, 200, { ok: true, analytics })
|
|
563
|
+
}),
|
|
564
|
+
}), 'dsh-subscriptions: /analytics')
|
|
565
|
+
|
|
566
|
+
// #518: Concurrency and request queue status
|
|
567
|
+
ctx.effect(() => ctx.webServer.register({
|
|
568
|
+
kind: 'exact',
|
|
569
|
+
path: '/dsh-subscriptions/queue',
|
|
570
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
571
|
+
if (req.method !== 'GET') {
|
|
572
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
576
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
const queueStats = defaultConcurrencyQueue.stats()
|
|
580
|
+
writeJson(res, 200, { ok: true, queue: queueStats })
|
|
581
|
+
}),
|
|
582
|
+
}), 'dsh-subscriptions: /queue')
|
|
510
583
|
}
|