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

@@ -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: "选择了无效的策略",
};