@goodandready/dsh-subscriptions 0.7.29 → 0.7.31
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-view.js +2 -0
- package/lib/accounts.js +20 -1
- package/lib/adapter-manager.js +3 -1
- package/lib/adapter.js +8 -1
- package/lib/client.js +123 -1
- package/lib/config-schema.js +7 -1
- package/lib/index.js +5 -2
- package/lib/models-catalog.js +12 -0
- package/lib/rotate.js +69 -7
- package/lib/stream-rotate.js +9 -3
- package/lib/subscriptions.js +3 -2
- package/package.json +1 -1
package/lib/accounts-view.js
CHANGED
|
@@ -145,6 +145,7 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
145
145
|
// Issue 303: expose the cooldown scope and quarantine so the card can
|
|
146
146
|
// explain why only some models are blocked.
|
|
147
147
|
cooldownFamilies: info.cooldownFamilies || null,
|
|
148
|
+
cooldownModels: info.cooldownModels || null,
|
|
148
149
|
quarantineUntil: info.quarantineUntil || 0,
|
|
149
150
|
quarantineReason: info.quarantineReason || null,
|
|
150
151
|
healthScore: info.healthScore,
|
|
@@ -166,6 +167,7 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
166
167
|
lastDoctorVerdict: info.lastDoctorVerdict || null,
|
|
167
168
|
availableModels: info.availableModels || null,
|
|
168
169
|
poolEnabled: slot.poolEnabled !== false,
|
|
170
|
+
isDefault: !!slot.isDefault,
|
|
169
171
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
170
172
|
capabilities,
|
|
171
173
|
billing,
|
package/lib/accounts.js
CHANGED
|
@@ -74,6 +74,7 @@ export function normalizeSlots(slots) {
|
|
|
74
74
|
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
75
75
|
...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
|
|
76
76
|
...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
|
|
77
|
+
...(slot.isDefault ? { isDefault: true } : {}),
|
|
77
78
|
...(Array.isArray(slot.poolModels)
|
|
78
79
|
? { poolModels: [...new Set(slot.poolModels.map((id) => String(id).trim()).filter(Boolean))] }
|
|
79
80
|
: {}),
|
|
@@ -82,6 +83,12 @@ export function normalizeSlots(slots) {
|
|
|
82
83
|
return out
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
export function defaultAccountRef(slots, provider) {
|
|
87
|
+
const list = normalizeSlots(slots).filter((slot) => slot.provider === provider)
|
|
88
|
+
const marked = list.find((slot) => slot.isDefault)
|
|
89
|
+
return (marked || list[0] || null)?.ref || null
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
export function vendorConfig(provider, cfg) {
|
|
86
93
|
const d = getVendor(provider).defaults()
|
|
87
94
|
const pick = (suffix, fallback) => {
|
|
@@ -323,6 +330,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
323
330
|
quarantineUntil: quarantines.get(ref) ? quarantines.get(ref).until : 0,
|
|
324
331
|
quarantineReason: quarantines.get(ref) ? quarantines.get(ref).reason : null,
|
|
325
332
|
cooldownFamilies: cooldowns.get(ref) && cooldowns.get(ref).families ? cooldowns.get(ref).families : null,
|
|
333
|
+
cooldownModels: cooldowns.get(ref) && cooldowns.get(ref).models ? cooldowns.get(ref).models : null,
|
|
326
334
|
quota: quotas.get(ref) || null,
|
|
327
335
|
pacePerHour: quotas.get(ref)?.pacePerHour || calculateBurnRatePerHour(quotaSamples.get(ref) || [], Date.now()),
|
|
328
336
|
samples: quotaSamples.get(ref) || [],
|
|
@@ -375,6 +383,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
375
383
|
usagePercent: info.usagePercent,
|
|
376
384
|
cooldownUntil: info.cooldownUntil,
|
|
377
385
|
cooldownFamilies: info.cooldownFamilies || null,
|
|
386
|
+
cooldownModels: info.cooldownModels || null,
|
|
378
387
|
quota: info.quota || null,
|
|
379
388
|
healthScore: computeHealthScore(info.health),
|
|
380
389
|
healthBadge: healthBadge(computeHealthScore(info.health)),
|
|
@@ -389,6 +398,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
389
398
|
weight: Number(slot.weight) || 0,
|
|
390
399
|
isPro: !!slot.isPro,
|
|
391
400
|
poolEnabled: slot.poolEnabled !== false,
|
|
401
|
+
isDefault: !!slot.isDefault,
|
|
392
402
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
393
403
|
})
|
|
394
404
|
}
|
|
@@ -583,7 +593,16 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
583
593
|
recordClockSkew,
|
|
584
594
|
// #87: family-scoped cooldowns. families=null blocks the whole account
|
|
585
595
|
// (legacy behavior); a list blocks only those model families.
|
|
586
|
-
rememberCooldown(ref, until, families) {
|
|
596
|
+
rememberCooldown(ref, until, families, models = null) {
|
|
597
|
+
if (models && typeof models === 'object') {
|
|
598
|
+
const prev = cooldowns.get(ref)
|
|
599
|
+
cooldowns.set(ref, {
|
|
600
|
+
until: prev ? prev.until : 0,
|
|
601
|
+
families: prev ? prev.families : null,
|
|
602
|
+
models: { ...(prev && prev.models ? prev.models : {}), ...models },
|
|
603
|
+
})
|
|
604
|
+
return
|
|
605
|
+
}
|
|
587
606
|
if (!families || !families.length) { cooldowns.set(ref, { until: Number(until) || 0, families: null }); return }
|
|
588
607
|
const prev = cooldowns.get(ref)
|
|
589
608
|
const merged = new Set(families)
|
package/lib/adapter-manager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SubscriptionAdapter } from './adapter.js'
|
|
2
2
|
import { resolveFallbackVendor, mapFallbackModel } from './cascade.js'
|
|
3
|
-
import { normalizeSlots, vendorConfig } from './accounts.js'
|
|
3
|
+
import { defaultAccountRef, normalizeSlots, vendorConfig } from './accounts.js'
|
|
4
4
|
import { PROVIDERS, isProvider, oauthRef } from './refs.js'
|
|
5
5
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
6
6
|
import { getVendor } from './vendors/index.js'
|
|
@@ -41,6 +41,8 @@ export function createAdapterManager({
|
|
|
41
41
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
42
42
|
getDynamicModels: (ref) => store.getDynamicModels(ref),
|
|
43
43
|
rememberDynamicModels: (ref, models) => store.rememberDynamicModels(ref, models),
|
|
44
|
+
poolEnabled: () => live().poolEnabled !== false,
|
|
45
|
+
defaultAccountRef: (provider) => defaultAccountRef(live().slots, provider),
|
|
44
46
|
loadBlob: (ref) => store.loadBlob(ref),
|
|
45
47
|
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
46
48
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
package/lib/adapter.js
CHANGED
|
@@ -193,11 +193,18 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
193
193
|
options.signal,
|
|
194
194
|
),
|
|
195
195
|
}
|
|
196
|
+
const poolEnabled = typeof deps.poolEnabled === 'function' ? deps.poolEnabled() !== false : true
|
|
197
|
+
const defaultRef = typeof deps.defaultAccountRef === 'function' ? deps.defaultAccountRef(provider) : null
|
|
196
198
|
const streamOptions = {
|
|
197
199
|
...hydratedOptions,
|
|
198
200
|
...(provider === 'codex' && getSpeedMode(sessionId) === 'fast' ? { service_tier: 'priority' } : {}),
|
|
199
201
|
...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
|
|
200
202
|
...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
|
|
203
|
+
// #830: kill switch. With no default account, use a sentinel that
|
|
204
|
+
// intentionally matches nothing, so the error is explicit and there
|
|
205
|
+
// is no silent fallback to another account.
|
|
206
|
+
...(!poolEnabled ? { onlyRef: defaultRef || '__no_default_account__' } : {}),
|
|
207
|
+
...(defaultRef ? { defaultRef } : {}),
|
|
201
208
|
_decisionTrace: decisionTrace,
|
|
202
209
|
autoPacing: effectiveAutoPacing,
|
|
203
210
|
drainBeforeReset: effectiveDrainBeforeReset,
|
|
@@ -216,7 +223,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
216
223
|
webhookUrl: typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl,
|
|
217
224
|
fetchImpl: deps.fetchImpl || fetch,
|
|
218
225
|
onCooldown: (account) => {
|
|
219
|
-
if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
|
|
226
|
+
if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null, account.cooldownModels || null)
|
|
220
227
|
if (typeof deps.rememberQuarantine === 'function') {
|
|
221
228
|
const currentQ = typeof deps.getQuarantine === 'function' ? deps.getQuarantine(account.ref) : null
|
|
222
229
|
if (currentQ && currentQ.attempts && currentQ.attempts > 1) {
|
package/lib/client.js
CHANGED
|
@@ -158,6 +158,12 @@ window.__ModuleLoader__.load({
|
|
|
158
158
|
"cascadingFallbackOrderHint": "Sequence of alternative providers to try when primary accounts are exhausted.",
|
|
159
159
|
"rotationStrategy": "Account Balancing Strategy",
|
|
160
160
|
"rotationStrategyHint": "Method used to distribute requests among available accounts of a provider.",
|
|
161
|
+
"rotationQuotaAware": "Quota-aware (urgent first)",
|
|
162
|
+
"poolEnabled": "Automatic account pool",
|
|
163
|
+
"poolEnabledHint": "When off, requests use only the provider default account. No silent fallback to other accounts.",
|
|
164
|
+
"mediaPoolEnabled": "Automatic media pool",
|
|
165
|
+
"mediaPoolEnabledHint": "Controls image/video account rotation separately from the text pool.",
|
|
166
|
+
"defaultAccountHint": "Used for direct routing and when the automatic pool is off.",
|
|
161
167
|
"rotationRoundRobin": "Round-Robin (Balanced rotation)",
|
|
162
168
|
"rotationLeastLoaded": "Least-Loaded (Fair-share quota)",
|
|
163
169
|
"rotationPriority": "Priority (Sequential by index/weight)",
|
|
@@ -196,6 +202,10 @@ window.__ModuleLoader__.load({
|
|
|
196
202
|
"modelShowMore": "Show more",
|
|
197
203
|
"modelShowAll": "Show all",
|
|
198
204
|
"modelNoMatches": "No models match this search.",
|
|
205
|
+
"modelCatalogSourceHint": "Catalog source: live, cached or builtin",
|
|
206
|
+
"modelNamesTitle": "Model display names",
|
|
207
|
+
"modelNamesHint": "Presentation only: routing, reasoning and model ids stay unchanged. Reset restores the catalog name.",
|
|
208
|
+
"modelNameReset": "Reset",
|
|
199
209
|
"rotationDecisionsTitle": "Rotation & Routing Decisions",
|
|
200
210
|
"rotationDecisionsDesc": "Real-time decision trace explaining account selection, skips, and fallback reasons.",
|
|
201
211
|
"rotationAttempt": "attempt",
|
|
@@ -302,7 +312,10 @@ window.__ModuleLoader__.load({
|
|
|
302
312
|
"maxQueueSizeSlot": "Queue",
|
|
303
313
|
"save": "Save",
|
|
304
314
|
"saved": "Saved",
|
|
315
|
+
"cancel": "Cancel",
|
|
316
|
+
"unsavedChanges": "Unsaved changes",
|
|
305
317
|
"accountLabel": "Account",
|
|
318
|
+
"usageRemaining": "Remaining quota",
|
|
306
319
|
"plan": "Plan",
|
|
307
320
|
"storedAs": "Stored as",
|
|
308
321
|
"slashFast": "Toggle Codex Speed (fast/standard)",
|
|
@@ -544,6 +557,12 @@ window.__ModuleLoader__.load({
|
|
|
544
557
|
"cascadingFallbackOrderHint": "首选服务商耗尽时按此顺序依次尝试备选服务商。",
|
|
545
558
|
"rotationStrategy": "账户负载均衡策略",
|
|
546
559
|
"rotationStrategyHint": "在同一服务商的多个可用账户之间分发请求的方式。",
|
|
560
|
+
"rotationQuotaAware": "配额感知(优先紧急)",
|
|
561
|
+
"poolEnabled": "自动账户池",
|
|
562
|
+
"poolEnabledHint": "关闭后请求只使用服务商默认账户,不会静默回退到其他账户。",
|
|
563
|
+
"mediaPoolEnabled": "自动媒体池",
|
|
564
|
+
"mediaPoolEnabledHint": "独立控制图片/视频账户轮询,与文本池分开。",
|
|
565
|
+
"defaultAccountHint": "用于直接路由,以及关闭自动账户池时的请求。",
|
|
547
566
|
"rotationRoundRobin": "轮询均衡(标准交替)",
|
|
548
567
|
"rotationLeastLoaded": "最少负载优先(配额公平共享)",
|
|
549
568
|
"rotationPriority": "优先级顺序(按序号/权重顺序)",
|
|
@@ -582,6 +601,10 @@ window.__ModuleLoader__.load({
|
|
|
582
601
|
"modelShowMore": "显示更多",
|
|
583
602
|
"modelShowAll": "显示全部",
|
|
584
603
|
"modelNoMatches": "没有匹配的模型。",
|
|
604
|
+
"modelCatalogSourceHint": "目录来源:实时、缓存或内置",
|
|
605
|
+
"modelNamesTitle": "模型显示名称",
|
|
606
|
+
"modelNamesHint": "仅影响显示:路由、推理和模型 ID 不变。重置恢复目录名称。",
|
|
607
|
+
"modelNameReset": "重置",
|
|
585
608
|
"rotationDecisionsTitle": "轮询调度与路由决策",
|
|
586
609
|
"rotationDecisionsDesc": "实时决策追踪,透明展示每次请求的账号选择、跳过与回退原因。",
|
|
587
610
|
"rotationAttempt": "次尝试",
|
|
@@ -688,7 +711,10 @@ window.__ModuleLoader__.load({
|
|
|
688
711
|
"maxQueueSizeSlot": "队列",
|
|
689
712
|
"save": "保存",
|
|
690
713
|
"saved": "已保存",
|
|
714
|
+
"cancel": "取消",
|
|
715
|
+
"unsavedChanges": "有未保存的更改",
|
|
691
716
|
"accountLabel": "账户",
|
|
717
|
+
"usageRemaining": "剩余配额",
|
|
692
718
|
"plan": "套餐方案",
|
|
693
719
|
"storedAs": "存储标识",
|
|
694
720
|
"slashFast": "切换 Codex 加速模式(快速/标准)",
|
|
@@ -1407,6 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1407
1433
|
}
|
|
1408
1434
|
const [checking, setChecking] = React.useState({})
|
|
1409
1435
|
const [saved, setSaved] = React.useState(false)
|
|
1436
|
+
const [savedSnapshot, setSavedSnapshot] = React.useState(null)
|
|
1410
1437
|
const [tokenDraft, setTokenDraft] = React.useState({})
|
|
1411
1438
|
const [openHelp, setOpenHelp] = React.useState({})
|
|
1412
1439
|
const [err, setErr] = React.useState('')
|
|
@@ -1428,7 +1455,9 @@ window.__ModuleLoader__.load({
|
|
|
1428
1455
|
|
|
1429
1456
|
const applyPayload = (data) => {
|
|
1430
1457
|
if (data && data.revision != null) setRevision(data.revision)
|
|
1431
|
-
|
|
1458
|
+
const next = JSON.parse(JSON.stringify((data && data.config) || {}))
|
|
1459
|
+
setDraft(next)
|
|
1460
|
+
setSavedSnapshot(JSON.parse(JSON.stringify(next)))
|
|
1432
1461
|
setAccounts((data && data.accounts) || [])
|
|
1433
1462
|
setProviders((data && data.providers) || [])
|
|
1434
1463
|
setModelsCatalog((data && data.modelsCatalog) || [])
|
|
@@ -1480,6 +1509,18 @@ window.__ModuleLoader__.load({
|
|
|
1480
1509
|
return () => timers.forEach(clearInterval)
|
|
1481
1510
|
}, [device])
|
|
1482
1511
|
|
|
1512
|
+
const dirty = Boolean(draft && savedSnapshot && JSON.stringify(draft) !== JSON.stringify(savedSnapshot))
|
|
1513
|
+
React.useEffect(() => {
|
|
1514
|
+
if (!dirty) return undefined
|
|
1515
|
+
const warn = (event) => {
|
|
1516
|
+
event.preventDefault()
|
|
1517
|
+
event.returnValue = ''
|
|
1518
|
+
return ''
|
|
1519
|
+
}
|
|
1520
|
+
window.addEventListener('beforeunload', warn)
|
|
1521
|
+
return () => window.removeEventListener('beforeunload', warn)
|
|
1522
|
+
}, [dirty])
|
|
1523
|
+
|
|
1483
1524
|
// #81: settings snapshot status - never render phantom inputs before the
|
|
1484
1525
|
// config snapshot arrives, and offer a retry when the store is unavailable.
|
|
1485
1526
|
if (!draft) {
|
|
@@ -2366,6 +2407,7 @@ h('div', { className: 'dsub-row' },
|
|
|
2366
2407
|
m.provider ? h('span', { className: 'dsub-brand dsub-brand-' + m.provider, style: { fontSize: '9px', padding: '1px 4px' } }, m.provider.toUpperCase()) : null,
|
|
2367
2408
|
h('code', null, m.name || m.id),
|
|
2368
2409
|
tags.length ? h('span', { className: 'dsub-model-tag' }, tags.join(' · ')) : null,
|
|
2410
|
+
Array.isArray(m.catalogSources) && m.catalogSources.length ? h('span', { className: 'dsub-model-tag', title: t('modelCatalogSourceHint') }, '📚 ' + m.catalogSources.join('/')) : null,
|
|
2369
2411
|
h('span', { className: 'dsub-model-tag ' + statusClass }, poolLabel),
|
|
2370
2412
|
)
|
|
2371
2413
|
})
|
|
@@ -2382,6 +2424,46 @@ h('div', { className: 'dsub-row' },
|
|
|
2382
2424
|
}, t('modelShowAll')),
|
|
2383
2425
|
) : null,
|
|
2384
2426
|
),
|
|
2427
|
+
h('details', { className: 'dsub-group', 'data-pane': 'models' },
|
|
2428
|
+
h('summary', { className: 'dsub-groupHead' }, '🏷 ' + t('modelNamesTitle')),
|
|
2429
|
+
h('div', { className: 'dsub-sub', style: { marginTop: 4 } }, t('modelNamesHint')),
|
|
2430
|
+
h('div', { style: { maxHeight: 260, overflow: 'auto', marginTop: 6 } },
|
|
2431
|
+
catalogShown.slice(0, 50).map((model) => {
|
|
2432
|
+
const current = (Array.isArray(draft.modelDisplayNames) ? draft.modelDisplayNames : [])
|
|
2433
|
+
.find((row) => row && row.provider === model.provider && String(row.id) === String(model.id))
|
|
2434
|
+
return h('div', { key: 'name:' + model.provider + ':' + model.id, className: 'dsub-row', style: { gap: 6 } },
|
|
2435
|
+
h('code', { style: { minWidth: 120, fontSize: 11 } }, model.id),
|
|
2436
|
+
h('input', {
|
|
2437
|
+
className: 'dsub-inp dsub-grow',
|
|
2438
|
+
value: current ? current.name : '',
|
|
2439
|
+
placeholder: model.name || model.id,
|
|
2440
|
+
onChange: (e) => {
|
|
2441
|
+
const value = e.target.value
|
|
2442
|
+
setDraft((d) => {
|
|
2443
|
+
const list = Array.isArray(d.modelDisplayNames) ? d.modelDisplayNames.slice() : []
|
|
2444
|
+
const idx = list.findIndex((row) => row && row.provider === model.provider && String(row.id) === String(model.id))
|
|
2445
|
+
if (value.trim()) {
|
|
2446
|
+
const next = { provider: model.provider, id: String(model.id), name: value.slice(0, 80) }
|
|
2447
|
+
if (idx >= 0) list[idx] = next
|
|
2448
|
+
else list.push(next)
|
|
2449
|
+
} else if (idx >= 0) {
|
|
2450
|
+
list.splice(idx, 1)
|
|
2451
|
+
}
|
|
2452
|
+
return Object.assign({}, d, { modelDisplayNames: list })
|
|
2453
|
+
})
|
|
2454
|
+
},
|
|
2455
|
+
}),
|
|
2456
|
+
h('button', {
|
|
2457
|
+
type: 'button', className: 'dsub-mini',
|
|
2458
|
+
onClick: () => setDraft((d) => Object.assign({}, d, {
|
|
2459
|
+
modelDisplayNames: (Array.isArray(d.modelDisplayNames) ? d.modelDisplayNames : [])
|
|
2460
|
+
.filter((row) => !(row && row.provider === model.provider && String(row.id) === String(model.id))),
|
|
2461
|
+
})),
|
|
2462
|
+
}, t('modelNameReset')),
|
|
2463
|
+
)
|
|
2464
|
+
})
|
|
2465
|
+
),
|
|
2466
|
+
),
|
|
2385
2467
|
h('details', { className: 'dsub-group', 'data-pane': 'models' },
|
|
2386
2468
|
h('summary', { className: 'dsub-groupHead' }, '🧠 ' + t('reasoningEffortTitle')),
|
|
2387
2469
|
h('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('reasoningEffortDesc')),
|
|
@@ -2489,10 +2571,33 @@ h('div', { className: 'dsub-row' },
|
|
|
2489
2571
|
},
|
|
2490
2572
|
h('option', { value: 'round-robin' }, t('rotationRoundRobin')),
|
|
2491
2573
|
h('option', { value: 'least-loaded' }, t('rotationLeastLoaded')),
|
|
2574
|
+
h('option', { value: 'quota-aware' }, t('rotationQuotaAware')),
|
|
2492
2575
|
h('option', { value: 'priority' }, t('rotationPriority')),
|
|
2493
2576
|
),
|
|
2494
2577
|
),
|
|
2495
2578
|
),
|
|
2579
|
+
h('div', { className: 'dsub-row', style: { marginTop: 12 } },
|
|
2580
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' } },
|
|
2581
|
+
h('input', {
|
|
2582
|
+
type: 'checkbox',
|
|
2583
|
+
checked: draft.poolEnabled !== false,
|
|
2584
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { poolEnabled: e.target.checked })),
|
|
2585
|
+
}),
|
|
2586
|
+
h('span', null, t('poolEnabled')),
|
|
2587
|
+
),
|
|
2588
|
+
h('div', { className: 'dsub-sub' }, t('poolEnabledHint')),
|
|
2589
|
+
),
|
|
2590
|
+
h('div', { className: 'dsub-row', style: { marginTop: 8 } },
|
|
2591
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' } },
|
|
2592
|
+
h('input', {
|
|
2593
|
+
type: 'checkbox',
|
|
2594
|
+
checked: draft.mediaPoolEnabled !== false,
|
|
2595
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { mediaPoolEnabled: e.target.checked })),
|
|
2596
|
+
}),
|
|
2597
|
+
h('span', null, t('mediaPoolEnabled')),
|
|
2598
|
+
),
|
|
2599
|
+
h('div', { className: 'dsub-sub' }, t('mediaPoolEnabledHint')),
|
|
2600
|
+
),
|
|
2496
2601
|
h('div', { className: 'dsub-row', style: { marginTop: 12 } },
|
|
2497
2602
|
h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' } },
|
|
2498
2603
|
h('input', {
|
|
@@ -2781,6 +2886,14 @@ h('div', { className: 'dsub-row' },
|
|
|
2781
2886
|
return h('div', { className: 'dsub-card', key: key },
|
|
2782
2887
|
h('div', { className: 'dsub-row', style: { alignItems: 'center' } },
|
|
2783
2888
|
avatarEl,
|
|
2889
|
+
h('label', { className: 'dsub-ref-tag', title: t('defaultAccountHint') },
|
|
2890
|
+
h('input', {
|
|
2891
|
+
type: 'checkbox',
|
|
2892
|
+
checked: !!row.slot.isDefault,
|
|
2893
|
+
onChange: (e) => updateSlot(row.slot.provider, row.slot.index, { isDefault: e.target.checked }),
|
|
2894
|
+
}),
|
|
2895
|
+
' ★'
|
|
2896
|
+
),
|
|
2784
2897
|
h('input', {
|
|
2785
2898
|
className: 'dsub-grow',
|
|
2786
2899
|
value: row.slot.label || '',
|
|
@@ -2865,6 +2978,9 @@ h('div', { className: 'dsub-row' },
|
|
|
2865
2978
|
},
|
|
2866
2979
|
}, '\u00d7'),
|
|
2867
2980
|
),
|
|
2981
|
+
(account && (account.quota?.remainingPercent != null || account.usagePercent != null)) ? h('div', { className: 'dsub-sub', style: { marginTop: 4 } },
|
|
2982
|
+
'📊 ' + t('usageRemaining') + ' ' + Math.round(account.quota?.remainingPercent != null ? Number(account.quota.remainingPercent) : (100 - Number(account.usagePercent))) + '%'
|
|
2983
|
+
) : null,
|
|
2868
2984
|
(account && account.quarantineUntil && Number(account.quarantineUntil) > Date.now()) ? h('div', { className: 'dsub-row', style: { marginTop: 4 } },
|
|
2869
2985
|
h('span', { className: 'dsub-sub', style: { color: 'var(--dsw-alias-state-error-primary)', fontVariantNumeric: 'tabular-nums' } },
|
|
2870
2986
|
'⛔ ' + (account.quarantineReason || 'QUARANTINE') + ' · ' + t('resetsIn') + ' ' + formatRelativeReset(account.quarantineUntil, t('lang'), Date.now())
|
|
@@ -3495,6 +3611,12 @@ h('div', { className: 'dsub-row' },
|
|
|
3495
3611
|
type: 'button', className: 'dsub-save',
|
|
3496
3612
|
onClick: () => save().catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
3497
3613
|
}, t('save')),
|
|
3614
|
+
h('button', {
|
|
3615
|
+
type: 'button', className: 'dsub-mini',
|
|
3616
|
+
disabled: !dirty,
|
|
3617
|
+
onClick: () => reload().catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
3618
|
+
}, t('cancel')),
|
|
3619
|
+
dirty ? h('span', { className: 'dsub-sub' }, t('unsavedChanges')) : null,
|
|
3498
3620
|
saved ? h('span', { className: 'dsub-ok' }, t('saved')) : null,
|
|
3499
3621
|
info ? h('span', { className: 'dsub-ok' }, info) : null,
|
|
3500
3622
|
err ? h('span', { className: 'dsub-bad' }, err) : null,
|
package/lib/config-schema.js
CHANGED
|
@@ -36,6 +36,8 @@ export const Slot = z.object({
|
|
|
36
36
|
.description('#171 Treat this account as Pro for weighted rotation.'),
|
|
37
37
|
poolEnabled: z.boolean().default(true)
|
|
38
38
|
.description('#795 Include this account in automatic pool selection. false keeps it available for explicit/manual use only.'),
|
|
39
|
+
isDefault: z.boolean().default(false)
|
|
40
|
+
.description('#829 Mark this account as the provider default for direct/fallback routing.'),
|
|
39
41
|
poolModels: z.array(z.string()).default(undefined)
|
|
40
42
|
.description('#795 Model allowlist for automatic pool selection. Absent = every model, empty = none, explicit list = only those ids.'),
|
|
41
43
|
})
|
|
@@ -154,7 +156,11 @@ export const Config = z.object({
|
|
|
154
156
|
cascadingFallbackOrder: z.array(z.string()).volatile().default(PROVIDERS)
|
|
155
157
|
.description('Configurable global priority sequence for cross-vendor fallback.'),
|
|
156
158
|
rotationStrategy: z.string().volatile().default('round-robin')
|
|
157
|
-
.description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), or priority (sequential).'),
|
|
159
|
+
.description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), quota-aware (urgent quota first), or priority (sequential).'),
|
|
160
|
+
poolEnabled: z.boolean().volatile().default(true)
|
|
161
|
+
.description('#830 Master switch for automatic account rotation. Off routes only to the provider default account and never silently falls back.'),
|
|
162
|
+
mediaPoolEnabled: z.boolean().volatile().default(true)
|
|
163
|
+
.description('#830 Separate switch for the image/video account pool.'),
|
|
158
164
|
enableSoftFailureSentinel: z.boolean().volatile().default(true)
|
|
159
165
|
.description('Intercept HTTP 200 soft-failure capacity apologies and automatically cascade to next healthy account.'),
|
|
160
166
|
cascadingChain: z.any().volatile().default({})
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { registerCustomProviderIds, registerDisplayName } from './refs.js'
|
|
|
17
17
|
import { createSubscriptionsService } from './subscriptions.js'
|
|
18
18
|
import { registerSubscriptionTools } from './native-tools.js'
|
|
19
19
|
import { detectConflicts, readProfilePluginNames, setActiveConflicts } from './coexistence.js'
|
|
20
|
-
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
20
|
+
import { createAccountStore, defaultAccountRef, normalizeSlots, vendorConfig } from './accounts.js'
|
|
21
21
|
import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
|
|
22
22
|
import { createResetCreditService } from './reset-credits.js'
|
|
23
23
|
import { maskEmail, maskLabel } from './mask.js'
|
|
@@ -389,12 +389,15 @@ export function apply(ctx, rawConfig) {
|
|
|
389
389
|
throw new Error(`unknown subscription provider: ${provider}`)
|
|
390
390
|
}
|
|
391
391
|
const accounts = await store.listAccounts(provider)
|
|
392
|
+
const mediaPool = live().mediaPoolEnabled !== false
|
|
393
|
+
const onlyRef = mediaPool ? null : defaultAccountRef(live().slots, provider)
|
|
392
394
|
const eligible = pickAccount(accounts || [], Date.now(), {
|
|
393
395
|
family: provider === 'codex' ? 'gpt' : 'grok',
|
|
394
396
|
drainBeforeReset: live().drainBeforeReset,
|
|
395
397
|
drainSafetyPercent: live().drainSafetyPercent,
|
|
398
|
+
...(onlyRef ? { onlyRef } : {}),
|
|
396
399
|
})
|
|
397
|
-
const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken && !poolBlockReason(row, {}))
|
|
400
|
+
const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken && !poolBlockReason(row, {}) && (!onlyRef || row.ref === onlyRef))
|
|
398
401
|
if (!slot) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
|
|
399
402
|
const raw = await store.resolveRaw(slot.ref)
|
|
400
403
|
if (!raw) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
|
package/lib/models-catalog.js
CHANGED
|
@@ -279,6 +279,17 @@ export function buildEnrichedCatalog({
|
|
|
279
279
|
|
|
280
280
|
const modelName = (typeof item === 'object' && (item.name || item.display_name)) ? (item.name || item.display_name) : modelId
|
|
281
281
|
const caps = inferModelCapabilities(provId, modelId)
|
|
282
|
+
const catalogSources = new Set()
|
|
283
|
+
for (const account of accounts) {
|
|
284
|
+
if (!account || String(account.provider || '').toLowerCase() !== String(provId).toLowerCase()) continue
|
|
285
|
+
const available = Array.isArray(account.availableModels) ? account.availableModels : []
|
|
286
|
+
const hasModel = available.some((model) => {
|
|
287
|
+
const id = typeof model === 'string' ? model : (model && (model.id || model.slug || model.name))
|
|
288
|
+
return String(id || '') === String(modelId)
|
|
289
|
+
})
|
|
290
|
+
if (hasModel) catalogSources.add(account.modelsSource || 'live')
|
|
291
|
+
}
|
|
292
|
+
if (!catalogSources.size) catalogSources.add('builtin')
|
|
282
293
|
|
|
283
294
|
result.push({
|
|
284
295
|
id: modelId,
|
|
@@ -288,6 +299,7 @@ export function buildEnrichedCatalog({
|
|
|
288
299
|
contextWindow: caps.contextWindow,
|
|
289
300
|
contextLabel: caps.contextLabel,
|
|
290
301
|
capabilities: caps.capabilities,
|
|
302
|
+
catalogSources: Array.from(catalogSources),
|
|
291
303
|
pool,
|
|
292
304
|
})
|
|
293
305
|
}
|
package/lib/rotate.js
CHANGED
|
@@ -50,7 +50,10 @@ export function modelFamily(provider, model) {
|
|
|
50
50
|
return 'standard'
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function cooldownBlocks(acc, family) {
|
|
53
|
+
export function cooldownBlocks(acc, family, model, nowMs = Date.now()) {
|
|
54
|
+
const modelUntil = acc && model && acc.cooldownModels && acc.cooldownModels[model]
|
|
55
|
+
if (modelUntil && Number(modelUntil) > Number(nowMs)) return true
|
|
56
|
+
if (model && acc && acc.cooldownModels && !acc.cooldownUntil) return false
|
|
54
57
|
const fams = acc && acc.cooldownFamilies
|
|
55
58
|
if (!Array.isArray(fams) || !fams.length) return true
|
|
56
59
|
if (!family) return true
|
|
@@ -79,6 +82,26 @@ export function getAccountRemainingPercent(acc) {
|
|
|
79
82
|
return null
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
export function earliestRecovery(accounts, nowMs = Date.now()) {
|
|
86
|
+
const now = Number(nowMs) || 0
|
|
87
|
+
let best = null
|
|
88
|
+
for (const account of Array.isArray(accounts) ? accounts : []) {
|
|
89
|
+
if (!account) continue
|
|
90
|
+
const candidates = []
|
|
91
|
+
if (account.cooldownUntil) candidates.push({ at: Number(account.cooldownUntil), reason: 'cooldown' })
|
|
92
|
+
if (account.quarantineUntil) candidates.push({ at: Number(account.quarantineUntil), reason: account.quarantineReason || 'quarantine' })
|
|
93
|
+
if (account.quota && account.quota.resetAt) candidates.push({ at: Number(account.quota.resetAt), reason: 'quota_reset' })
|
|
94
|
+
for (const [model, until] of Object.entries(account.cooldownModels || {})) {
|
|
95
|
+
if (Number(until) > now) candidates.push({ at: Number(until), reason: 'model_cooldown', model })
|
|
96
|
+
}
|
|
97
|
+
for (const item of candidates) {
|
|
98
|
+
if (!Number.isFinite(item.at) || item.at <= now) continue
|
|
99
|
+
if (!best || item.at < best.at) best = { ref: account.ref || account.id || 'unknown', ...item }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return best
|
|
103
|
+
}
|
|
104
|
+
|
|
82
105
|
export function poolBlockReason(account, opts) {
|
|
83
106
|
if (!account) return null
|
|
84
107
|
if (account.poolEnabled === false) return 'pool_disabled'
|
|
@@ -93,7 +116,11 @@ export function poolBlockReason(account, opts) {
|
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
96
|
-
const
|
|
119
|
+
const allAccounts = Array.isArray(accounts) ? accounts : []
|
|
120
|
+
const onlyRef = opts && opts.onlyRef
|
|
121
|
+
const list = onlyRef
|
|
122
|
+
? allAccounts.filter((account) => account && (account.ref || account.id) === onlyRef)
|
|
123
|
+
: allAccounts
|
|
97
124
|
const now = Number(nowMs) || 0
|
|
98
125
|
const thrRaw = opts && opts.switchAtRemaining != null ? Number(opts.switchAtRemaining) : 0
|
|
99
126
|
const thr = Number.isFinite(thrRaw) ? thrRaw : 0
|
|
@@ -142,7 +169,8 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
142
169
|
pinned: true,
|
|
143
170
|
})
|
|
144
171
|
} else if (pinned && pinned.hasToken && !isQuarantined(pinned, now)) {
|
|
145
|
-
const
|
|
172
|
+
const pinnedModelCooling = opts && opts.model && pinned.cooldownModels && Number(pinned.cooldownModels[opts.model] || 0) > now
|
|
173
|
+
const isCooldown = pinnedModelCooling || (pinned.cooldownUntil && Number(pinned.cooldownUntil) > now && cooldownBlocks(pinned, opts && opts.family, opts && opts.model, now))
|
|
146
174
|
const isPinnedZero = Boolean(pinned.quota && (
|
|
147
175
|
(pinned.quota.remaining != null && Number(pinned.quota.remaining) <= 0) ||
|
|
148
176
|
(pinned.quota.remainingPercent != null && Number(pinned.quota.remainingPercent) <= 0)
|
|
@@ -252,9 +280,17 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
252
280
|
continue
|
|
253
281
|
}
|
|
254
282
|
|
|
255
|
-
const
|
|
283
|
+
const modelCooling = opts && opts.model && acc.cooldownModels && Number(acc.cooldownModels[opts.model] || 0) > now
|
|
284
|
+
const isCooldown = modelCooling || (acc.cooldownUntil && Number(acc.cooldownUntil) > now && cooldownBlocks(acc, opts && opts.family, opts && opts.model, now))
|
|
256
285
|
if (isCooldown) {
|
|
257
|
-
evaluations.push({
|
|
286
|
+
evaluations.push({
|
|
287
|
+
ref,
|
|
288
|
+
eligible: false,
|
|
289
|
+
tier: null,
|
|
290
|
+
reason: modelCooling ? 'model_cooldown' : 'cooldown',
|
|
291
|
+
until: modelCooling ? acc.cooldownModels[opts.model] : acc.cooldownUntil,
|
|
292
|
+
...(modelCooling ? { model: opts.model } : {}),
|
|
293
|
+
})
|
|
258
294
|
continue
|
|
259
295
|
}
|
|
260
296
|
|
|
@@ -333,6 +369,11 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
333
369
|
const aIdx = list.indexOf(a)
|
|
334
370
|
const bIdx = list.indexOf(b)
|
|
335
371
|
if (aIdx !== -1 && bIdx !== -1 && aIdx !== bIdx) return aIdx - bIdx
|
|
372
|
+
} else if (strategy === 'quota-aware') {
|
|
373
|
+
const floor = opts && opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5
|
|
374
|
+
const aUrgency = calculateBurnUrgency(getAccountRemainingPercent(a), a.quota && a.quota.resetAt, now, floor)
|
|
375
|
+
const bUrgency = calculateBurnUrgency(getAccountRemainingPercent(b), b.quota && b.quota.resetAt, now, floor)
|
|
376
|
+
if (Math.abs(aUrgency - bUrgency) > 0.01) return bUrgency - aUrgency
|
|
336
377
|
}
|
|
337
378
|
|
|
338
379
|
// 0. Auto-Pacing: lower pacing risk preferred (#352)
|
|
@@ -358,7 +399,17 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
358
399
|
return (b.healthScore ?? 100) - (a.healthScore ?? 100)
|
|
359
400
|
})
|
|
360
401
|
|
|
361
|
-
|
|
402
|
+
let chosen = tier[0]
|
|
403
|
+
const strategy = (opts && opts.rotationStrategy) || 'round-robin'
|
|
404
|
+
if (strategy === 'quota-aware' && opts && opts.defaultRef) {
|
|
405
|
+
const byDefault = tier.find((account) => (account.ref || account.id) === opts.defaultRef)
|
|
406
|
+
if (byDefault && byDefault !== chosen) {
|
|
407
|
+
const floor = opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5
|
|
408
|
+
const chosenUrgency = calculateBurnUrgency(getAccountRemainingPercent(chosen), chosen.quota && chosen.quota.resetAt, now, floor)
|
|
409
|
+
const defaultUrgency = calculateBurnUrgency(getAccountRemainingPercent(byDefault), byDefault.quota && byDefault.quota.resetAt, now, floor)
|
|
410
|
+
if (defaultUrgency >= chosenUrgency - 5) chosen = byDefault
|
|
411
|
+
}
|
|
412
|
+
}
|
|
362
413
|
const chosenRef = chosen.ref || chosen.id
|
|
363
414
|
if (targetSession && chosen) {
|
|
364
415
|
pinSession(targetSession, chosenRef, {
|
|
@@ -382,6 +433,8 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
382
433
|
resetAt: chosen.quota?.resetAt || null,
|
|
383
434
|
weight: chosen.weight || (chosen.isPro ? 5 : 1),
|
|
384
435
|
healthScore: chosen.healthScore ?? 100,
|
|
436
|
+
urgency: calculateBurnUrgency(getAccountRemainingPercent(chosen), chosen.quota && chosen.quota.resetAt, now, opts && opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5),
|
|
437
|
+
defaultAccount: Boolean(opts && opts.defaultRef && chosenRef === opts.defaultRef),
|
|
385
438
|
},
|
|
386
439
|
},
|
|
387
440
|
}
|
|
@@ -405,12 +458,21 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
405
458
|
return res ? res.account : null
|
|
406
459
|
}
|
|
407
460
|
|
|
408
|
-
export function markCooldown(account, nowMs, cooldownMs, family, customUntil = null) {
|
|
461
|
+
export function markCooldown(account, nowMs, cooldownMs, family, customUntil = null, model = null) {
|
|
409
462
|
const wait = Number(cooldownMs)
|
|
410
463
|
const ms = Number.isFinite(wait) && wait > 0 ? wait : 30 * 60 * 1000
|
|
411
464
|
const until = (customUntil && Number(customUntil) > (Number(nowMs) || 0))
|
|
412
465
|
? Number(customUntil)
|
|
413
466
|
: ((Number(nowMs) || 0) + ms)
|
|
467
|
+
// #814: Claude/Antigravity quotas are model-specific. Auth/session errors
|
|
468
|
+
// are handled elsewhere as account-wide; ordinary model quota failures stay
|
|
469
|
+
// scoped to the failing model.
|
|
470
|
+
if (model && (account.provider === 'claude' || account.provider === 'antigravity')) {
|
|
471
|
+
return {
|
|
472
|
+
...account,
|
|
473
|
+
cooldownModels: { ...(account.cooldownModels || {}), [model]: until },
|
|
474
|
+
}
|
|
475
|
+
}
|
|
414
476
|
const prev = Array.isArray(account.cooldownFamilies) ? account.cooldownFamilies.slice() : []
|
|
415
477
|
const fams = family ? (prev.includes(family) ? prev : prev.concat(family)) : prev
|
|
416
478
|
return {
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { inspectAccountCapabilities, normalizeCapabilitiesRequest } from './capabilities.js'
|
|
2
2
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
3
3
|
import { recordSessionCacheUsage } from './session-pin.js'
|
|
4
|
-
import { pickAccountWithTrace, markCooldown, isSwitchableError, isRegionError, isAuthError, modelFamily } from './rotate.js'
|
|
4
|
+
import { pickAccountWithTrace, markCooldown, isSwitchableError, isRegionError, isAuthError, modelFamily, earliestRecovery } from './rotate.js'
|
|
5
5
|
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED, REASON_DEGRADED } from './quarantine.js'
|
|
6
6
|
import { notifySessionExpired, notifyQuarantineEntered } from './alerts.js'
|
|
7
7
|
import { withRunawayGuard } from './runaway-guard.js'
|
|
@@ -52,6 +52,8 @@ export async function* streamWithRotation({
|
|
|
52
52
|
const pickRes = pickAccountWithTrace(pool, nowMs(), {
|
|
53
53
|
switchAtRemaining,
|
|
54
54
|
model: options && options.model,
|
|
55
|
+
onlyRef: options && options.onlyRef,
|
|
56
|
+
defaultRef: options && options.defaultRef,
|
|
55
57
|
family: modelFamily(options && options.provider, options && options.model),
|
|
56
58
|
sessionId: options && options.sessionId,
|
|
57
59
|
tag: options && options.tag,
|
|
@@ -96,10 +98,13 @@ export async function* streamWithRotation({
|
|
|
96
98
|
lastError.retryAfterMs = Math.min(wait, maxWait)
|
|
97
99
|
lastError.resetAt = nowMs() + lastError.retryAfterMs
|
|
98
100
|
}
|
|
101
|
+
lastError.recovery = lastError.recovery || earliestRecovery(pool, nowMs())
|
|
99
102
|
throw lastError
|
|
100
103
|
}
|
|
101
|
-
const err = /** @type {Error & { code?: string }} */ (new Error('no usable subscription account for this provider'))
|
|
104
|
+
const err = /** @type {Error & { code?: string, recovery?: any, onlyRef?: string }} */ (new Error('no usable subscription account for this provider'))
|
|
102
105
|
err.code = 'RATE_LIMIT'
|
|
106
|
+
err.recovery = earliestRecovery(pool, nowMs())
|
|
107
|
+
if (options && options.onlyRef) err.onlyRef = options.onlyRef
|
|
103
108
|
throw err
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -233,9 +238,10 @@ export async function* streamWithRotation({
|
|
|
233
238
|
|
|
234
239
|
// Move slot to cooldown and quarantine (#172, #538)
|
|
235
240
|
const effectiveCooldownMs = isSoft ? Math.min(cooldownMs, 5 * 60 * 1000) : (isAuth ? 86400000 : cooldownMs)
|
|
236
|
-
const cooled = markCooldown(account, nowMs(), effectiveCooldownMs, modelFamily(options && options.provider, options && options.model), accurateUntil)
|
|
241
|
+
const cooled = markCooldown(account, nowMs(), effectiveCooldownMs, modelFamily(options && options.provider, options && options.model), accurateUntil, options && options.model)
|
|
237
242
|
account.cooldownUntil = cooled.cooldownUntil
|
|
238
243
|
if (cooled.cooldownFamilies) account.cooldownFamilies = cooled.cooldownFamilies
|
|
244
|
+
if (cooled.cooldownModels) account.cooldownModels = cooled.cooldownModels
|
|
239
245
|
|
|
240
246
|
const status = Number(err && (err.status || err.statusCode) || 0)
|
|
241
247
|
const code = String(err && err.code || '')
|
package/lib/subscriptions.js
CHANGED
|
@@ -299,9 +299,10 @@ export function createSubscriptionsService(deps) {
|
|
|
299
299
|
currentAttempt.switchError = err ? (err.code || err.status || err.message) : 'switchable'
|
|
300
300
|
lastError = err
|
|
301
301
|
if (!isSwitchableError(err)) throw err
|
|
302
|
-
const cooled = markCooldown(account, Date.now(), cooldownMs, modelFamily(provider, body && body.model))
|
|
302
|
+
const cooled = markCooldown(account, Date.now(), cooldownMs, modelFamily(provider, body && body.model), null, body && body.model)
|
|
303
303
|
account.cooldownUntil = cooled.cooldownUntil
|
|
304
|
-
if (
|
|
304
|
+
if (cooled.cooldownModels) account.cooldownModels = cooled.cooldownModels
|
|
305
|
+
if (typeof deps.rememberCooldown === "function") deps.rememberCooldown(account.ref, account.cooldownUntil, cooled.cooldownFamilies || null, cooled.cooldownModels || null)
|
|
305
306
|
// try next account
|
|
306
307
|
}
|
|
307
308
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.31",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|