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 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.16",
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
@@ -285,7 +285,7 @@
285
285
  "keyHint": "openrouter.ai → Keys (автодобавлено, vision)",
286
286
  "envVar": "PROVIDER_OPENROUTER_APIKEY",
287
287
  "free": true,
288
- "category": "vision",
288
+ "category": "general",
289
289
  "context_window": 1048576
290
290
  },
291
291
  "or-minimax-m2-7-free": {
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
- let pool = windowPool;
543
- const underLimit = windowPool.filter(([_, p]) => {
544
- const limit = p.dailyLimit || 1000;
545
- const used = (getStats().dailyUsage?.[p.key]?.[today]) || 0;
546
- return used < limit * 0.9;
547
- });
548
- if (underLimit.length > 0) pool = underLimit;
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
- const dailyLimit = provider.dailyLimit || 1000;
575
- const usedToday = getStats().providerUsage[key] || 0;
576
- if (usedToday >= dailyLimit * 0.9) weight *= 0.5;
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];