feat: add configurable account selection strategies

Refactor account selection into a strategy pattern with three options:
- Sticky: cache-optimized, stays on same account until rate-limited
- Round-robin: load-balanced, rotates every request
- Hybrid (default): smart distribution using health scores, token buckets, and LRU

The hybrid strategy uses multiple signals for optimal account selection:
health tracking for reliability, client-side token buckets for rate limiting,
and LRU freshness to prefer rested accounts.

Includes WebUI settings for strategy selection and unit tests.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Badri Narayanan S
2026-01-18 03:48:43 +05:30
parent 973234372b
commit 5ae19a5b72
31 changed files with 2721 additions and 353 deletions

View File

@@ -248,5 +248,102 @@ window.Components.serverConfig = () => ({
const { MAX_WAIT_MIN, MAX_WAIT_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('maxWaitBeforeErrorMs', value, 'Max Wait Threshold',
(v) => window.Validators.validateTimeout(v, MAX_WAIT_MIN, MAX_WAIT_MAX));
},
toggleRateLimitDedupWindowMs(value) {
const { RATE_LIMIT_DEDUP_MIN, RATE_LIMIT_DEDUP_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('rateLimitDedupWindowMs', value, 'Rate Limit Dedup Window',
(v) => window.Validators.validateTimeout(v, RATE_LIMIT_DEDUP_MIN, RATE_LIMIT_DEDUP_MAX));
},
toggleMaxConsecutiveFailures(value) {
const { MAX_CONSECUTIVE_FAILURES_MIN, MAX_CONSECUTIVE_FAILURES_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('maxConsecutiveFailures', value, 'Max Consecutive Failures',
(v) => window.Validators.validateRange(v, MAX_CONSECUTIVE_FAILURES_MIN, MAX_CONSECUTIVE_FAILURES_MAX, 'Max Consecutive Failures'));
},
toggleExtendedCooldownMs(value) {
const { EXTENDED_COOLDOWN_MIN, EXTENDED_COOLDOWN_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('extendedCooldownMs', value, 'Extended Cooldown',
(v) => window.Validators.validateTimeout(v, EXTENDED_COOLDOWN_MIN, EXTENDED_COOLDOWN_MAX));
},
toggleCapacityRetryDelayMs(value) {
const { CAPACITY_RETRY_DELAY_MIN, CAPACITY_RETRY_DELAY_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('capacityRetryDelayMs', value, 'Capacity Retry Delay',
(v) => window.Validators.validateTimeout(v, CAPACITY_RETRY_DELAY_MIN, CAPACITY_RETRY_DELAY_MAX));
},
toggleMaxCapacityRetries(value) {
const { MAX_CAPACITY_RETRIES_MIN, MAX_CAPACITY_RETRIES_MAX } = window.AppConstants.VALIDATION;
this.saveConfigField('maxCapacityRetries', value, 'Max Capacity Retries',
(v) => window.Validators.validateRange(v, MAX_CAPACITY_RETRIES_MIN, MAX_CAPACITY_RETRIES_MAX, 'Max Capacity Retries'));
},
// Toggle Account Selection Strategy
async toggleStrategy(strategy) {
const store = Alpine.store('global');
const validStrategies = ['sticky', 'round-robin', 'hybrid'];
if (!validStrategies.includes(strategy)) {
store.showToast(store.t('invalidStrategy'), 'error');
return;
}
// Optimistic update
const previousValue = this.serverConfig.accountSelection?.strategy || 'hybrid';
if (!this.serverConfig.accountSelection) {
this.serverConfig.accountSelection = {};
}
this.serverConfig.accountSelection.strategy = strategy;
try {
const { response, newPassword } = await window.utils.request('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountSelection: { strategy } })
}, store.webuiPassword);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
const strategyLabel = this.getStrategyLabel(strategy);
store.showToast(store.t('strategyUpdated', { strategy: strategyLabel }), 'success');
await this.fetchServerConfig(); // Confirm server state
} else {
throw new Error(data.error || store.t('failedToUpdateStrategy'));
}
} catch (e) {
// Rollback on error
if (!this.serverConfig.accountSelection) {
this.serverConfig.accountSelection = {};
}
this.serverConfig.accountSelection.strategy = previousValue;
store.showToast(store.t('failedToUpdateStrategy') + ': ' + e.message, 'error');
}
},
// Get display label for a strategy
getStrategyLabel(strategy) {
const store = Alpine.store('global');
const labels = {
'sticky': store.t('strategyStickyLabel'),
'round-robin': store.t('strategyRoundRobinLabel'),
'hybrid': store.t('strategyHybridLabel')
};
return labels[strategy] || strategy;
},
// Get description for current strategy
currentStrategyDescription() {
const store = Alpine.store('global');
const strategy = this.serverConfig.accountSelection?.strategy || 'hybrid';
const descriptions = {
'sticky': store.t('strategyStickyDesc'),
'round-robin': store.t('strategyRoundRobinDesc'),
'hybrid': store.t('strategyHybridDesc')
};
return descriptions[strategy] || '';
}
});

View File

@@ -67,7 +67,27 @@ window.AppConstants.VALIDATION = {
// Max wait threshold (1 - 30 minutes)
MAX_WAIT_MIN: 60000,
MAX_WAIT_MAX: 1800000
MAX_WAIT_MAX: 1800000,
// Rate limit dedup window (1 - 30 seconds)
RATE_LIMIT_DEDUP_MIN: 1000,
RATE_LIMIT_DEDUP_MAX: 30000,
// Consecutive failures (1 - 10)
MAX_CONSECUTIVE_FAILURES_MIN: 1,
MAX_CONSECUTIVE_FAILURES_MAX: 10,
// Extended cooldown (10 seconds - 5 minutes)
EXTENDED_COOLDOWN_MIN: 10000,
EXTENDED_COOLDOWN_MAX: 300000,
// Capacity retry delay (500ms - 10 seconds)
CAPACITY_RETRY_DELAY_MIN: 500,
CAPACITY_RETRY_DELAY_MAX: 10000,
// Capacity retries (1 - 10)
MAX_CAPACITY_RETRIES_MIN: 1,
MAX_CAPACITY_RETRIES_MAX: 10
};
/**

View File

@@ -237,6 +237,18 @@ window.translations.en = {
defaultCooldownDesc: "Fallback cooldown when API doesn't provide a reset time.",
maxWaitThreshold: "Max Wait Before Error",
maxWaitDesc: "If all accounts are rate-limited longer than this, error immediately instead of waiting.",
// Error Handling Tuning
errorHandlingTuning: "Error Handling Tuning",
rateLimitDedupWindow: "Rate Limit Dedup Window",
rateLimitDedupWindowDesc: "Prevents concurrent retry storms when multiple requests hit rate limits simultaneously.",
maxConsecutiveFailures: "Max Consecutive Failures",
maxConsecutiveFailuresDesc: "Number of consecutive failures before applying extended cooldown to an account.",
extendedCooldown: "Extended Cooldown",
extendedCooldownDesc: "Cooldown duration applied after max consecutive failures reached.",
capacityRetryDelay: "Capacity Retry Delay",
capacityRetryDelayDesc: "Delay before retrying when model capacity is exhausted (not quota).",
maxCapacityRetries: "Max Capacity Retries",
maxCapacityRetriesDesc: "Maximum retries for capacity exhaustion before switching accounts.",
saveConfigServer: "Save Configuration",
serverRestartAlert: "Changes saved to {path}. Restart server to apply some settings.",
changePassword: "Change WebUI Password",
@@ -318,6 +330,18 @@ window.translations.en = {
failedToUpdateModelConfig: "Failed to update model config",
fieldUpdated: "{displayName} updated to {value}",
failedToUpdateField: "Failed to update {displayName}",
// Account Selection Strategy
accountSelectionStrategy: "Account Selection Strategy",
selectionStrategy: "Selection Strategy",
strategyStickyLabel: "Sticky (Cache Optimized)",
strategyRoundRobinLabel: "Round Robin (Load Balanced)",
strategyHybridLabel: "Hybrid (Smart Distribution)",
strategyStickyDesc: "Stays on same account until rate-limited. Best for prompt caching.",
strategyRoundRobinDesc: "Rotates to next account on every request. Maximum throughput.",
strategyHybridDesc: "Smart selection based on health, tokens, and freshness.",
strategyUpdated: "Strategy updated to: {strategy}",
failedToUpdateStrategy: "Failed to update strategy",
invalidStrategy: "Invalid strategy selected",
// Validation Messages
mustBeValidNumber: "{fieldName} must be a valid number",
mustBeAtLeast: "{fieldName} must be at least {min}",

View File

@@ -270,6 +270,18 @@ window.translations.id = {
defaultCooldownDesc: "Cooldown bawaan jika API tidak memberikan waktu reset.",
maxWaitThreshold: "Batas Tunggu Maksimal",
maxWaitDesc: "Jika semua akun terkena rate limit lebih lama dari ini, langsung gagal.",
// Error Handling Tuning
errorHandlingTuning: "Penyetelan Penanganan Error",
rateLimitDedupWindow: "Jendela Deduplikasi Rate Limit",
rateLimitDedupWindowDesc: "Mencegah badai retry ketika beberapa permintaan terkena rate limit bersamaan.",
maxConsecutiveFailures: "Maks. Kegagalan Berturut-turut",
maxConsecutiveFailuresDesc: "Jumlah kegagalan berturut-turut sebelum menerapkan cooldown diperpanjang.",
extendedCooldown: "Cooldown Diperpanjang",
extendedCooldownDesc: "Durasi cooldown setelah mencapai maks. kegagalan berturut-turut.",
capacityRetryDelay: "Jeda Retry Kapasitas",
capacityRetryDelayDesc: "Jeda sebelum retry saat kapasitas model habis (bukan kuota).",
maxCapacityRetries: "Maks. Retry Kapasitas",
maxCapacityRetriesDesc: "Maksimum retry untuk kehabisan kapasitas sebelum ganti akun.",
saveConfigServer: "Simpan Konfigurasi",
serverRestartAlert: "Tersimpan ke {path}. Restart server untuk menerapkan.",
@@ -368,4 +380,17 @@ window.translations.id = {
mustBeAtMost: "{fieldName} maksimal {max}",
cannotBeEmpty: "{fieldName} tidak boleh kosong",
mustBeTrueOrFalse: "Nilai harus true atau false",
// Account Selection Strategy translations
accountSelectionStrategy: "Strategi Pemilihan Akun",
selectionStrategy: "Strategi Pemilihan",
strategyStickyLabel: "Tetap (Optimisasi Cache)",
strategyRoundRobinLabel: "Bergilir (Load Balanced)",
strategyHybridLabel: "Hibrida (Distribusi Cerdas)",
strategyStickyDesc: "Tetap di akun yang sama hingga terkena rate limit. Terbaik untuk cache prompt.",
strategyRoundRobinDesc: "Berputar ke akun berikutnya setiap permintaan. Throughput maksimum.",
strategyHybridDesc: "Pemilihan cerdas berdasarkan kesehatan, token, dan kesegaran.",
strategyUpdated: "Strategi diubah ke: {strategy}",
failedToUpdateStrategy: "Gagal memperbarui strategi",
invalidStrategy: "Strategi tidak valid dipilih",
};

View File

@@ -212,8 +212,21 @@ window.translations.pt = {
persistTokenDesc: "Salvar sessões OAuth no disco para reinicializações mais rápidas",
rateLimiting: "Limitação de Taxa de Conta & Timeouts",
defaultCooldown: "Tempo de Resfriamento Padrão",
defaultCooldownDesc: "Resfriamento de fallback quando a API não fornece tempo de reset.",
maxWaitThreshold: "Limiar Máximo de Espera (Sticky)",
maxWaitDesc: "Tempo máximo para aguardar uma conta sticky resetar antes de trocar.",
// Ajuste de Tratamento de Erros
errorHandlingTuning: "Ajuste de Tratamento de Erros",
rateLimitDedupWindow: "Janela de Deduplicação de Rate Limit",
rateLimitDedupWindowDesc: "Previne tempestades de retry quando múltiplas requisições atingem rate limits simultaneamente.",
maxConsecutiveFailures: "Máx. Falhas Consecutivas",
maxConsecutiveFailuresDesc: "Número de falhas consecutivas antes de aplicar resfriamento estendido.",
extendedCooldown: "Resfriamento Estendido",
extendedCooldownDesc: "Duração do resfriamento aplicado após atingir máx. de falhas consecutivas.",
capacityRetryDelay: "Atraso de Retry de Capacidade",
capacityRetryDelayDesc: "Atraso antes de tentar novamente quando capacidade do modelo está esgotada (não quota).",
maxCapacityRetries: "Máx. Retries de Capacidade",
maxCapacityRetriesDesc: "Máximo de retries para esgotamento de capacidade antes de trocar conta.",
saveConfigServer: "Salvar Configuração",
serverRestartAlert: "Alterações salvas em {path}. Reinicie o servidor para aplicar algumas configurações.",
changePassword: "Alterar Senha da WebUI",
@@ -258,4 +271,17 @@ window.translations.pt = {
gemini1mDesc: "Adiciona sufixo [1m] aos modelos Gemini para suporte a janela de contexto de 1M.",
gemini1mWarning: "⚠ Contexto grande pode reduzir o desempenho do Gemini-3-Pro.",
clickToSet: "Clique para configurar...",
// Account Selection Strategy translations
accountSelectionStrategy: "Estratégia de Seleção de Conta",
selectionStrategy: "Estratégia de Seleção",
strategyStickyLabel: "Fixo (Otimizado para Cache)",
strategyRoundRobinLabel: "Rodízio (Balanceamento de Carga)",
strategyHybridLabel: "Híbrido (Distribuição Inteligente)",
strategyStickyDesc: "Permanece na mesma conta até atingir limite. Melhor para cache de prompts.",
strategyRoundRobinDesc: "Alterna para próxima conta a cada requisição. Máximo throughput.",
strategyHybridDesc: "Seleção inteligente baseada em saúde, tokens e frescor.",
strategyUpdated: "Estratégia atualizada para: {strategy}",
failedToUpdateStrategy: "Falha ao atualizar estratégia",
invalidStrategy: "Estratégia inválida selecionada",
};

View File

@@ -216,8 +216,21 @@ window.translations.tr = {
persistTokenDesc: "Daha hızlı yeniden başlatmalar için OAuth oturumlarını diske kaydet",
rateLimiting: "Hesap Hız Sınırlama ve Zaman Aşımları",
defaultCooldown: "Varsayılan Soğuma Süresi",
defaultCooldownDesc: "API sıfırlama zamanı sağlamadığında yedek soğuma süresi.",
maxWaitThreshold: "Maksimum Bekleme Eşiği (Yapışkan)",
maxWaitDesc: "Yapışkan bir hesabın değiştirmeden önce sıfırlanması için beklenecek maksimum süre.",
// Hata İşleme Ayarları
errorHandlingTuning: "Hata İşleme Ayarları",
rateLimitDedupWindow: "Hız Sınırı Tekilleştirme Penceresi",
rateLimitDedupWindowDesc: "Birden fazla istek aynı anda hız sınırına ulaştığında yeniden deneme fırtınasını önler.",
maxConsecutiveFailures: "Maks. Ardışık Başarısızlık",
maxConsecutiveFailuresDesc: "Uzatılmış soğuma uygulamadan önce ardışık başarısızlık sayısı.",
extendedCooldown: "Uzatılmış Soğuma",
extendedCooldownDesc: "Maks. ardışık başarısızlık sonrası uygulanan soğuma süresi.",
capacityRetryDelay: "Kapasite Yeniden Deneme Gecikmesi",
capacityRetryDelayDesc: "Model kapasitesi tükendiğinde (kota değil) yeniden denemeden önceki gecikme.",
maxCapacityRetries: "Maks. Kapasite Yeniden Denemesi",
maxCapacityRetriesDesc: "Hesap değiştirmeden önce kapasite tükenmesi için maksimum yeniden deneme.",
saveConfigServer: "Yapılandırmayı Kaydet",
serverRestartAlert: "Değişiklikler {path} konumuna kaydedildi. Bazı ayarları uygulamak için sunucuyu yeniden başlatın.",
changePassword: "WebUI Parolasını Değiştir",
@@ -313,4 +326,17 @@ window.translations.tr = {
// TODO: Missing translation - Server config (exists in EN but missing here)
// defaultCooldownDesc: "Fallback cooldown when API doesn't provide a reset time.",
// Account Selection Strategy translations
accountSelectionStrategy: "Hesap Seçim Stratejisi",
selectionStrategy: "Seçim Stratejisi",
strategyStickyLabel: "Sabit (Önbellek Optimizasyonu)",
strategyRoundRobinLabel: "Döngüsel (Yük Dengeleme)",
strategyHybridLabel: "Hibrit (Akıllı Dağıtım)",
strategyStickyDesc: "Hız sınırına ulaşılana kadar aynı hesapta kalır. Önbellek için en iyisi.",
strategyRoundRobinDesc: "Her istekte bir sonraki hesaba geçer. Maksimum verimlilik.",
strategyHybridDesc: "Sağlık, token ve tazeliğe dayalı akıllı seçim.",
strategyUpdated: "Strateji şu şekilde güncellendi: {strategy}",
failedToUpdateStrategy: "Strateji güncellenemedi",
invalidStrategy: "Geçersiz strateji seçildi",
};

View File

@@ -237,6 +237,18 @@ window.translations.zh = {
defaultCooldownDesc: "当 API 未提供重置时间时的备用冷却时间。",
maxWaitThreshold: "最大等待阈值",
maxWaitDesc: "如果所有账号的限流时间超过此阈值,立即返回错误而非等待。",
// 错误处理调优
errorHandlingTuning: "错误处理调优",
rateLimitDedupWindow: "限流去重窗口",
rateLimitDedupWindowDesc: "当多个请求同时触发限流时,防止并发重试风暴。",
maxConsecutiveFailures: "最大连续失败次数",
maxConsecutiveFailuresDesc: "触发扩展冷却前允许的连续失败次数。",
extendedCooldown: "扩展冷却时间",
extendedCooldownDesc: "达到最大连续失败后应用的冷却时长。",
capacityRetryDelay: "容量重试延迟",
capacityRetryDelayDesc: "模型容量耗尽(非配额)时重试前的延迟。",
maxCapacityRetries: "最大容量重试次数",
maxCapacityRetriesDesc: "容量耗尽时在切换账号前的最大重试次数。",
saveConfigServer: "保存配置",
serverRestartAlert: "配置已保存至 {path}。部分更改可能需要重启服务器。",
changePassword: "修改 WebUI 密码",
@@ -329,4 +341,17 @@ window.translations.zh = {
// mustBeAtMost: "{fieldName} must be at most {max}",
// cannotBeEmpty: "{fieldName} cannot be empty",
// mustBeTrueOrFalse: "Value must be true or false",
// Account Selection Strategy translations
accountSelectionStrategy: "账户选择策略",
selectionStrategy: "选择策略",
strategyStickyLabel: "固定 (缓存优化)",
strategyRoundRobinLabel: "轮询 (负载均衡)",
strategyHybridLabel: "混合 (智能分配)",
strategyStickyDesc: "保持使用同一账户直到被限速。最适合提示词缓存。",
strategyRoundRobinDesc: "每次请求轮换到下一个账户。最大吞吐量。",
strategyHybridDesc: "基于健康度、令牌和新鲜度的智能选择。",
strategyUpdated: "策略已更新为: {strategy}",
failedToUpdateStrategy: "更新策略失败",
invalidStrategy: "选择了无效的策略",
};

View File

@@ -936,6 +936,35 @@
</div>
</div>
<!-- 🔀 Account Selection Strategy -->
<div class="space-y-3">
<div class="flex items-center gap-2 mb-1 px-1">
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-widest"
x-text="$store.global.t('accountSelectionStrategy')">Account Selection Strategy</span>
<div class="h-px flex-1 bg-space-border/30"></div>
</div>
<div class="form-control view-card border-space-border/50 hover:border-neon-cyan/50">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1 flex-1">
<span class="text-sm font-medium text-gray-200"
x-text="$store.global.t('selectionStrategy')">Selection Strategy</span>
<span class="text-[11px] text-gray-500"
x-text="currentStrategyDescription()">How accounts are selected for requests</span>
</div>
<select
class="select bg-space-800 border-space-border text-gray-200 focus:border-neon-cyan focus:ring-neon-cyan/20 w-64"
:value="serverConfig.accountSelection?.strategy || 'hybrid'"
@change="toggleStrategy($el.value)"
aria-label="Account selection strategy">
<option value="hybrid" x-text="$store.global.t('strategyHybridLabel')">Hybrid (Smart Distribution)</option>
<option value="sticky" x-text="$store.global.t('strategyStickyLabel')">Sticky (Cache Optimized)</option>
<option value="round-robin" x-text="$store.global.t('strategyRoundRobinLabel')">Round Robin (Load Balanced)</option>
</select>
</div>
</div>
</div>
<!-- ▼ Advanced Tuning (Fixed Logic) -->
<div class="view-card !p-0 border-space-border/50">
<div class="flex items-center justify-between p-4 cursor-pointer hover:bg-white/5 transition-colors"
@@ -1095,6 +1124,134 @@
x-text="$store.global.t('maxWaitDesc')">If all accounts are rate-limited longer than this, error immediately.</p>
</div>
</div>
<!-- Error Handling Tuning -->
<div class="space-y-4 pt-2 border-t border-space-border/10">
<div class="flex items-center gap-2 mb-2">
<span class="text-[10px] text-gray-500 font-bold uppercase tracking-widest"
x-text="$store.global.t('errorHandlingTuning')">Error Handling Tuning</span>
</div>
<div class="form-control">
<label class="label pt-0">
<span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('rateLimitDedupWindow')">Rate Limit Dedup Window</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="Math.round((serverConfig.rateLimitDedupWindowMs || 5000) / 1000) + 's'"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="1000" max="30000" step="1000"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.rateLimitDedupWindowMs || 5000"
:style="`background-size: ${((serverConfig.rateLimitDedupWindowMs || 5000) - 1000) / 290}% 100%`"
@input="toggleRateLimitDedupWindowMs($event.target.value)"
aria-label="Rate limit dedup window slider">
<input type="number" min="1000" max="30000" step="1000"
class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.rateLimitDedupWindowMs || 5000"
@change="toggleRateLimitDedupWindowMs($event.target.value)"
aria-label="Rate limit dedup window value">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('rateLimitDedupWindowDesc')">Prevents concurrent retry storms.</p>
</div>
<div class="form-control">
<label class="label pt-0">
<span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('maxConsecutiveFailures')">Max Consecutive Failures</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="serverConfig.maxConsecutiveFailures || 3"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="1" max="10" step="1"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.maxConsecutiveFailures || 3"
:style="`background-size: ${((serverConfig.maxConsecutiveFailures || 3) - 1) / 0.09}% 100%`"
@input="toggleMaxConsecutiveFailures($event.target.value)"
aria-label="Max consecutive failures slider">
<input type="number" min="1" max="10" step="1"
class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxConsecutiveFailures || 3"
@change="toggleMaxConsecutiveFailures($event.target.value)"
aria-label="Max consecutive failures value">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxConsecutiveFailuresDesc')">Failures before extended cooldown.</p>
</div>
<div class="form-control">
<label class="label pt-0">
<span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('extendedCooldown')">Extended Cooldown</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="((serverConfig.extendedCooldownMs || 60000) >= 60000 ? Math.round((serverConfig.extendedCooldownMs || 60000) / 60000) + 'm' : Math.round((serverConfig.extendedCooldownMs || 60000) / 1000) + 's')"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="10000" max="300000" step="10000"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.extendedCooldownMs || 60000"
:style="`background-size: ${((serverConfig.extendedCooldownMs || 60000) - 10000) / 2900}% 100%`"
@input="toggleExtendedCooldownMs($event.target.value)"
aria-label="Extended cooldown slider">
<input type="number" min="10000" max="300000" step="10000"
class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.extendedCooldownMs || 60000"
@change="toggleExtendedCooldownMs($event.target.value)"
aria-label="Extended cooldown value">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('extendedCooldownDesc')">Applied after max consecutive failures.</p>
</div>
<div class="form-control">
<label class="label pt-0">
<span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('capacityRetryDelay')">Capacity Retry Delay</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="Math.round((serverConfig.capacityRetryDelayMs || 2000) / 1000) + 's'"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="500" max="10000" step="500"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.capacityRetryDelayMs || 2000"
:style="`background-size: ${((serverConfig.capacityRetryDelayMs || 2000) - 500) / 95}% 100%`"
@input="toggleCapacityRetryDelayMs($event.target.value)"
aria-label="Capacity retry delay slider">
<input type="number" min="500" max="10000" step="500"
class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.capacityRetryDelayMs || 2000"
@change="toggleCapacityRetryDelayMs($event.target.value)"
aria-label="Capacity retry delay value">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('capacityRetryDelayDesc')">Delay for capacity (not quota) issues.</p>
</div>
<div class="form-control">
<label class="label pt-0">
<span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('maxCapacityRetries')">Max Capacity Retries</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="serverConfig.maxCapacityRetries || 3"></span>
</label>
<div class="flex gap-3 items-center">
<input type="range" min="1" max="10" step="1"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.maxCapacityRetries || 3"
:style="`background-size: ${((serverConfig.maxCapacityRetries || 3) - 1) / 0.09}% 100%`"
@input="toggleMaxCapacityRetries($event.target.value)"
aria-label="Max capacity retries slider">
<input type="number" min="1" max="10" step="1"
class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxCapacityRetries || 3"
@change="toggleMaxCapacityRetries($event.target.value)"
aria-label="Max capacity retries value">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxCapacityRetriesDesc')">Retries before switching accounts.</p>
</div>
</div>
</div>
</div>