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 +74 -0
- package/lib/modelmanager.js +14 -0
- package/package.json +1 -1
- package/providers.json +71 -1
- package/server.js +51 -0
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 = '';
|
package/lib/modelmanager.js
CHANGED
|
@@ -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.
|
|
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) {
|