freegate 0.6.16 → 0.6.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dashboard.js +11 -2
- package/package.json +1 -1
- package/providers.json +1 -1
- package/server.js +35 -11
package/lib/dashboard.js
CHANGED
|
@@ -252,7 +252,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
252
252
|
tabOverview: 'Обзор', tabProviders: 'Провайдеры', tabModels: 'Модели',
|
|
253
253
|
tabContext: 'Контекст', tabSetup: 'Настройки (ключи)',
|
|
254
254
|
kpiProviders: 'Провайдеры', kpiRequests: 'Запросы', kpiRpm: 'RPM', kpiCache: 'Кэш',
|
|
255
|
-
kpiLimit: 'Лимит', kpiModels: 'Модели', kpiUptime: 'Аптайм',
|
|
255
|
+
kpiToday: 'Успех сегодня', kpiLimit: 'Лимит', kpiModels: 'Модели', kpiUptime: 'Аптайм',
|
|
256
256
|
cardSavings: 'Экономия (против платных API)', savings: 'Сэкономлено', tokens: 'токенов',
|
|
257
257
|
savingsBasis: 'расчёт: input $3/M, output $15/M (репрезентативно)',
|
|
258
258
|
cardCache: 'Кэш', cardTokens: 'Токены', cardPool: 'Очередь',
|
|
@@ -288,7 +288,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
288
288
|
tabOverview: 'Overview', tabProviders: 'Providers', tabModels: 'Models',
|
|
289
289
|
tabContext: 'Context', tabSetup: 'Settings (keys)',
|
|
290
290
|
kpiProviders: 'Providers', kpiRequests: 'Requests', kpiRpm: 'RPM', kpiCache: 'Cache',
|
|
291
|
-
kpiLimit: 'Limit', kpiModels: 'Models', kpiUptime: 'Uptime',
|
|
291
|
+
kpiToday: 'Success today', kpiLimit: 'Limit', kpiModels: 'Models', kpiUptime: 'Uptime',
|
|
292
292
|
cardSavings: 'Savings (vs paid APIs)', savings: 'Saved', tokens: 'tokens',
|
|
293
293
|
savingsBasis: 'estimate: input $3/M, output $15/M (representative)',
|
|
294
294
|
cardCache: 'Cache', cardTokens: 'Tokens', cardPool: 'Queue',
|
|
@@ -414,6 +414,12 @@ const HTML = `<!DOCTYPE html>
|
|
|
414
414
|
html += '<div class="kpi-chip">' + t('kpiRequests') + '<b>' + (_statsData ? _statsData.total_requests : 0) + '</b></div>';
|
|
415
415
|
html += '<div class="kpi-chip">' + t('kpiRpm') + '<b>' + rpm + '</b></div>';
|
|
416
416
|
html += '<div class="kpi-chip">' + t('kpiCache') + '<b>' + cacheRate + '%</b></div>';
|
|
417
|
+
// Успех сегодня — главная метрика ценности. Зелёная при высоком, жёлтая/красная при деградации.
|
|
418
|
+
if (_statsData && _statsData.today) {
|
|
419
|
+
var td = _statsData.today;
|
|
420
|
+
var tbCls = td.successRate >= 80 ? '' : td.successRate >= 50 ? ' warn' : ' err';
|
|
421
|
+
html += '<div class="kpi-chip' + tbCls + '">' + t('kpiToday') + '<b>' + td.successRate + '%</b></div>';
|
|
422
|
+
}
|
|
417
423
|
if (riskKey) {
|
|
418
424
|
var cls = riskPct > 90 ? ' err' : riskPct > 60 ? ' warn' : '';
|
|
419
425
|
html += '<div class="kpi-chip' + cls + '">' + t('kpiLimit') + ': ' + esc(riskKey) + '<b>' + riskPct + '%</b></div>';
|
|
@@ -470,8 +476,11 @@ const HTML = `<!DOCTYPE html>
|
|
|
470
476
|
var sv = document.getElementById('savings');
|
|
471
477
|
if (sv) {
|
|
472
478
|
if (_savings && _savings.totalTokens > 0) {
|
|
479
|
+
var td = _statsData && _statsData.today;
|
|
480
|
+
var todayHtml = td ? '<div class="stat"><span class="stat-label">' + t('kpiToday') + '</span><span class="stat-value">' + td.successRate + '% (' + td.requests + ')</span></div>' : '';
|
|
473
481
|
sv.innerHTML =
|
|
474
482
|
'<div class="stat savings"><span class="stat-label">' + t('savings') + '</span><span class="stat-value savings-usd">$' + _savings.savedUsd.toFixed(2) + '</span></div>' +
|
|
483
|
+
todayHtml +
|
|
475
484
|
'<div class="stat"><span class="stat-label">' + t('tokens') + '</span><span class="stat-value">' + fmtTokens(_savings.totalTokens) + '</span></div>' +
|
|
476
485
|
'<div class="stat"><span class="stat-label">in / out</span><span class="stat-value" style="font-size:11px">' + fmtTokens(_savings.promptTokens) + ' / ' + fmtTokens(_savings.completionTokens) + '</span></div>' +
|
|
477
486
|
'<div style="margin-top:6px;font-size:10px;color:#888;">' + t('savingsBasis') + '</div>';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freegate",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
4
4
|
"description": "Free multi-provider LLM gateway with automatic failover. One OpenAI-compatible endpoint routes to 25 free models (Groq, Mistral, Gemini, NIM, OpenRouter, ZAI, Cerebras, DeepSeek). Never pay for LLMs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/providers.json
CHANGED
package/server.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { LRUCache } = require('./lib/cache');
|
|
6
6
|
const { PROVIDERS, MODEL_MAP, callProvider, reloadProviders } = require('./lib/providers');
|
|
7
|
-
const { loadState, initHealth, isCircuitOpen, recordSuccess, recordFailure, recordRequest, recordTokens, getHealth, getStats, recordRecent, recordRpm, getRecent, getRpm, recordSelection, getLastSelection, getBandit, recordBandit, warmBanditPriors, getContextStats } = require('./lib/health');
|
|
7
|
+
const { loadState, initHealth, isCircuitOpen, recordSuccess, recordFailure, recordRequest, recordTokens, getHealth, getStats, getReliability, recordRecent, recordRpm, getRecent, getRpm, recordSelection, getLastSelection, getBandit, recordBandit, warmBanditPriors, getContextStats } = require('./lib/health');
|
|
8
8
|
const { checkRateLimit } = require('./lib/rateLimit');
|
|
9
9
|
const { handleDashboard } = require('./lib/dashboard');
|
|
10
10
|
const { acquire, stats: poolStats } = require('./lib/pool');
|
|
@@ -539,13 +539,22 @@ async function handleChatCompletion(req, res, body) {
|
|
|
539
539
|
|
|
540
540
|
// Prefer providers below 90% of their daily limit; only fall back to
|
|
541
541
|
// near-exhausted ones if that leaves nothing (avoids avoidable 429s).
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
542
|
+
// Крутящийся пул по дневным лимитам: провайдер, исчерпавший дневной лимит,
|
|
543
|
+
// исключается из выбора и не возвращается до сброса. Это лечит 429-спираль
|
|
544
|
+
// (модель с лимитом 50/день сгорает к обеду, дальше каждая попытка на ней —
|
|
545
|
+
// бесполезный 429). Остаёмся на живых; выгоревшие — только как крайний резерв.
|
|
546
|
+
const usedTodayFor = (key) => (getStats().dailyUsage?.[key]?.[today]) || 0;
|
|
547
|
+
const pool = (() => {
|
|
548
|
+
// Провайдеры, у которых дневной лимит ещё не исчерпан (strict < limit).
|
|
549
|
+
const exemptables = windowPool.filter(([_, p]) => {
|
|
550
|
+
const limit = p.dailyLimit || 0;
|
|
551
|
+
if (limit <= 0) return true; // нет лимита — считаем «бесконечным»
|
|
552
|
+
return usedTodayFor(p.key) < limit;
|
|
553
|
+
});
|
|
554
|
+
// Есть живые → только они. Все выгорели → вернуть весь пул (best-effort,
|
|
555
|
+
// bandit-штраф ниже сделает их маловероятными, но не невозможными).
|
|
556
|
+
return exemptables.length > 0 ? exemptables : windowPool;
|
|
557
|
+
})();
|
|
549
558
|
|
|
550
559
|
let selected = [];
|
|
551
560
|
if (pool.length > 0) {
|
|
@@ -571,9 +580,17 @@ async function handleChatCompletion(req, res, body) {
|
|
|
571
580
|
if (taskCategory === 'coding' && cat === 'coding') weight *= 1.5;
|
|
572
581
|
else if (taskCategory === 'reasoning' && cat === 'reasoning') weight *= 1.5;
|
|
573
582
|
else if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'general') weight *= 1.2;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (
|
|
583
|
+
// Простой факт/поиск не должен «думать вслух» на reasoning-модели:
|
|
584
|
+
// это дорого по лимитам и даёт размышления вместо краткого ответа.
|
|
585
|
+
if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'reasoning') weight *= 0.15;
|
|
586
|
+
else if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'vision') weight *= 0.3;
|
|
587
|
+
const dailyLimit = provider.dailyLimit || 0;
|
|
588
|
+
// Провайдер, исчерпавший дневной лимит (попал в пул лишь как крайний
|
|
589
|
+
// резерв, когда живы все выгорели), — сильно штрафуем, чтобы выбрать
|
|
590
|
+
// его только в безвыходной ситуации, а не в первой же попытке.
|
|
591
|
+
const usedToday = usedTodayFor(key);
|
|
592
|
+
if (dailyLimit > 0 && usedToday >= dailyLimit) weight *= 0.03;
|
|
593
|
+
else if (dailyLimit > 0 && usedToday >= dailyLimit * 0.9) weight *= 0.4;
|
|
577
594
|
return { key, provider, weight };
|
|
578
595
|
});
|
|
579
596
|
|
|
@@ -1252,6 +1269,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1252
1269
|
res.end(JSON.stringify({
|
|
1253
1270
|
total_requests: s.totalRequests, successful_requests: s.successfulRequests, failed_requests: s.failedRequests,
|
|
1254
1271
|
provider_usage: s.providerUsage, token_usage: s.tokenUsage, errors: s.errors, uptime_seconds: Math.floor((Date.now() - s.startTime) / 1000),
|
|
1272
|
+
today: (() => {
|
|
1273
|
+
const r = getReliability();
|
|
1274
|
+
let success = 0, fail = 0;
|
|
1275
|
+
for (const [_, v] of Object.entries(r)) { if (v && v.day === today) { success += v.success || 0; fail += v.fail || 0; } }
|
|
1276
|
+
const total = success + fail;
|
|
1277
|
+
return { requests: total, success, failed: fail, successRate: total > 0 ? Math.round((success / total) * 100) : 0 };
|
|
1278
|
+
})(),
|
|
1255
1279
|
savings: (() => { try { return aggregateSavings(s.tokenUsage || {}); } catch { return null; } })(),
|
|
1256
1280
|
health: Object.fromEntries(Object.entries(getHealth()).map(([k, v]) => {
|
|
1257
1281
|
const limit = limits[k];
|