@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/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}' +
|
|
@@ -130,6 +134,15 @@ window.__ModuleLoader__.load({
|
|
|
130
134
|
'.dsub-model-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-size:11.5px;color:var(--dsw-alias-label-primary)}' +
|
|
131
135
|
'.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}' +
|
|
132
136
|
'.dsub-model-tag{font-size:10px;padding:1px 4px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-weight:600}' +
|
|
137
|
+
'.dsub-trace-box{padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);margin-top:8px;font-size:12px}' +
|
|
138
|
+
'.dsub-trace-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l3)}' +
|
|
139
|
+
'.dsub-trace-row:last-child{border-bottom:none}' +
|
|
140
|
+
'.dsub-model-status-available{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}' +
|
|
141
|
+
'.dsub-model-status-degraded{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}' +
|
|
142
|
+
'.dsub-model-status-quarantined{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}' +
|
|
143
|
+
'.dsub-model-status-cooldown{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent)}' +
|
|
144
|
+
'.dsub-model-status-exhausted{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}' +
|
|
145
|
+
'.dsub-model-status-unconfigured{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3)}' +
|
|
133
146
|
'.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
|
|
134
147
|
'.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
|
|
135
148
|
'.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
@@ -408,6 +421,17 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
408
421
|
"smokeFail": "Smoke test failed",
|
|
409
422
|
"modelsCatalogTitle": "Curated Subscription Models",
|
|
410
423
|
"modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
|
|
424
|
+
"rotationDecisionsTitle": "Rotation & Routing Decisions",
|
|
425
|
+
"rotationDecisionsDesc": "Real-time decision trace explaining account selection, skips, and fallback reasons.",
|
|
426
|
+
"rotationAttempt": "attempt",
|
|
427
|
+
"rotationAttempts": "attempts",
|
|
428
|
+
"rotationCascaded": "Cascaded fallback",
|
|
429
|
+
"modelPoolAvailable": "Available",
|
|
430
|
+
"modelPoolDegraded": "Degraded",
|
|
431
|
+
"modelPoolQuarantined": "Quarantined",
|
|
432
|
+
"modelPoolCooldown": "Cooldown",
|
|
433
|
+
"modelPoolExhausted": "Exhausted",
|
|
434
|
+
"modelPoolUnconfigured": "Unconfigured",
|
|
411
435
|
"resetCredits": "Reset credits",
|
|
412
436
|
"resetCreditsHint": "ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.",
|
|
413
437
|
"resetCreditsAction": "Reset ChatGPT Limit",
|
|
@@ -549,6 +573,27 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
549
573
|
"toastReleased": "Account active and healthy",
|
|
550
574
|
"toastQuotaWarning": "Quota threshold exceeded",
|
|
551
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",
|
|
552
597
|
"field.enableToasts": "Show popup toast notifications on quarantine or quota events"
|
|
553
598
|
};
|
|
554
599
|
|
|
@@ -619,6 +664,17 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
619
664
|
"smokeFail": "冒烟测试失败",
|
|
620
665
|
"modelsCatalogTitle": "精选订阅模型",
|
|
621
666
|
"modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
|
|
667
|
+
"rotationDecisionsTitle": "轮询调度与路由决策",
|
|
668
|
+
"rotationDecisionsDesc": "实时决策追踪,透明展示每次请求的账号选择、跳过与回退原因。",
|
|
669
|
+
"rotationAttempt": "次尝试",
|
|
670
|
+
"rotationAttempts": "次尝试",
|
|
671
|
+
"rotationCascaded": "级联回退",
|
|
672
|
+
"modelPoolAvailable": "就绪可用",
|
|
673
|
+
"modelPoolDegraded": "降级运行",
|
|
674
|
+
"modelPoolQuarantined": "已隔离",
|
|
675
|
+
"modelPoolCooldown": "冷却中",
|
|
676
|
+
"modelPoolExhausted": "额度耗尽",
|
|
677
|
+
"modelPoolUnconfigured": "未配置",
|
|
622
678
|
"resetCredits": "重置 ChatGPT 额度",
|
|
623
679
|
"resetCreditsHint": "ChatGPT 重置卡:紧急重置 5 小时额度窗口。扣除前需等待 5 秒倒计时确认。",
|
|
624
680
|
"resetCreditsAction": "重置 ChatGPT 限制",
|
|
@@ -760,6 +816,27 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
760
816
|
"toastReleased": "账户已解除隔离恢复使用",
|
|
761
817
|
"toastQuotaWarning": "额度预警",
|
|
762
818
|
"enableToasts": "应用内弹窗提醒",
|
|
819
|
+
"groupQueue": "并发限制与请求队列",
|
|
820
|
+
"enableQueue": "启用并发请求排队",
|
|
821
|
+
"enableQueueHint": "当账户达到并发上限时将多余请求排队等待,避免直接触发 429 报错",
|
|
822
|
+
"accountMaxConcurrency": "单账户最大并发流数",
|
|
823
|
+
"accountMaxConcurrencyHint": "每个账户允许同时执行的最大流式请求数(默认 2)",
|
|
824
|
+
"maxQueueSize": "队列最大等待深度",
|
|
825
|
+
"maxQueueSizeHint": "每个账户允许排队的最大请求数,超限后拒绝执行(默认 20)",
|
|
826
|
+
"queueTimeoutSec": "排队超时时间(秒)",
|
|
827
|
+
"queueTimeoutSecHint": "在队列中等待槽位的最长时间,超时后自动放弃并回退(默认 30 秒)",
|
|
828
|
+
"analyticsTitle": "订阅与缓存深度分析",
|
|
829
|
+
"analyticsDesc": "缓存命中率、Token 缓存效率、首字响应延迟 (TTFT)、生成速度及 24 小时负载分布",
|
|
830
|
+
"ttftMetric": "首字响应延迟 (TTFT)",
|
|
831
|
+
"speedMetric": "生成速度",
|
|
832
|
+
"queueActive": "活跃流",
|
|
833
|
+
"queueWaiting": "排队中",
|
|
834
|
+
"queueRejected": "已拒绝",
|
|
835
|
+
"activity24h": "24 小时活动分布",
|
|
836
|
+
"forecastTitle": "额度消耗预测与预警",
|
|
837
|
+
"forecastRunway": "预计可用时间",
|
|
838
|
+
"forecastBurnRate": "消耗速率",
|
|
839
|
+
"forecastExhaustWarning": "预计在额度重置前耗尽",
|
|
763
840
|
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒"
|
|
764
841
|
};
|
|
765
842
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
@@ -878,6 +955,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
878
955
|
const [draft, setDraft] = React.useState(null)
|
|
879
956
|
const [accounts, setAccounts] = React.useState([])
|
|
880
957
|
const [providers, setProviders] = React.useState([])
|
|
958
|
+
const [modelsCatalog, setModelsCatalog] = React.useState([])
|
|
881
959
|
const [paste, setPaste] = React.useState({})
|
|
882
960
|
const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
|
|
883
961
|
const [proxyRes, setProxyRes] = React.useState({}) // #88 key -> {ok,latencyMs,error}
|
|
@@ -983,6 +1061,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
983
1061
|
const [checkRes, setCheckRes] = React.useState({})
|
|
984
1062
|
const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
|
|
985
1063
|
const [telemetry, setTelemetry] = React.useState(null)
|
|
1064
|
+
const [analytics, setAnalytics] = React.useState(null)
|
|
1065
|
+
const [queueStats, setQueueStats] = React.useState(null)
|
|
986
1066
|
const [smoke, setSmoke] = React.useState({ testing: false, result: null })
|
|
987
1067
|
const [updater, setUpdater] = React.useState({ checking: false, updating: false, status: null, message: '' })
|
|
988
1068
|
const [backupModal, setBackupModal] = React.useState(null)
|
|
@@ -1085,6 +1165,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1085
1165
|
.then((r) => r.json())
|
|
1086
1166
|
.then((d) => { if (d && d.ok && d.telemetry) setTelemetry(d.telemetry) })
|
|
1087
1167
|
.catch(() => {})
|
|
1168
|
+
fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
|
|
1169
|
+
.then((r) => r.json())
|
|
1170
|
+
.then((d) => { if (d && d.ok && d.analytics) setAnalytics(d.analytics) })
|
|
1171
|
+
.catch(() => {})
|
|
1172
|
+
fetch('/dsh-subscriptions/queue', { cache: 'no-store' })
|
|
1173
|
+
.then((r) => r.json())
|
|
1174
|
+
.then((d) => { if (d && d.ok && d.queue) setQueueStats(d.queue) })
|
|
1175
|
+
.catch(() => {})
|
|
1088
1176
|
}
|
|
1089
1177
|
|
|
1090
1178
|
const runSmokeTest = async (provider, index) => {
|
|
@@ -1115,6 +1203,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1115
1203
|
setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
|
|
1116
1204
|
setAccounts((data && data.accounts) || [])
|
|
1117
1205
|
setProviders((data && data.providers) || [])
|
|
1206
|
+
setModelsCatalog((data && data.modelsCatalog) || [])
|
|
1118
1207
|
}
|
|
1119
1208
|
|
|
1120
1209
|
const reload = () => {
|
|
@@ -1473,6 +1562,88 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1473
1562
|
React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLastActivity')),
|
|
1474
1563
|
),
|
|
1475
1564
|
) : null,
|
|
1565
|
+
(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' } },
|
|
1566
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '📊 ' + t('analyticsTitle')),
|
|
1567
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('analyticsDesc')),
|
|
1568
|
+
React.createElement('div', { className: 'dsub-grid-2', style: { marginTop: '10px' } },
|
|
1569
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1570
|
+
React.createElement('div', { className: 'dsub-stat-val' }, ((analytics && analytics.cache && analytics.cache.cacheHitRate) || 0) + '%'),
|
|
1571
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('cacheHitRate') + (analytics && analytics.cache ? ' (' + (analytics.cache.cacheReadTokens || 0).toLocaleString() + ' tok)' : '')),
|
|
1572
|
+
),
|
|
1573
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1574
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (analytics && analytics.performance && analytics.performance.ttft && analytics.performance.ttft.avgMs != null ? analytics.performance.ttft.avgMs + ' ms' : '—')),
|
|
1575
|
+
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' : '')),
|
|
1576
|
+
),
|
|
1577
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1578
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (analytics && analytics.performance && analytics.performance.tps && analytics.performance.tps.avg != null ? analytics.performance.tps.avg + ' tok/s' : '—')),
|
|
1579
|
+
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 : '')),
|
|
1580
|
+
),
|
|
1581
|
+
React.createElement('div', { className: 'dsub-stat-box' },
|
|
1582
|
+
React.createElement('div', { className: 'dsub-stat-val' }, (queueStats ? queueStats.totalActive : 0) + ' / ' + (queueStats ? queueStats.totalQueued : 0)),
|
|
1583
|
+
React.createElement('div', { className: 'dsub-stat-lbl' }, t('queueActive') + ' / ' + t('queueWaiting') + (queueStats && queueStats.metrics && queueStats.metrics.totalOverflowed ? ' · ' + queueStats.metrics.totalOverflowed + ' ' + t('queueRejected') : '')),
|
|
1584
|
+
),
|
|
1585
|
+
),
|
|
1586
|
+
analytics && Array.isArray(analytics.hourly) && analytics.hourly.length ? (function() {
|
|
1587
|
+
var maxReq = Math.max.apply(Math, [1].concat(analytics.hourly.map(function(b) { return b.requests || 0 })))
|
|
1588
|
+
return React.createElement('div', { style: { marginTop: '12px' } },
|
|
1589
|
+
React.createElement('div', { style: { fontSize: '11.5px', fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('activity24h')),
|
|
1590
|
+
React.createElement('div', { className: 'dsub-chart-wrap' },
|
|
1591
|
+
analytics.hourly.map(function(b, idx) {
|
|
1592
|
+
var pct = Math.max(4, Math.round(((b.requests || 0) / maxReq) * 100))
|
|
1593
|
+
var title = b.label + ': ' + b.requests + ' req (' + b.cacheReadTokens + ' cached tok, ' + b.errorCount + ' err)'
|
|
1594
|
+
return React.createElement('div', {
|
|
1595
|
+
key: idx,
|
|
1596
|
+
className: 'dsub-chart-bar',
|
|
1597
|
+
style: {
|
|
1598
|
+
height: pct + '%',
|
|
1599
|
+
backgroundColor: b.errorCount > 0 && b.requests === b.errorCount
|
|
1600
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
1601
|
+
: (b.cacheReadTokens > 0 ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-state-success-primary)'),
|
|
1602
|
+
},
|
|
1603
|
+
title: title,
|
|
1604
|
+
})
|
|
1605
|
+
})
|
|
1606
|
+
),
|
|
1607
|
+
React.createElement('div', { className: 'dsub-chart-labels' },
|
|
1608
|
+
React.createElement('span', null, '-24h'),
|
|
1609
|
+
React.createElement('span', null, '-18h'),
|
|
1610
|
+
React.createElement('span', null, '-12h'),
|
|
1611
|
+
React.createElement('span', null, '-6h'),
|
|
1612
|
+
React.createElement('span', null, 'Now'),
|
|
1613
|
+
),
|
|
1614
|
+
)
|
|
1615
|
+
})() : null,
|
|
1616
|
+
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' } },
|
|
1617
|
+
React.createElement('div', { style: { fontSize: '11.5px', fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary)', marginBottom: '6px' } }, '⚠️ ' + t('forecastTitle')),
|
|
1618
|
+
analytics.forecast.filter(function(f) { return f.willExhaustBeforeReset || (f.burnRateTokensPerHour > 0 && f.hoursToExhaustion != null && f.hoursToExhaustion < 72) }).map(function(f, idx) {
|
|
1619
|
+
return React.createElement('div', { key: idx, className: 'dsub-alert-bad', style: { marginBottom: '6px' } },
|
|
1620
|
+
(f.provider ? f.provider.toUpperCase() : 'LLM') + ' (' + (f.label || f.ref) + '): ' +
|
|
1621
|
+
(f.willExhaustBeforeReset ? t('forecastExhaustWarning') : (t('forecastRunway') + ': ~' + f.hoursToExhaustion + 'h')) +
|
|
1622
|
+
' · ' + t('forecastBurnRate') + ': ' + f.burnRateTokensPerHour.toLocaleString() + ' tok/h'
|
|
1623
|
+
)
|
|
1624
|
+
})
|
|
1625
|
+
) : null,
|
|
1626
|
+
) : null,
|
|
1627
|
+
telemetry && Array.isArray(telemetry.recentDecisions) && telemetry.recentDecisions.length ? React.createElement('details', { className: 'dsub-group', style: { marginBottom: '14px' } },
|
|
1628
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '🧭 ' + t('rotationDecisionsTitle')),
|
|
1629
|
+
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('rotationDecisionsDesc')),
|
|
1630
|
+
React.createElement('div', { className: 'dsub-trace-box' },
|
|
1631
|
+
telemetry.recentDecisions.map((dec, idx) => {
|
|
1632
|
+
const reasonBadge = dec.cascaded ? ('🔀 ' + t('rotationCascaded')) : (dec.decisionReason === 'session_pinned' ? '📌 Pinned' : ('⚡ ' + dec.decisionReason))
|
|
1633
|
+
return React.createElement('div', { key: idx, className: 'dsub-trace-row' },
|
|
1634
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
1635
|
+
dec.provider ? React.createElement('span', { className: 'dsub-brand dsub-brand-' + dec.provider, style: { fontSize: '9px', padding: '1px 4px' } }, dec.provider.toUpperCase()) : null,
|
|
1636
|
+
React.createElement('span', { style: { fontWeight: 600 } }, dec.ref || '—'),
|
|
1637
|
+
dec.model ? React.createElement('code', { style: { fontSize: '11px', opacity: 0.8 } }, dec.model) : null,
|
|
1638
|
+
),
|
|
1639
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1640
|
+
React.createElement('span', { className: 'dsub-model-tag' }, reasonBadge),
|
|
1641
|
+
React.createElement('span', { style: { fontSize: '10.5px', opacity: 0.6 } }, dec.attemptsCount > 1 ? (dec.attemptsCount + ' ' + t('rotationAttempts')) : ('1 ' + t('rotationAttempt'))),
|
|
1642
|
+
),
|
|
1643
|
+
)
|
|
1644
|
+
})
|
|
1645
|
+
),
|
|
1646
|
+
) : null,
|
|
1476
1647
|
accounts && accounts.length ? React.createElement('details', { className: 'dsub-group', open: true, style: { marginBottom: '14px' } },
|
|
1477
1648
|
React.createElement('summary', { className: 'dsub-groupHead' }, '📊 ' + (t('healthMatrix') || 'Health Matrix & Quota Runway')),
|
|
1478
1649
|
React.createElement('div', { style: { padding: '8px 0', overflowX: 'auto' } },
|
|
@@ -1587,6 +1758,57 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1587
1758
|
React.createElement('div', { className: 'dsub-sub' },
|
|
1588
1759
|
t('expiryNotifyDaysHint')),
|
|
1589
1760
|
),
|
|
1761
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1762
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
|
|
1763
|
+
React.createElement('label', { className: 'dsub-row' },
|
|
1764
|
+
React.createElement('input', {
|
|
1765
|
+
type: 'checkbox',
|
|
1766
|
+
checked: draft.enableQueue !== false,
|
|
1767
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { enableQueue: e.target.checked }) }) },
|
|
1768
|
+
}),
|
|
1769
|
+
React.createElement('span', null, t('enableQueue')),
|
|
1770
|
+
),
|
|
1771
|
+
React.createElement('div', { className: 'dsub-sub' }, t('enableQueueHint')),
|
|
1772
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1773
|
+
React.createElement('span', { className: 'dsub-h' }, t('accountMaxConcurrency')),
|
|
1774
|
+
React.createElement('input', {
|
|
1775
|
+
type: 'number',
|
|
1776
|
+
className: 'dsub-inp',
|
|
1777
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1778
|
+
min: 1,
|
|
1779
|
+
max: 20,
|
|
1780
|
+
value: draft.accountMaxConcurrency != null ? draft.accountMaxConcurrency : 2,
|
|
1781
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { accountMaxConcurrency: Math.max(1, Number(e.target.value) || 2) }) }) },
|
|
1782
|
+
}),
|
|
1783
|
+
),
|
|
1784
|
+
React.createElement('div', { className: 'dsub-sub' }, t('accountMaxConcurrencyHint')),
|
|
1785
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1786
|
+
React.createElement('span', { className: 'dsub-h' }, t('maxQueueSize')),
|
|
1787
|
+
React.createElement('input', {
|
|
1788
|
+
type: 'number',
|
|
1789
|
+
className: 'dsub-inp',
|
|
1790
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1791
|
+
min: 1,
|
|
1792
|
+
max: 100,
|
|
1793
|
+
value: draft.maxQueueSize != null ? draft.maxQueueSize : 20,
|
|
1794
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { maxQueueSize: Math.max(1, Number(e.target.value) || 20) }) }) },
|
|
1795
|
+
}),
|
|
1796
|
+
),
|
|
1797
|
+
React.createElement('div', { className: 'dsub-sub' }, t('maxQueueSizeHint')),
|
|
1798
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1799
|
+
React.createElement('span', { className: 'dsub-h' }, t('queueTimeoutSec')),
|
|
1800
|
+
React.createElement('input', {
|
|
1801
|
+
type: 'number',
|
|
1802
|
+
className: 'dsub-inp',
|
|
1803
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1804
|
+
min: 1,
|
|
1805
|
+
max: 120,
|
|
1806
|
+
value: Math.round((draft.queueTimeoutMs != null ? draft.queueTimeoutMs : 30000) / 1000),
|
|
1807
|
+
onChange: function(e) { setDraft(function(d) { return Object.assign({}, d, { queueTimeoutMs: Math.max(1, Number(e.target.value) || 30) * 1000 }) }) },
|
|
1808
|
+
}),
|
|
1809
|
+
),
|
|
1810
|
+
React.createElement('div', { className: 'dsub-sub' }, t('queueTimeoutSecHint')),
|
|
1811
|
+
),
|
|
1590
1812
|
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1591
1813
|
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
|
|
1592
1814
|
React.createElement('label', { className: 'dsub-row' },
|
|
@@ -1648,14 +1870,51 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1648
1870
|
React.createElement('summary', { className: 'dsub-groupHead' }, '🎯 ' + t('modelsCatalogTitle')),
|
|
1649
1871
|
React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('modelsCatalogDesc')),
|
|
1650
1872
|
React.createElement('div', { className: 'dsub-model-grid' },
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1873
|
+
(Array.isArray(modelsCatalog) && modelsCatalog.length ? modelsCatalog : [
|
|
1874
|
+
{ id: 'gpt-4o', contextLabel: '128k', capabilities: ['vision'], pool: { status: 'available' } },
|
|
1875
|
+
{ id: 'o1 / o3-mini', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: 'available' } },
|
|
1876
|
+
{ id: 'claude-3-7-sonnet', contextLabel: '200k', capabilities: ['reasoning'], pool: { status: 'available' } },
|
|
1877
|
+
{ id: 'claude-3-5-sonnet', contextLabel: '200k', capabilities: ['coding'], pool: { status: 'available' } },
|
|
1878
|
+
{ id: 'grok-beta', contextLabel: '128k', capabilities: ['fast'], pool: { status: 'available' } },
|
|
1879
|
+
{ id: 'gemini-2.5-pro', contextLabel: '1M', capabilities: ['vision'], pool: { status: 'available' } },
|
|
1880
|
+
{ id: 'kimi-k1.5', contextLabel: '128k', capabilities: ['math'], pool: { status: 'available' } },
|
|
1881
|
+
{ id: 'glm-4-plus', contextLabel: '128k', capabilities: ['chinese'], pool: { status: 'available' } },
|
|
1882
|
+
]).map((m, idx) => {
|
|
1883
|
+
const poolStatus = m.pool ? m.pool.status : 'unconfigured'
|
|
1884
|
+
const statusClass = 'dsub-model-status-' + poolStatus
|
|
1885
|
+
let poolLabel = ''
|
|
1886
|
+
if (poolStatus === 'available') {
|
|
1887
|
+
poolLabel = '🟢 ' + (m.pool && m.pool.remainingPercent != null ? (m.pool.remainingPercent + '%') : t('modelPoolAvailable'))
|
|
1888
|
+
} else if (poolStatus === 'degraded') {
|
|
1889
|
+
poolLabel = '🟡 ' + t('modelPoolDegraded') + (m.pool ? ' (' + m.pool.healthyAccounts + '/' + m.pool.totalAccounts + ')' : '')
|
|
1890
|
+
} else if (poolStatus === 'quarantined') {
|
|
1891
|
+
poolLabel = '⛔ ' + t('modelPoolQuarantined')
|
|
1892
|
+
} else if (poolStatus === 'cooldown') {
|
|
1893
|
+
poolLabel = '❄️ ' + t('modelPoolCooldown')
|
|
1894
|
+
} else if (poolStatus === 'exhausted') {
|
|
1895
|
+
poolLabel = '⚠️ ' + t('modelPoolExhausted')
|
|
1896
|
+
} else {
|
|
1897
|
+
poolLabel = '⚪ ' + t('modelPoolUnconfigured')
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const tags = []
|
|
1901
|
+
if (m.contextLabel) tags.push(m.contextLabel)
|
|
1902
|
+
if (Array.isArray(m.capabilities)) {
|
|
1903
|
+
for (const c of m.capabilities) {
|
|
1904
|
+
if (c === 'vision') tags.push('Vision')
|
|
1905
|
+
else if (c === 'reasoning') tags.push('Reasoning')
|
|
1906
|
+
else if (c === 'coding') tags.push('Coding')
|
|
1907
|
+
else if (c === 'fast') tags.push('Fast')
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
return React.createElement('div', { key: (m.id || idx) + '_' + idx, className: 'dsub-model-chip' },
|
|
1912
|
+
m.provider ? React.createElement('span', { className: 'dsub-brand dsub-brand-' + m.provider, style: { fontSize: '9px', padding: '1px 4px' } }, m.provider.toUpperCase()) : null,
|
|
1913
|
+
React.createElement('code', null, m.name || m.id),
|
|
1914
|
+
tags.length ? React.createElement('span', { className: 'dsub-model-tag' }, tags.join(' · ')) : null,
|
|
1915
|
+
React.createElement('span', { className: 'dsub-model-tag ' + statusClass }, poolLabel),
|
|
1916
|
+
)
|
|
1917
|
+
})
|
|
1659
1918
|
),
|
|
1660
1919
|
),
|
|
1661
1920
|
React.createElement('details', { className: 'dsub-details' },
|
|
@@ -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
|
})
|
package/lib/diagnostics.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
2
|
+
import { calculateCacheMetrics } from './analytics.js'
|
|
1
3
|
import { maskText } from './mask.js'
|
|
2
4
|
import { normalizeSlots } from './accounts.js'
|
|
3
5
|
import { PROVIDERS } from './refs.js'
|
|
@@ -46,6 +48,21 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
46
48
|
runtime: { node: process.version, platform: process.platform, arch: process.arch, uptimeSec: Math.round(process.uptime()) },
|
|
47
49
|
providers,
|
|
48
50
|
requests: { total: rows.length, byStatus, lastErrors },
|
|
51
|
+
cache: calculateCacheMetrics(rows),
|
|
52
|
+
queue: defaultConcurrencyQueue.stats(),
|
|
53
|
+
decisions: {
|
|
54
|
+
recent: rows.filter((r) => r && r.decisionTrace).slice(0, 10).map((r) => ({
|
|
55
|
+
ts: r.ts,
|
|
56
|
+
provider: r.provider,
|
|
57
|
+
ref: r.ref,
|
|
58
|
+
outcome: r.decisionTrace.finalOutcome || 'success',
|
|
59
|
+
attempts: (r.decisionTrace.attempts || []).map((a) => ({
|
|
60
|
+
ref: a.ref,
|
|
61
|
+
status: a.status,
|
|
62
|
+
decision: a.decisionReason || a.kind,
|
|
63
|
+
})),
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
49
66
|
settings: {
|
|
50
67
|
cooldownMs: cfg.cooldownMs,
|
|
51
68
|
switchAtRemaining: cfg.switchAtRemaining,
|