freegate 0.6.20 → 0.6.21

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
@@ -238,6 +238,19 @@ const HTML = `<!DOCTYPE html>
238
238
  .status-wait { color: var(--muted); font-size: 12px; }
239
239
  .hidden { display: none; }
240
240
 
241
+ /* ── Опции оптимизации ── */
242
+ .opt-row { display: flex; align-items: center; gap: 12px; margin: 12px 0 2px; font-size: 13.5px; font-weight: 600; }
243
+ .opt-switch { display: flex; align-items: center; gap: 10px; margin-left: auto; }
244
+ .switch { position: relative; display: inline-block; width: 40px; height: 22px; }
245
+ .switch input { display: none; }
246
+ .slider { position: absolute; inset: 0; background: var(--panel2); border: 1px solid var(--border); border-radius: 999px; cursor: pointer; transition: all .15s ease; }
247
+ .slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; background: var(--muted); border-radius: 50%; transition: all .15s ease; }
248
+ .switch input:checked + .slider { background: rgba(124,92,255,.35); border-color: var(--accent); }
249
+ .switch input:checked + .slider::before { transform: translateX(18px); background: #fff; }
250
+ .opt-hint { font-size: 12px; color: var(--accent-dim); font-weight: 500; }
251
+ .opt-desc { font-size: 12px; color: var(--muted); line-height: 1.55; margin: 0 0 8px; }
252
+ .opt-note { font-size: 12px; color: var(--ok); margin-top: 8px; }
253
+
241
254
  @media (max-width: 900px) {
242
255
  #sidebar { width: 64px; padding: 20px 8px; }
243
256
  .side-brand .name, .tab .label, .side-user .info { display: none; }
@@ -375,6 +388,23 @@ const HTML = `<!DOCTYPE html>
375
388
  <button class="btn btn-save" data-i18n="saveAllKeys" onclick="saveAllKeys()" style="margin-left: 8px;"></button>
376
389
  <span id="save-status" class="status-wait" style="margin-left: 12px;"></span>
377
390
  </div>
391
+ <div class="card" style="margin-bottom:14px;">
392
+ <h2 data-i18n="optBlock"></h2>
393
+ <div class="opt-row"><span data-i18n="optCompress"></span><div class="opt-switch"><label class="switch"><input type="checkbox" id="optCompress"><span class="slider"></span></label><span class="opt-hint" id="optCompressState"></span></div></div>
394
+ <div class="opt-desc" data-i18n="optCompressDesc"></div>
395
+ <div class="opt-row"><span data-i18n="optVetting"></span><div class="opt-switch"><label class="switch"><input type="checkbox" id="optVetting"><span class="slider"></span></label><span class="opt-hint" id="optVettingState"></span></div></div>
396
+ <div class="opt-desc" data-i18n="optVettingDesc"></div>
397
+ <div class="opt-row"><span data-i18n="optRouting"></span>
398
+ <select id="optRouting" class="filter" onchange="saveOpts()">
399
+ <option value="weighted" data-i18n="optWeighted"></option>
400
+ <option value="weighted-roundrobin" data-i18n="optRoundrobin"></option>
401
+ <option value="weighted-least" data-i18n="optLeast"></option>
402
+ </select>
403
+ <span class="opt-hint" id="optRoutingState"></span>
404
+ </div>
405
+ <div class="opt-desc" data-i18n="optRoutingDesc"></div>
406
+ <div class="opt-note" id="optSaveNote"></div>
407
+ </div>
378
408
  <div id="setup-grid" class="setup-grid" data-i18n="loading"></div>
379
409
  </section>
380
410
  </main>
@@ -395,6 +425,11 @@ const HTML = `<!DOCTYPE html>
395
425
  ru: {
396
426
  tabOverview: 'Обзор', tabProviders: 'Провайдеры', tabModels: 'Модели',
397
427
  tabContext: 'Контекст', tabSetup: 'Настройки (ключи)',
428
+ optBlock: 'Опции оптимизации', optCompress: 'Сжатие промптов', optVetting: 'Самопроверка', optRouting: 'Стратегия роутинга',
429
+ optWeighted: 'weighted (умный)', optRoundrobin: 'weighted-roundrobin (по кругу)', optLeast: 'weighted-least (меньше нагруженные)',
430
+ optCompressDesc: 'Убирает вежливость и «мусорные» слова («пожалуйста», «привет», «наверное») из последнего сообщения — меньше токенов, быстрее и дешевле. Не трогает код.',
431
+ optVettingDesc: 'Сложные ответы (код/рассуждения) проверяет второй моделью и помечает ошибки. Тратит один лишний запрос на проверку.',
432
+ optRoutingDesc: 'Как Freegate выбирает провайдера: умный взвешенный, по кругу (равномерно), или наименее нагруженные.', optSaved: 'Сохранено! Перезапусти Freegate для применения.',
398
433
  crumb: 'Админ', pageTitle: 'Панель управления', pageSub: 'Мониторинг и управление Freegate', topTitle: 'Панель управления',
399
434
  kpiProviders: 'Провайдеры', kpiRequests: 'Запросы', kpiRpm: 'RPM', kpiCache: 'Кэш',
400
435
  kpiToday: 'Успех сегодня', kpiLimit: 'Лимит', kpiModels: 'Модели', kpiUptime: 'Аптайм',
@@ -433,6 +468,11 @@ const HTML = `<!DOCTYPE html>
433
468
  en: {
434
469
  tabOverview: 'Overview', tabProviders: 'Providers', tabModels: 'Models',
435
470
  tabContext: 'Context', tabSetup: 'Settings (keys)',
471
+ optBlock: 'Optimization options', optCompress: 'Prompt compression', optVetting: 'Self-check', optRouting: 'Routing strategy',
472
+ optWeighted: 'weighted (smart)', optRoundrobin: 'weighted-roundrobin (round-robin)', optLeast: 'weighted-least (least-used)',
473
+ optCompressDesc: 'Strips politeness and filler words ("please", "hello", "probably") from the last message — fewer tokens, faster and cheaper. Never touches code.',
474
+ optVettingDesc: 'Complex answers (code/reasoning) get vetted by a second model and flagged on error. Costs one extra request per check.',
475
+ optRoutingDesc: 'How Freegate picks a provider: smart weighted, round-robin (even), or least-loaded.', optSaved: 'Saved! Restart Freegate to apply.',
436
476
  crumb: 'Admin', pageTitle: 'Control Panel', pageSub: 'Freegate monitoring & management', topTitle: 'Control Panel',
437
477
  kpiProviders: 'Providers', kpiRequests: 'Requests', kpiRpm: 'RPM', kpiCache: 'Cache',
438
478
  kpiToday: 'Success today', kpiLimit: 'Limit', kpiModels: 'Models', kpiUptime: 'Uptime',
@@ -995,7 +1035,41 @@ const HTML = `<!DOCTYPE html>
995
1035
  _setupData = await res.json();
996
1036
  renderSetup(_setupData);
997
1037
  } catch (e) { console.error('Setup load error:', e); }
1038
+ loadOpts();
1039
+ }
1040
+
1041
+ // ── Опции оптимизации (compress/vetting/routing) ──
1042
+ async function loadOpts() {
1043
+ try {
1044
+ var res = await fetch(_api('/v1/config'));
1045
+ var c = await res.json();
1046
+ document.getElementById('optCompress').checked = !!c.compress.enabled;
1047
+ document.getElementById('optVetting').checked = !!c.vetting.enabled;
1048
+ document.getElementById('optRouting').value = c.routing.strategy || 'weighted';
1049
+ var note = document.getElementById('optSaveNote');
1050
+ if (note) note.textContent = '';
1051
+ } catch (e) { console.error('Config load error:', e); }
1052
+ }
1053
+ function saveOpts() {
1054
+ var body = {
1055
+ compress: { enabled: document.getElementById('optCompress').checked },
1056
+ vetting: { enabled: document.getElementById('optVetting').checked },
1057
+ routing: { strategy: document.getElementById('optRouting').value },
1058
+ };
1059
+ fetch(_api('/v1/config'), {
1060
+ method: 'POST',
1061
+ headers: { 'Content-Type': 'application/json' },
1062
+ body: JSON.stringify(body),
1063
+ }).then(function (r) { return r.json(); }).then(function (d) {
1064
+ var note = document.getElementById('optSaveNote');
1065
+ if (note) note.textContent = d.ok ? (t('optSaved')) : (d.error || t('saveErr'));
1066
+ }).catch(function (e) {
1067
+ var note = document.getElementById('optSaveNote');
1068
+ if (note) note.textContent = e.message;
1069
+ });
998
1070
  }
1071
+ document.getElementById('optCompress').addEventListener('change', saveOpts);
1072
+ document.getElementById('optVetting').addEventListener('change', saveOpts);
999
1073
  function renderSetup(data) {
1000
1074
  var grid = document.getElementById('setup-grid');
1001
1075
  var html = '';
@@ -26,6 +26,13 @@ function inferSource(providerEntry) {
26
26
  const env = providerEntry.envVar || '';
27
27
  if (env.includes('OPENROUTER')) return 'openrouter';
28
28
  if (env.includes('HF')) return 'huggingface';
29
+ if (env.includes('SAMBANOVA')) return 'sambanova';
30
+ if (env.includes('SILICONFLOW')) return 'siliconflow';
31
+ if (env.includes('DEEPINFRA')) return 'deepinfra';
32
+ if (env.includes('HYPERBOLIC')) return 'hyperbolic';
33
+ if (env.includes('COHERE')) return 'cohere';
34
+ if (env.includes('LLM7')) return 'llm7';
35
+ if (env.includes('NARA')) return 'nara';
29
36
  const ep = providerEntry.endpoint || '';
30
37
  if (ep.includes('api.groq.com')) return 'groq';
31
38
  if (ep.includes('api.mistral.ai')) return 'mistral';
@@ -33,6 +40,13 @@ function inferSource(providerEntry) {
33
40
  if (ep.includes('api.cerebras.ai')) return 'cerebras';
34
41
  if (ep.includes('api.deepseek.com')) return 'deepseek';
35
42
  if (ep.includes('integrate.api.nvidia')) return 'nim';
43
+ if (ep.includes('api.sambanova.ai')) return 'sambanova';
44
+ if (ep.includes('api.siliconflow.com')) return 'siliconflow';
45
+ if (ep.includes('api.deepinfra.com')) return 'deepinfra';
46
+ if (ep.includes('api.hyperbolic.xyz')) return 'hyperbolic';
47
+ if (ep.includes('api.cohere.com')) return 'cohere';
48
+ if (ep.includes('api.llm7.io')) return 'llm7';
49
+ if (ep.includes('router.bynara.id')) return 'nara';
36
50
  return 'unknown';
37
51
  }
38
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freegate",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
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
@@ -634,5 +634,75 @@
634
634
  "free": true,
635
635
  "category": "general",
636
636
  "context_window": 1000000
637
+ },
638
+ "gemini-gemma-4-26b-a4b-it": {
639
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
640
+ "model": "gemma-4-26b-a4b-it",
641
+ "priority": 1,
642
+ "dailyLimit": 1500,
643
+ "keyHint": "gemini → Keys (автодобавлено, general)",
644
+ "envVar": "PROVIDER_GEMINI_APIKEY",
645
+ "free": true,
646
+ "category": "general"
647
+ },
648
+ "gemini-gemma-4-31b-it": {
649
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
650
+ "model": "gemma-4-31b-it",
651
+ "priority": 5,
652
+ "dailyLimit": 1500,
653
+ "keyHint": "gemini → Keys (автодобавлено, general)",
654
+ "envVar": "PROVIDER_GEMINI_APIKEY",
655
+ "free": true,
656
+ "category": "general"
657
+ },
658
+ "gemini-gemini-flash-lite-latest": {
659
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
660
+ "model": "gemini-flash-lite-latest",
661
+ "priority": 2,
662
+ "dailyLimit": 1500,
663
+ "keyHint": "gemini → Keys (автодобавлено, general)",
664
+ "envVar": "PROVIDER_GEMINI_APIKEY",
665
+ "free": true,
666
+ "category": "general"
667
+ },
668
+ "gemini-gemini-3-1-flash-lite-preview": {
669
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
670
+ "model": "gemini-3.1-flash-lite-preview",
671
+ "priority": 6,
672
+ "dailyLimit": 1500,
673
+ "keyHint": "gemini → Keys (автодобавлено, general)",
674
+ "envVar": "PROVIDER_GEMINI_APIKEY",
675
+ "free": true,
676
+ "category": "general"
677
+ },
678
+ "gemini-gemini-3-5-flash": {
679
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
680
+ "model": "gemini-3.5-flash",
681
+ "priority": 4,
682
+ "dailyLimit": 1500,
683
+ "keyHint": "gemini → Keys (автодобавлено, general)",
684
+ "envVar": "PROVIDER_GEMINI_APIKEY",
685
+ "free": true,
686
+ "category": "general"
687
+ },
688
+ "gemini-gemini-3-5-flash-lite": {
689
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
690
+ "model": "gemini-3.5-flash-lite",
691
+ "priority": 3,
692
+ "dailyLimit": 1500,
693
+ "keyHint": "gemini → Keys (автодобавлено, general)",
694
+ "envVar": "PROVIDER_GEMINI_APIKEY",
695
+ "free": true,
696
+ "category": "general"
697
+ },
698
+ "gemini-gemini-3-1-flash-lite": {
699
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
700
+ "model": "gemini-3.1-flash-lite",
701
+ "priority": 7,
702
+ "dailyLimit": 1500,
703
+ "keyHint": "gemini → Keys (автодобавлено, general)",
704
+ "envVar": "PROVIDER_GEMINI_APIKEY",
705
+ "free": true,
706
+ "category": "general"
637
707
  }
638
- }
708
+ }
package/server.js CHANGED
@@ -1253,6 +1253,57 @@ const server = http.createServer(async (req, res) => {
1253
1253
  return;
1254
1254
  }
1255
1255
 
1256
+ if (parsedUrl.pathname === '/v1/config') {
1257
+ // Чтение/запись опций оптимизации (compress/vetting/routing) в config.json.
1258
+ if (AUTH_KEY) {
1259
+ const apiKey = (req.headers.authorization || '').replace('Bearer ', '').trim();
1260
+ const keyFromQuery = parsedUrl.searchParams.get('key');
1261
+ if (apiKey !== AUTH_KEY && keyFromQuery !== AUTH_KEY) {
1262
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1263
+ res.end(JSON.stringify({ error: { message: 'Invalid API key' } }));
1264
+ return;
1265
+ }
1266
+ }
1267
+ try {
1268
+ const userCfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
1269
+ if (req.method === 'GET') {
1270
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1271
+ res.end(JSON.stringify({
1272
+ compress: { enabled: !!(userCfg.compress && userCfg.compress.enabled), minLen: userCfg.compress?.minLen || 60 },
1273
+ vetting: { enabled: !!(userCfg.vetting && userCfg.vetting.enabled), minAnswerLen: userCfg.vetting?.minAnswerLen || 120, complexityOnly: userCfg.vetting?.complexityOnly !== false },
1274
+ routing: { strategy: userCfg.routing?.strategy || 'weighted' },
1275
+ }));
1276
+ return;
1277
+ }
1278
+ if (req.method === 'POST') {
1279
+ let body = '';
1280
+ req.on('data', (c) => { body += c; if (body.length > 100000) req.destroy(); });
1281
+ req.on('end', () => {
1282
+ try {
1283
+ const patch = JSON.parse(body || '{}');
1284
+ if (typeof patch.compress === 'object') userCfg.compress = Object.assign({ enabled: false, minLen: 60 }, userCfg.compress, patch.compress);
1285
+ if (typeof patch.vetting === 'object') userCfg.vetting = Object.assign({ enabled: false, minAnswerLen: 120, complexityOnly: true }, userCfg.vetting, patch.vetting);
1286
+ if (typeof patch.routing === 'object') userCfg.routing = Object.assign({ strategy: 'weighted' }, userCfg.routing, patch.routing);
1287
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1288
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1289
+ res.end(JSON.stringify({ ok: true }));
1290
+ } catch (e) {
1291
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1292
+ res.end(JSON.stringify({ error: { message: 'Invalid config body: ' + e.message } }));
1293
+ }
1294
+ });
1295
+ return;
1296
+ }
1297
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1298
+ res.end(JSON.stringify({ error: { message: 'Method not allowed' } }));
1299
+ return;
1300
+ } catch (err) {
1301
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1302
+ res.end(JSON.stringify({ error: { message: 'Config read failed: ' + err.message } }));
1303
+ return;
1304
+ }
1305
+ }
1306
+
1256
1307
  if (parsedUrl.pathname === '/v1/models-db' && req.method === 'GET') {
1257
1308
  // Структурированная база моделей: паспорта + статистика + топ по скору.
1258
1309
  if (AUTH_KEY) {