@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 +6 -0
- package/README.ru.md +6 -0
- package/README.zh.md +4 -0
- package/lib/analytics.js +338 -0
- package/lib/client.js +298 -2
- package/lib/concurrency-queue.js +222 -0
- package/lib/config-schema.js +8 -0
- package/lib/diagnostic-wizard.js +408 -0
- package/lib/diagnostics.js +6 -0
- package/lib/history.js +5 -0
- package/lib/rotate.js +23 -8
- package/lib/routes/status.js +76 -0
- package/lib/session-pin.js +216 -10
- package/lib/stream-rotate.js +43 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -122,6 +122,10 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
'.dsub-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}' +
|
|
123
123
|
'.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}' +
|
|
124
124
|
'.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
125
|
+
'.dsub-chart-wrap{display:flex;align-items:flex-end;gap:3px;height:52px;padding:6px 0;margin-top:8px;border-bottom:1px solid var(--dsw-alias-border-l3)}' +
|
|
126
|
+
'.dsub-chart-bar{flex:1;background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:3px;transition:height .2s ease;opacity:0.85;cursor:pointer}' +
|
|
127
|
+
'.dsub-chart-bar:hover{opacity:1;background:var(--dsw-alias-state-success-primary)}' +
|
|
128
|
+
'.dsub-chart-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}' +
|
|
125
129
|
'.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}' +
|
|
126
130
|
'.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}' +
|
|
127
131
|
'.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}' +
|
|
@@ -569,6 +573,42 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
569
573
|
"toastReleased": "Account active and healthy",
|
|
570
574
|
"toastQuotaWarning": "Quota threshold exceeded",
|
|
571
575
|
"enableToasts": "In-app toast alerts",
|
|
576
|
+
"groupQueue": "Concurrency & Request Queue",
|
|
577
|
+
"enableQueue": "Enable request queue",
|
|
578
|
+
"enableQueueHint": "Queue excess requests when account hits concurrency limit instead of failing with 429",
|
|
579
|
+
"accountMaxConcurrency": "Max concurrent streams per account",
|
|
580
|
+
"accountMaxConcurrencyHint": "Maximum simultaneous streaming requests allowed per account (default 2)",
|
|
581
|
+
"maxQueueSize": "Max queue depth",
|
|
582
|
+
"maxQueueSizeHint": "Maximum queued requests per account before rejecting with overflow (default 20)",
|
|
583
|
+
"queueTimeoutSec": "Queue wait timeout (seconds)",
|
|
584
|
+
"queueTimeoutSecHint": "Maximum wait time in queue before timeout rejection (default 30s)",
|
|
585
|
+
"analyticsTitle": "Subscription & Cache Analytics",
|
|
586
|
+
"analyticsDesc": "Cache hit rate, token caching efficiency, TTFT latency, generation speed, and 24h activity",
|
|
587
|
+
"ttftMetric": "TTFT Latency",
|
|
588
|
+
"speedMetric": "Speed",
|
|
589
|
+
"queueActive": "Active Streams",
|
|
590
|
+
"queueWaiting": "Queued",
|
|
591
|
+
"queueRejected": "Rejected",
|
|
592
|
+
"activity24h": "24-Hour Activity Distribution",
|
|
593
|
+
"forecastTitle": "Quota Exhaustion Forecast",
|
|
594
|
+
"forecastRunway": "Estimated runway",
|
|
595
|
+
"forecastBurnRate": "Burn rate",
|
|
596
|
+
"forecastExhaustWarning": "Quota projected to exhaust before reset window",
|
|
597
|
+
"wizardButton": "Diagnose",
|
|
598
|
+
"wizardTitle": "Diagnostic Wizard",
|
|
599
|
+
"wizardAll": "Account Wizard",
|
|
600
|
+
"wizardIncludeNetwork": "Include active network checks (proxy ping & model catalog discovery)",
|
|
601
|
+
"wizardRunning": "Running diagnostic checklist...",
|
|
602
|
+
"wizardStepCreds": "Credentials & Expiry",
|
|
603
|
+
"wizardStepProxy": "Proxy & Gateway",
|
|
604
|
+
"wizardStepToken": "Token Validation",
|
|
605
|
+
"wizardStepCatalog": "Model Catalog Reachability",
|
|
606
|
+
"wizardHealthy": "Healthy",
|
|
607
|
+
"wizardDegraded": "Degraded / Warnings",
|
|
608
|
+
"wizardFailing": "Issues Detected",
|
|
609
|
+
"wizardRecommendations": "Recommendations & Next Steps",
|
|
610
|
+
"wizardCopy": "Copy Masked Diagnostic Report",
|
|
611
|
+
"cachePinnedBadge": "Cache Pinned",
|
|
572
612
|
"field.enableToasts": "Show popup toast notifications on quarantine or quota events"
|
|
573
613
|
};
|
|
574
614
|
|
|
@@ -791,6 +831,42 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
791
831
|
"toastReleased": "账户已解除隔离恢复使用",
|
|
792
832
|
"toastQuotaWarning": "额度预警",
|
|
793
833
|
"enableToasts": "应用内弹窗提醒",
|
|
834
|
+
"groupQueue": "并发限制与请求队列",
|
|
835
|
+
"enableQueue": "启用并发请求排队",
|
|
836
|
+
"enableQueueHint": "当账户达到并发上限时将多余请求排队等待,避免直接触发 429 报错",
|
|
837
|
+
"accountMaxConcurrency": "单账户最大并发流数",
|
|
838
|
+
"accountMaxConcurrencyHint": "每个账户允许同时执行的最大流式请求数(默认 2)",
|
|
839
|
+
"maxQueueSize": "队列最大等待深度",
|
|
840
|
+
"maxQueueSizeHint": "每个账户允许排队的最大请求数,超限后拒绝执行(默认 20)",
|
|
841
|
+
"queueTimeoutSec": "排队超时时间(秒)",
|
|
842
|
+
"queueTimeoutSecHint": "在队列中等待槽位的最长时间,超时后自动放弃并回退(默认 30 秒)",
|
|
843
|
+
"analyticsTitle": "订阅与缓存深度分析",
|
|
844
|
+
"analyticsDesc": "缓存命中率、Token 缓存效率、首字响应延迟 (TTFT)、生成速度及 24 小时负载分布",
|
|
845
|
+
"ttftMetric": "首字响应延迟 (TTFT)",
|
|
846
|
+
"speedMetric": "生成速度",
|
|
847
|
+
"queueActive": "活跃流",
|
|
848
|
+
"queueWaiting": "排队中",
|
|
849
|
+
"queueRejected": "已拒绝",
|
|
850
|
+
"activity24h": "24 小时活动分布",
|
|
851
|
+
"forecastTitle": "额度消耗预测与预警",
|
|
852
|
+
"forecastRunway": "预计可用时间",
|
|
853
|
+
"forecastBurnRate": "消耗速率",
|
|
854
|
+
"forecastExhaustWarning": "预计在额度重置前耗尽",
|
|
855
|
+
"wizardButton": "诊断",
|
|
856
|
+
"wizardTitle": "账户诊断向导",
|
|
857
|
+
"wizardAll": "诊断向导",
|
|
858
|
+
"wizardIncludeNetwork": "包含活跃网络探测(代理连通性与模型目录探测)",
|
|
859
|
+
"wizardRunning": "正在执行诊断检查清单...",
|
|
860
|
+
"wizardStepCreds": "凭据完整性与有效期",
|
|
861
|
+
"wizardStepProxy": "网络代理与网关",
|
|
862
|
+
"wizardStepToken": "令牌有效性验证",
|
|
863
|
+
"wizardStepCatalog": "模型目录可达性",
|
|
864
|
+
"wizardHealthy": "健康正常",
|
|
865
|
+
"wizardDegraded": "降级预警",
|
|
866
|
+
"wizardFailing": "发现故障",
|
|
867
|
+
"wizardRecommendations": "排查建议与修复步骤",
|
|
868
|
+
"wizardCopy": "复制脱敏诊断报告",
|
|
869
|
+
"cachePinnedBadge": "缓存粘性",
|
|
794
870
|
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒"
|
|
795
871
|
};
|
|
796
872
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
@@ -1015,6 +1091,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1015
1091
|
const [checkRes, setCheckRes] = React.useState({})
|
|
1016
1092
|
const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
|
|
1017
1093
|
const [telemetry, setTelemetry] = React.useState(null)
|
|
1094
|
+
const [analytics, setAnalytics] = React.useState(null)
|
|
1095
|
+
const [queueStats, setQueueStats] = React.useState(null)
|
|
1018
1096
|
const [smoke, setSmoke] = React.useState({ testing: false, result: null })
|
|
1019
1097
|
const [updater, setUpdater] = React.useState({ checking: false, updating: false, status: null, message: '' })
|
|
1020
1098
|
const [backupModal, setBackupModal] = React.useState(null)
|
|
@@ -1023,7 +1101,31 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1023
1101
|
const [backupErr, setBackupErr] = React.useState('')
|
|
1024
1102
|
const [backupInfo, setBackupInfo] = React.useState('')
|
|
1025
1103
|
const [backupLoading, setBackupLoading] = React.useState(false)
|
|
1026
|
-
|
|
1104
|
+
const [wizardModal, setWizardModal] = React.useState(null)
|
|
1105
|
+
|
|
1106
|
+
const runWizard = async (provider, index, ref, includeNetwork = false) => {
|
|
1107
|
+
setWizardModal({
|
|
1108
|
+
provider: provider || 'all',
|
|
1109
|
+
index: index || 1,
|
|
1110
|
+
ref: ref || null,
|
|
1111
|
+
loading: true,
|
|
1112
|
+
report: null,
|
|
1113
|
+
includeNetwork,
|
|
1114
|
+
error: '',
|
|
1115
|
+
})
|
|
1116
|
+
try {
|
|
1117
|
+
const res = await fetch('/dsh-subscriptions/diagnostic-wizard', {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1120
|
+
body: JSON.stringify({ provider, index, ref, includeNetwork }),
|
|
1121
|
+
})
|
|
1122
|
+
const d = await res.json()
|
|
1123
|
+
if (!d || !d.ok) throw new Error((d && d.error && d.error.message) || ('HTTP ' + res.status))
|
|
1124
|
+
setWizardModal((m) => Object.assign({}, m, { loading: false, report: d.report, error: '' }))
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
setWizardModal((m) => Object.assign({}, m, { loading: false, error: cleanErrorMessage(e && e.message || e) }))
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1027
1129
|
const doExportBackup = async () => {
|
|
1028
1130
|
setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
|
|
1029
1131
|
try {
|
|
@@ -1117,6 +1219,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1117
1219
|
.then((r) => r.json())
|
|
1118
1220
|
.then((d) => { if (d && d.ok && d.telemetry) setTelemetry(d.telemetry) })
|
|
1119
1221
|
.catch(() => {})
|
|
1222
|
+
fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
|
|
1223
|
+
.then((r) => r.json())
|
|
1224
|
+
.then((d) => { if (d && d.ok && d.analytics) setAnalytics(d.analytics) })
|
|
1225
|
+
.catch(() => {})
|
|
1226
|
+
fetch('/dsh-subscriptions/queue', { cache: 'no-store' })
|
|
1227
|
+
.then((r) => r.json())
|
|
1228
|
+
.then((d) => { if (d && d.ok && d.queue) setQueueStats(d.queue) })
|
|
1229
|
+
.catch(() => {})
|
|
1120
1230
|
}
|
|
1121
1231
|
|
|
1122
1232
|
const runSmokeTest = async (provider, index) => {
|
|
@@ -1506,12 +1616,74 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1506
1616
|
React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLastActivity')),
|
|
1507
1617
|
),
|
|
1508
1618
|
) : null,
|
|
1619
|
+
(analytics && (analytics.totalRequests > 0 || (analytics.hourly && analytics.hourly.some(function(b) { return b.requests > 0 })))) || (queueStats && (queueStats.totalActive > 0 || queueStats.totalQueued > 0)) ? React.createElement('details', { className: 'dsub-group', open: true, style: { marginBottom: '14px' } },
|
|
1620
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '📊 ' + t('analyticsTitle')),
|
|
1621
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('analyticsDesc')),
|
|
1622
|
+
React.createElement('div', { className: 'dsub-grid-2', style: { marginTop: '10px' } },
|
|
1623
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1624
|
+
React.createElement('div', { className: 'dsub-stat-val' }, ((analytics && analytics.cache && analytics.cache.cacheHitRate) || 0) + '%'),
|
|
1625
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('cacheHitRate') + (analytics && analytics.cache ? ' (' + (analytics.cache.cacheReadTokens || 0).toLocaleString() + ' tok)' : '')),
|
|
1626
|
+
),
|
|
1627
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1628
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (analytics && analytics.performance && analytics.performance.ttft && analytics.performance.ttft.avgMs != null ? analytics.performance.ttft.avgMs + ' ms' : '—')),
|
|
1629
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('ttftMetric') + (analytics && analytics.performance && analytics.performance.ttft && analytics.performance.ttft.p95Ms != null ? ' · p95 ' + analytics.performance.ttft.p95Ms + ' ms' : '')),
|
|
1630
|
+
),
|
|
1631
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1632
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (analytics && analytics.performance && analytics.performance.tps && analytics.performance.tps.avg != null ? analytics.performance.tps.avg + ' tok/s' : '—')),
|
|
1633
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('speedMetric') + (analytics && analytics.performance && analytics.performance.tps && analytics.performance.tps.p95 != null ? ' · p95 ' + analytics.performance.tps.p95 : '')),
|
|
1634
|
+
),
|
|
1635
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1636
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (queueStats ? queueStats.totalActive : 0) + ' / ' + (queueStats ? queueStats.totalQueued : 0)),
|
|
1637
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('queueActive') + ' / ' + t('queueWaiting') + (queueStats && queueStats.metrics && queueStats.metrics.totalOverflowed ? ' · ' + queueStats.metrics.totalOverflowed + ' ' + t('queueRejected') : '')),
|
|
1638
|
+
),
|
|
1639
|
+
),
|
|
1640
|
+
analytics && Array.isArray(analytics.hourly) && analytics.hourly.length ? (function() {
|
|
1641
|
+
var maxReq = Math.max.apply(Math, [1].concat(analytics.hourly.map(function(b) { return b.requests || 0 })))
|
|
1642
|
+
return React.createElement('div', { style: { marginTop: '12px' } },
|
|
1643
|
+
React.createElement('div', { style: { fontSize: '11.5px', fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('activity24h')),
|
|
1644
|
+
React.createElement('div', { className: 'dsub-chart-wrap' },
|
|
1645
|
+
analytics.hourly.map(function(b, idx) {
|
|
1646
|
+
var pct = Math.max(4, Math.round(((b.requests || 0) / maxReq) * 100))
|
|
1647
|
+
var title = b.label + ': ' + b.requests + ' req (' + b.cacheReadTokens + ' cached tok, ' + b.errorCount + ' err)'
|
|
1648
|
+
return React.createElement('div', {
|
|
1649
|
+
key: idx,
|
|
1650
|
+
className: 'dsub-chart-bar',
|
|
1651
|
+
style: {
|
|
1652
|
+
height: pct + '%',
|
|
1653
|
+
backgroundColor: b.errorCount > 0 && b.requests === b.errorCount
|
|
1654
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
1655
|
+
: (b.cacheReadTokens > 0 ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-state-success-primary)'),
|
|
1656
|
+
},
|
|
1657
|
+
title: title,
|
|
1658
|
+
})
|
|
1659
|
+
})
|
|
1660
|
+
),
|
|
1661
|
+
React.createElement('div', { className: 'dsub-chart-labels' },
|
|
1662
|
+
React.createElement('span', null, '-24h'),
|
|
1663
|
+
React.createElement('span', null, '-18h'),
|
|
1664
|
+
React.createElement('span', null, '-12h'),
|
|
1665
|
+
React.createElement('span', null, '-6h'),
|
|
1666
|
+
React.createElement('span', null, 'Now'),
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
})() : null,
|
|
1670
|
+
analytics && Array.isArray(analytics.forecast) && analytics.forecast.some(function(f) { return f.willExhaustBeforeReset || (f.burnRateTokensPerHour > 0 && f.hoursToExhaustion != null && f.hoursToExhaustion < 72) }) ? React.createElement('div', { style: { marginTop: '12px' } },
|
|
1671
|
+
React.createElement('div', { style: { fontSize: '11.5px', fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary)', marginBottom: '6px' } }, '⚠️ ' + t('forecastTitle')),
|
|
1672
|
+
analytics.forecast.filter(function(f) { return f.willExhaustBeforeReset || (f.burnRateTokensPerHour > 0 && f.hoursToExhaustion != null && f.hoursToExhaustion < 72) }).map(function(f, idx) {
|
|
1673
|
+
return React.createElement('div', { key: idx, className: 'dsub-alert-bad', style: { marginBottom: '6px' } },
|
|
1674
|
+
(f.provider ? f.provider.toUpperCase() : 'LLM') + ' (' + (f.label || f.ref) + '): ' +
|
|
1675
|
+
(f.willExhaustBeforeReset ? t('forecastExhaustWarning') : (t('forecastRunway') + ': ~' + f.hoursToExhaustion + 'h')) +
|
|
1676
|
+
' · ' + t('forecastBurnRate') + ': ' + f.burnRateTokensPerHour.toLocaleString() + ' tok/h'
|
|
1677
|
+
)
|
|
1678
|
+
})
|
|
1679
|
+
) : null,
|
|
1680
|
+
) : null,
|
|
1509
1681
|
telemetry && Array.isArray(telemetry.recentDecisions) && telemetry.recentDecisions.length ? React.createElement('details', { className: 'dsub-group', style: { marginBottom: '14px' } },
|
|
1510
1682
|
React.createElement('summary', { className: 'dsub-groupHead' }, '🧭 ' + t('rotationDecisionsTitle')),
|
|
1511
1683
|
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('rotationDecisionsDesc')),
|
|
1512
1684
|
React.createElement('div', { className: 'dsub-trace-box' },
|
|
1513
1685
|
telemetry.recentDecisions.map((dec, idx) => {
|
|
1514
|
-
const reasonBadge = dec.cascaded ? ('🔀 ' + t('rotationCascaded')) : (dec.decisionReason === 'session_pinned' ? '📌 Pinned' : ('⚡ ' + dec.decisionReason))
|
|
1686
|
+
const reasonBadge = dec.cascaded ? ('🔀 ' + t('rotationCascaded')) : (dec.decisionReason === 'cache_pinned' ? ('📌 ' + t('cachePinnedBadge')) : (dec.decisionReason === 'session_pinned' ? '📌 Pinned' : ('⚡ ' + dec.decisionReason)))
|
|
1515
1687
|
return React.createElement('div', { key: idx, className: 'dsub-trace-row' },
|
|
1516
1688
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
1517
1689
|
dec.provider ? React.createElement('span', { className: 'dsub-brand dsub-brand-' + dec.provider, style: { fontSize: '9px', padding: '1px 4px' } }, dec.provider.toUpperCase()) : null,
|
|
@@ -1640,6 +1812,57 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1640
1812
|
React.createElement('div', { className: 'dsub-sub' },
|
|
1641
1813
|
t('expiryNotifyDaysHint')),
|
|
1642
1814
|
),
|
|
1815
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1816
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
|
|
1817
|
+
React.createElement('label', { className: 'dsub-row' },
|
|
1818
|
+
React.createElement('input', {
|
|
1819
|
+
type: 'checkbox',
|
|
1820
|
+
checked: draft.enableQueue !== false,
|
|
1821
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { enableQueue: e.target.checked }) }) },
|
|
1822
|
+
}),
|
|
1823
|
+
React.createElement('span', null, t('enableQueue')),
|
|
1824
|
+
),
|
|
1825
|
+
React.createElement('div', { className: 'dsub-sub' }, t('enableQueueHint')),
|
|
1826
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1827
|
+
React.createElement('span', { className: 'dsub-h' }, t('accountMaxConcurrency')),
|
|
1828
|
+
React.createElement('input', {
|
|
1829
|
+
type: 'number',
|
|
1830
|
+
className: 'dsub-inp',
|
|
1831
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1832
|
+
min: 1,
|
|
1833
|
+
max: 20,
|
|
1834
|
+
value: draft.accountMaxConcurrency != null ? draft.accountMaxConcurrency : 2,
|
|
1835
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { accountMaxConcurrency: Math.max(1, Number(e.target.value) || 2) }) }) },
|
|
1836
|
+
}),
|
|
1837
|
+
),
|
|
1838
|
+
React.createElement('div', { className: 'dsub-sub' }, t('accountMaxConcurrencyHint')),
|
|
1839
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1840
|
+
React.createElement('span', { className: 'dsub-h' }, t('maxQueueSize')),
|
|
1841
|
+
React.createElement('input', {
|
|
1842
|
+
type: 'number',
|
|
1843
|
+
className: 'dsub-inp',
|
|
1844
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1845
|
+
min: 1,
|
|
1846
|
+
max: 100,
|
|
1847
|
+
value: draft.maxQueueSize != null ? draft.maxQueueSize : 20,
|
|
1848
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { maxQueueSize: Math.max(1, Number(e.target.value) || 20) }) }) },
|
|
1849
|
+
}),
|
|
1850
|
+
),
|
|
1851
|
+
React.createElement('div', { className: 'dsub-sub' }, t('maxQueueSizeHint')),
|
|
1852
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1853
|
+
React.createElement('span', { className: 'dsub-h' }, t('queueTimeoutSec')),
|
|
1854
|
+
React.createElement('input', {
|
|
1855
|
+
type: 'number',
|
|
1856
|
+
className: 'dsub-inp',
|
|
1857
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1858
|
+
min: 1,
|
|
1859
|
+
max: 120,
|
|
1860
|
+
value: Math.round((draft.queueTimeoutMs != null ? draft.queueTimeoutMs : 30000) / 1000),
|
|
1861
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { queueTimeoutMs: Math.max(1, Number(e.target.value) || 30) * 1000 }) }) },
|
|
1862
|
+
}),
|
|
1863
|
+
),
|
|
1864
|
+
React.createElement('div', { className: 'dsub-sub' }, t('queueTimeoutSecHint')),
|
|
1865
|
+
),
|
|
1643
1866
|
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1644
1867
|
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
|
|
1645
1868
|
React.createElement('label', { className: 'dsub-row' },
|
|
@@ -1791,6 +2014,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1791
2014
|
),
|
|
1792
2015
|
React.createElement('div', { className: 'dsub-block' },
|
|
1793
2016
|
React.createElement('div', { className: 'dsub-row' },
|
|
2017
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: () => runWizard(null, null, null, false) }, '🩺 ' + t('wizardAll')),
|
|
1794
2018
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: genDiag }, t('diagGenerate')),
|
|
1795
2019
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: runAnalyzeSession }, analyzingCache ? '…' : t('cacheAnalyze')),
|
|
1796
2020
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('export'); setBackupErr(''); setBackupInfo('') } }, '🔒 ' + t('backupExport')),
|
|
@@ -1809,6 +2033,72 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1809
2033
|
}, t('diagCopy')),
|
|
1810
2034
|
React.createElement('pre', { className: 'dsub-diag' }, diag),
|
|
1811
2035
|
) : null,
|
|
2036
|
+
wizardModal ? React.createElement('div', { className: 'dsub-modalWrap', onClick: (e) => { if (e.target === e.currentTarget) setWizardModal(null) } },
|
|
2037
|
+
React.createElement('div', { className: 'dsub-modal' },
|
|
2038
|
+
React.createElement('div', { className: 'dsub-modalHead' },
|
|
2039
|
+
React.createElement('div', { className: 'dsub-modalTitleWrap' },
|
|
2040
|
+
React.createElement('div', { className: 'dsub-modalIcon' }, '🩺'),
|
|
2041
|
+
React.createElement('div', null,
|
|
2042
|
+
React.createElement('div', { style: { fontWeight: 700, fontSize: '15px' } }, t('wizardTitle')),
|
|
2043
|
+
React.createElement('div', { style: { fontSize: '11.5px', opacity: 0.7 } },
|
|
2044
|
+
wizardModal.provider && wizardModal.provider !== 'all' ? (wizardModal.provider.toUpperCase() + ' #' + wizardModal.index + (wizardModal.ref ? ' (' + wizardModal.ref + ')' : '')) : t('wizardAll')
|
|
2045
|
+
),
|
|
2046
|
+
),
|
|
2047
|
+
),
|
|
2048
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: () => setWizardModal(null) }, '✕ ' + t('closeModal')),
|
|
2049
|
+
),
|
|
2050
|
+
React.createElement('div', { style: { marginTop: '14px' } },
|
|
2051
|
+
React.createElement('label', { className: 'dsub-row', style: { marginBottom: '10px' } },
|
|
2052
|
+
React.createElement('input', {
|
|
2053
|
+
type: 'checkbox',
|
|
2054
|
+
checked: !!wizardModal.includeNetwork,
|
|
2055
|
+
onChange: (e) => runWizard(wizardModal.provider === 'all' ? null : wizardModal.provider, wizardModal.index, wizardModal.ref, e.target.checked),
|
|
2056
|
+
}),
|
|
2057
|
+
React.createElement('span', { style: { fontSize: '12px' } }, t('wizardIncludeNetwork')),
|
|
2058
|
+
),
|
|
2059
|
+
wizardModal.loading ? React.createElement('div', { className: 'dsub-alert-ok', style: { marginBottom: '12px' } }, '⏳ ' + t('wizardRunning')) : null,
|
|
2060
|
+
wizardModal.error ? React.createElement('div', { className: 'dsub-alert-bad', style: { marginBottom: '12px' } }, wizardModal.error) : null,
|
|
2061
|
+
wizardModal.report ? React.createElement('div', null,
|
|
2062
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' } },
|
|
2063
|
+
React.createElement('span', {
|
|
2064
|
+
className: 'dsub-badge ' + (wizardModal.report.overallStatus === 'healthy' ? 'dsub-badge-ok' : (wizardModal.report.overallStatus === 'degraded' ? 'dsub-badge-warn' : 'dsub-badge-bad'))
|
|
2065
|
+
}, (wizardModal.report.overallStatus === 'healthy' ? '🟢 ' + t('wizardHealthy') : (wizardModal.report.overallStatus === 'degraded' ? '🟡 ' + t('wizardDegraded') : '🔴 ' + t('wizardFailing')))),
|
|
2066
|
+
React.createElement('span', { style: { fontSize: '11.5px', opacity: 0.7 } },
|
|
2067
|
+
wizardModal.report.summary.passed + ' passed · ' + wizardModal.report.summary.warnings + ' warn · ' + wizardModal.report.summary.failed + ' fail'
|
|
2068
|
+
),
|
|
2069
|
+
),
|
|
2070
|
+
React.createElement('div', { className: 'dsub-trace-box', style: { marginBottom: '12px' } },
|
|
2071
|
+
wizardModal.report.steps.map((st, idx) => {
|
|
2072
|
+
const icon = st.status === 'pass' ? '✅' : (st.status === 'warn' ? '⚠️' : (st.status === 'fail' ? '❌' : '⚪'))
|
|
2073
|
+
const color = st.status === 'pass' ? 'var(--dsw-alias-state-success-primary)' : (st.status === 'warn' ? 'var(--dsw-alias-state-warn-primary)' : (st.status === 'fail' ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-label-secondary)'))
|
|
2074
|
+
return React.createElement('div', { key: idx, className: 'dsub-trace-row' },
|
|
2075
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2076
|
+
React.createElement('span', null, icon),
|
|
2077
|
+
React.createElement('span', { style: { fontWeight: 600 } }, st.name),
|
|
2078
|
+
st.latencyMs != null ? React.createElement('span', { style: { fontSize: '10px', opacity: 0.6 } }, st.latencyMs + 'ms') : null,
|
|
2079
|
+
),
|
|
2080
|
+
React.createElement('span', { style: { fontSize: '11px', color: color } }, st.message),
|
|
2081
|
+
)
|
|
2082
|
+
})
|
|
2083
|
+
),
|
|
2084
|
+
wizardModal.report.recommendations && wizardModal.report.recommendations.length ? React.createElement('div', { style: { marginBottom: '12px' } },
|
|
2085
|
+
React.createElement('div', { style: { fontSize: '11.5px', fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary)', marginBottom: '4px' } }, '💡 ' + t('wizardRecommendations')),
|
|
2086
|
+
wizardModal.report.recommendations.map((rec, rIdx) => React.createElement('div', { key: rIdx, className: 'dsub-alert-bad', style: { marginBottom: '4px', fontSize: '11.5px' } }, rec))
|
|
2087
|
+
) : null,
|
|
2088
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '10px' } },
|
|
2089
|
+
React.createElement('button', {
|
|
2090
|
+
type: 'button',
|
|
2091
|
+
className: 'dsub-mini',
|
|
2092
|
+
onClick: () => {
|
|
2093
|
+
const txt = JSON.stringify(wizardModal.report, null, 2)
|
|
2094
|
+
try { navigator.clipboard.writeText(txt) } catch { /* ignore */ }
|
|
2095
|
+
},
|
|
2096
|
+
}, '📋 ' + t('wizardCopy')),
|
|
2097
|
+
),
|
|
2098
|
+
) : null,
|
|
2099
|
+
),
|
|
2100
|
+
),
|
|
2101
|
+
) : null,
|
|
1812
2102
|
backupModal ? React.createElement('div', { className: 'dsub-panel-box', style: { marginTop: '10px' } },
|
|
1813
2103
|
React.createElement('div', { className: 'dsub-panel-title' },
|
|
1814
2104
|
React.createElement('span', null, backupModal === 'export' ? ('🔒 ' + t('backupExportTitle')) : ('📥 ' + t('backupImportTitle'))),
|
|
@@ -1909,6 +2199,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1909
2199
|
disabled: checking[key],
|
|
1910
2200
|
onClick: () => doCheck(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
1911
2201
|
}, checking[key] ? t('checking') : t('check')),
|
|
2202
|
+
React.createElement('button', {
|
|
2203
|
+
type: 'button', className: 'dsub-mini',
|
|
2204
|
+
title: t('wizardTitle'),
|
|
2205
|
+
onClick: () => runWizard(row.slot.provider, row.slot.index, row.slot.ref, false),
|
|
2206
|
+
}, '🩺 ' + t('wizardButton')),
|
|
2207
|
+
|
|
1912
2208
|
(account && account.quarantineUntil && Number(account.quarantineUntil) > Date.now() ? React.createElement(React.Fragment, null,
|
|
1913
2209
|
React.createElement('button', {
|
|
1914
2210
|
type: 'button', className: 'dsub-mini', title: t('probeWarmupTitle'),
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Concurrency limiting, bounded queue, and cooperative cancellation per account/provider.
|
|
2
|
+
// Standardized error codes: QUEUE_OVERFLOW, QUEUE_TIMEOUT, ABORT_ERR, QUEUE_DISPOSED.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {string} code
|
|
7
|
+
* @param {number} [status]
|
|
8
|
+
* @returns {Error & { code: string, status?: number }}
|
|
9
|
+
*/
|
|
10
|
+
function createQueueError(message, code, status) {
|
|
11
|
+
const err = /** @type {Error & { code: string, status?: number }} */ (new Error(message))
|
|
12
|
+
err.code = code
|
|
13
|
+
if (status !== undefined) err.status = status
|
|
14
|
+
return err
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ConcurrencyQueue {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.maxConcurrency = options.maxConcurrency !== undefined ? Number(options.maxConcurrency) : 2
|
|
20
|
+
this.maxQueueSize = options.maxQueueSize !== undefined ? Number(options.maxQueueSize) : 20
|
|
21
|
+
this.timeoutMs = options.timeoutMs !== undefined ? Number(options.timeoutMs) : 30000
|
|
22
|
+
this.enabled = options.enabled !== undefined ? Boolean(options.enabled) : true
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, number>} */
|
|
25
|
+
this._active = new Map()
|
|
26
|
+
/** @type {Map<string, Array<{ id: string, resolve: Function, reject: Function, timer: any, signal: any, abortHandler: Function | null, enqueuedAt: number }>>} */
|
|
27
|
+
this._waiting = new Map()
|
|
28
|
+
|
|
29
|
+
this._stats = {
|
|
30
|
+
totalAcquired: 0,
|
|
31
|
+
totalReleased: 0,
|
|
32
|
+
totalQueued: 0,
|
|
33
|
+
totalTimedOut: 0,
|
|
34
|
+
totalOverflowed: 0,
|
|
35
|
+
totalAborted: 0,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
configure(options = {}) {
|
|
40
|
+
if (options.maxConcurrency !== undefined) this.maxConcurrency = Number(options.maxConcurrency)
|
|
41
|
+
if (options.maxQueueSize !== undefined) this.maxQueueSize = Number(options.maxQueueSize)
|
|
42
|
+
if (options.timeoutMs !== undefined) this.timeoutMs = Number(options.timeoutMs)
|
|
43
|
+
if (options.enabled !== undefined) this.enabled = Boolean(options.enabled)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async acquire(key = 'default', options = {}) {
|
|
47
|
+
if (!this.enabled) {
|
|
48
|
+
return () => {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const signal = options.signal
|
|
52
|
+
if (signal && signal.aborted) {
|
|
53
|
+
const err = createQueueError('Request aborted before acquiring queue slot', 'ABORT_ERR')
|
|
54
|
+
err.name = 'AbortError'
|
|
55
|
+
this._stats.totalAborted++
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const k = String(key || 'default')
|
|
60
|
+
const active = this._active.get(k) || 0
|
|
61
|
+
const limit = options.maxConcurrency !== undefined ? Number(options.maxConcurrency) : this.maxConcurrency
|
|
62
|
+
|
|
63
|
+
if (active < limit) {
|
|
64
|
+
this._active.set(k, active + 1)
|
|
65
|
+
this._stats.totalAcquired++
|
|
66
|
+
return this._createRelease(k)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Queue is required
|
|
70
|
+
const queueList = this._waiting.get(k) || []
|
|
71
|
+
const maxQ = options.maxQueueSize !== undefined ? Number(options.maxQueueSize) : this.maxQueueSize
|
|
72
|
+
|
|
73
|
+
if (queueList.length >= maxQ) {
|
|
74
|
+
this._stats.totalOverflowed++
|
|
75
|
+
const err = createQueueError(`Concurrency queue overflow for ${k} (max queue size: ${maxQ})`, 'QUEUE_OVERFLOW', 429)
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const timeoutDuration = options.timeoutMs !== undefined ? Number(options.timeoutMs) : this.timeoutMs
|
|
80
|
+
this._stats.totalQueued++
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let timer = null
|
|
84
|
+
let abortHandler = null
|
|
85
|
+
const id = Math.random().toString(36).slice(2)
|
|
86
|
+
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
if (timer) clearTimeout(timer)
|
|
89
|
+
if (signal && abortHandler && typeof signal.removeEventListener === 'function') {
|
|
90
|
+
signal.removeEventListener('abort', abortHandler)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (timeoutDuration > 0) {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
cleanup()
|
|
97
|
+
this._removeFromWaiting(k, id)
|
|
98
|
+
this._stats.totalTimedOut++
|
|
99
|
+
const err = createQueueError(`Concurrency queue wait timeout for ${k} after ${timeoutDuration}ms`, 'QUEUE_TIMEOUT', 504)
|
|
100
|
+
reject(err)
|
|
101
|
+
}, timeoutDuration)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (signal) {
|
|
105
|
+
abortHandler = () => {
|
|
106
|
+
cleanup()
|
|
107
|
+
this._removeFromWaiting(k, id)
|
|
108
|
+
this._stats.totalAborted++
|
|
109
|
+
const err = createQueueError('Request aborted while waiting in concurrency queue', 'ABORT_ERR')
|
|
110
|
+
err.name = 'AbortError'
|
|
111
|
+
reject(err)
|
|
112
|
+
}
|
|
113
|
+
signal.addEventListener('abort', abortHandler, { once: true })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const item = {
|
|
117
|
+
id,
|
|
118
|
+
resolve: () => {
|
|
119
|
+
cleanup()
|
|
120
|
+
this._stats.totalAcquired++
|
|
121
|
+
resolve(this._createRelease(k))
|
|
122
|
+
},
|
|
123
|
+
reject: (err) => {
|
|
124
|
+
cleanup()
|
|
125
|
+
reject(err)
|
|
126
|
+
},
|
|
127
|
+
timer,
|
|
128
|
+
signal,
|
|
129
|
+
abortHandler,
|
|
130
|
+
enqueuedAt: Date.now(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!this._waiting.has(k)) {
|
|
134
|
+
this._waiting.set(k, [])
|
|
135
|
+
}
|
|
136
|
+
this._waiting.get(k).push(item)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_createRelease(k) {
|
|
141
|
+
let released = false
|
|
142
|
+
return () => {
|
|
143
|
+
if (released) return
|
|
144
|
+
released = true
|
|
145
|
+
this._release(k)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_release(k) {
|
|
150
|
+
this._stats.totalReleased++
|
|
151
|
+
const waiting = this._waiting.get(k)
|
|
152
|
+
if (waiting && waiting.length > 0) {
|
|
153
|
+
const next = waiting.shift()
|
|
154
|
+
if (waiting.length === 0) this._waiting.delete(k)
|
|
155
|
+
// Hand over slot directly to next queued consumer
|
|
156
|
+
next.resolve()
|
|
157
|
+
} else {
|
|
158
|
+
const active = this._active.get(k) || 0
|
|
159
|
+
if (active <= 1) {
|
|
160
|
+
this._active.delete(k)
|
|
161
|
+
} else {
|
|
162
|
+
this._active.set(k, active - 1)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_removeFromWaiting(k, id) {
|
|
168
|
+
const list = this._waiting.get(k)
|
|
169
|
+
if (!list) return
|
|
170
|
+
const idx = list.findIndex((item) => item.id === id)
|
|
171
|
+
if (idx !== -1) {
|
|
172
|
+
list.splice(idx, 1)
|
|
173
|
+
if (list.length === 0) this._waiting.delete(k)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stats() {
|
|
178
|
+
let totalActive = 0
|
|
179
|
+
let totalQueued = 0
|
|
180
|
+
const byKey = {}
|
|
181
|
+
|
|
182
|
+
for (const [k, count] of this._active) {
|
|
183
|
+
totalActive += count
|
|
184
|
+
if (!byKey[k]) byKey[k] = { active: 0, queued: 0 }
|
|
185
|
+
byKey[k].active = count
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const [k, list] of this._waiting) {
|
|
189
|
+
totalQueued += list.length
|
|
190
|
+
if (!byKey[k]) byKey[k] = { active: 0, queued: 0 }
|
|
191
|
+
byKey[k].queued = list.length
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
enabled: this.enabled,
|
|
196
|
+
maxConcurrency: this.maxConcurrency,
|
|
197
|
+
maxQueueSize: this.maxQueueSize,
|
|
198
|
+
timeoutMs: this.timeoutMs,
|
|
199
|
+
totalActive,
|
|
200
|
+
totalQueued,
|
|
201
|
+
byKey,
|
|
202
|
+
metrics: { ...this._stats },
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
dispose() {
|
|
207
|
+
for (const [, list] of this._waiting) {
|
|
208
|
+
for (const item of list) {
|
|
209
|
+
if (item.timer) clearTimeout(item.timer)
|
|
210
|
+
if (item.signal && item.abortHandler && typeof item.signal.removeEventListener === 'function') {
|
|
211
|
+
item.signal.removeEventListener('abort', item.abortHandler)
|
|
212
|
+
}
|
|
213
|
+
const err = createQueueError('Concurrency queue was disposed', 'QUEUE_DISPOSED')
|
|
214
|
+
item.reject(err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this._waiting.clear()
|
|
218
|
+
this._active.clear()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const defaultConcurrencyQueue = new ConcurrencyQueue()
|
package/lib/config-schema.js
CHANGED
|
@@ -118,6 +118,14 @@ export const Config = z.object({
|
|
|
118
118
|
.description('#485 Prioritize accounts with closest reset window to burn expiring quota first.'),
|
|
119
119
|
drainSafetyPercent: z.number().volatile().default(5)
|
|
120
120
|
.description('#485 Safety margin percent: avoid draining accounts below this threshold when alternatives exist.'),
|
|
121
|
+
enableQueue: z.boolean().volatile().default(true)
|
|
122
|
+
.description('#518 Bounded concurrency queue to prevent bursts of 429 errors.'),
|
|
123
|
+
accountMaxConcurrency: z.number().volatile().default(2)
|
|
124
|
+
.description('#518 Max concurrent requests per account before queuing.'),
|
|
125
|
+
maxQueueSize: z.number().volatile().default(20)
|
|
126
|
+
.description('#518 Max requests waiting in queue before reject.'),
|
|
127
|
+
queueTimeoutMs: z.number().volatile().default(30000)
|
|
128
|
+
.description('#518 Max queue wait time in ms before timeout.'),
|
|
121
129
|
customVendors: z.array(z.any()).volatile().default([])
|
|
122
130
|
.description('Declarative OpenAI-Responses-compatible providers. See README.'),
|
|
123
131
|
})
|