freegate 0.6.18 → 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/.env.example +13 -1
- package/README.md +9 -4
- package/README.ru.md +9 -3
- package/assets/dashboard-en.gif +0 -0
- package/assets/dashboard-en.png +0 -0
- package/assets/dashboard-models-en.png +0 -0
- package/assets/dashboard.gif +0 -0
- package/bin/freegate.js +107 -0
- package/config.example.json +33 -0
- package/lib/cache.js +19 -4
- package/lib/compress.js +85 -0
- package/lib/dashboard.js +156 -11
- package/lib/doctor.js +106 -0
- package/lib/health.js +48 -0
- package/lib/modelmanager.js +14 -0
- package/lib/modelscan.js +7 -2
- package/lib/onboarding.js +79 -0
- package/lib/setup.js +136 -0
- package/lib/strategy.js +57 -0
- package/lib/vetting.js +91 -0
- package/package.json +1 -1
- package/providers.json +335 -1
- package/server.js +120 -6
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, getReliability, 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, getHourly } = 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');
|
|
@@ -99,6 +99,30 @@ const METHODOLOGY_CONFIG = Object.assign(
|
|
|
99
99
|
(config.methodology && typeof config.methodology === 'object') ? config.methodology : {}
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
+
// --- Самопроверка ответа второй моделью (vetting) ---
|
|
103
|
+
// Опционально: после не-stream ответа отправляем краткий чек другой модели.
|
|
104
|
+
// Выключено по умолчанию (жжёт 2-й free-лимит). Включается config.vetting.enabled.
|
|
105
|
+
const { shouldVet, vetAnswer, VETTING_DEFAULTS } = require('./lib/vetting');
|
|
106
|
+
const VETTING_CONFIG = Object.assign(
|
|
107
|
+
{ ...VETTING_DEFAULTS },
|
|
108
|
+
(config.vetting && typeof config.vetting === 'object') ? config.vetting : {}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// --- Стратегия роутинга ---
|
|
112
|
+
// Опциональные модификаторы равномерности (round-robin / least-used).
|
|
113
|
+
// По умолчанию 'weighted' — поведение без изменений.
|
|
114
|
+
const { makeWeightModifier } = require('./lib/strategy');
|
|
115
|
+
const ROUTING_STRATEGY = (config.routing && config.routing.strategy) || 'weighted';
|
|
116
|
+
|
|
117
|
+
// --- Сжатие промпта (Caveman-стиль) ---
|
|
118
|
+
// Опционально убирает вежливость/заполнители из последнего user-сообщения,
|
|
119
|
+
// экономя токены. config.compress.enabled=true включает.
|
|
120
|
+
const { compressMessages } = require('./lib/compress');
|
|
121
|
+
const COMPRESS_CONFIG = Object.assign(
|
|
122
|
+
{ enabled: false, minLen: 60 },
|
|
123
|
+
(config.compress && typeof config.compress === 'object') ? config.compress : {}
|
|
124
|
+
);
|
|
125
|
+
|
|
102
126
|
// --- Веб-поиск для search-задач ---
|
|
103
127
|
// Бесплатный поиск фактов (DuckDuckGo, без ключа) для запросов-поиска, чтобы
|
|
104
128
|
// модель не галлюцинировала («что такое минимакс дизайн» → реальная инфа про
|
|
@@ -459,6 +483,15 @@ async function handleChatCompletion(req, res, body) {
|
|
|
459
483
|
if (METHODOLOGY_CONFIG.enabled && Array.isArray(body.messages)) {
|
|
460
484
|
try {
|
|
461
485
|
taskCategory = classifyTask(body.messages);
|
|
486
|
+
// Сжатие промпта: убираем вежливость/заполнители ДО методолога (методолог
|
|
487
|
+
// не должен суммировать сжатый текст). Опционально (config.compress).
|
|
488
|
+
if (COMPRESS_CONFIG.enabled) {
|
|
489
|
+
const compressed = compressMessages(body.messages, COMPRESS_CONFIG);
|
|
490
|
+
if (compressed !== body.messages && Array.isArray(compressed)) {
|
|
491
|
+
body.messages = compressed;
|
|
492
|
+
measure.compressed = 1;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
462
495
|
const injected = injectMethodology(body.messages, taskCategory, METHODOLOGY_CONFIG);
|
|
463
496
|
if (injected !== body.messages) {
|
|
464
497
|
body.messages = injected;
|
|
@@ -608,6 +641,9 @@ async function handleChatCompletion(req, res, body) {
|
|
|
608
641
|
.sort((a, b) => (b.provider.context_window || 0) - (a.provider.context_window || 0))
|
|
609
642
|
.slice(0, 1);
|
|
610
643
|
} else {
|
|
644
|
+
const usedTodayList = {};
|
|
645
|
+
for (const [k] of pool) usedTodayList[k] = usedTodayFor(k);
|
|
646
|
+
const strategyModifier = makeWeightModifier(ROUTING_STRATEGY, { keys: pool.map(([k]) => k), usedTodayList });
|
|
611
647
|
const scored = pool.map(([key, provider]) => {
|
|
612
648
|
const h = getHealth()[key];
|
|
613
649
|
let score = h.score || 50;
|
|
@@ -640,6 +676,9 @@ async function handleChatCompletion(req, res, body) {
|
|
|
640
676
|
const usedToday = usedTodayFor(key);
|
|
641
677
|
if (dailyLimit > 0 && usedToday >= dailyLimit) weight *= 0.03;
|
|
642
678
|
else if (dailyLimit > 0 && usedToday >= dailyLimit * 0.9) weight *= 0.4;
|
|
679
|
+
// Стратегия роутинга: равномерность (round-robin / least-used) как
|
|
680
|
+
// лёгкий модификатор к базовому weight — не ломает основной скоринг.
|
|
681
|
+
weight *= strategyModifier(key);
|
|
643
682
|
return { key, provider, weight };
|
|
644
683
|
});
|
|
645
684
|
|
|
@@ -836,7 +875,7 @@ async function handleChatCompletion(req, res, body) {
|
|
|
836
875
|
model: provider.model,
|
|
837
876
|
choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
|
|
838
877
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
839
|
-
});
|
|
878
|
+
}, key);
|
|
840
879
|
}
|
|
841
880
|
commit(200);
|
|
842
881
|
res.end();
|
|
@@ -946,7 +985,7 @@ async function handleChatCompletion(req, res, body) {
|
|
|
946
985
|
model: provider.model,
|
|
947
986
|
choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
|
|
948
987
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
949
|
-
});
|
|
988
|
+
}, key);
|
|
950
989
|
}
|
|
951
990
|
commit(200);
|
|
952
991
|
res.end();
|
|
@@ -971,8 +1010,31 @@ async function handleChatCompletion(req, res, body) {
|
|
|
971
1010
|
measure.provider = key;
|
|
972
1011
|
measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
|
|
973
1012
|
measure.win = PROVIDERS[key]?.context_window || 0;
|
|
1013
|
+
// Самопроверка второй моделью (опционально): только не-stream, только по конфигу.
|
|
1014
|
+
if (VETTING_CONFIG.enabled && !isStreaming) {
|
|
1015
|
+
const answer = result.data?.choices?.[0]?.message?.content || '';
|
|
1016
|
+
if (shouldVet({ config: VETTING_CONFIG, complexity, category: taskCategory, answerLen: answer.length })) {
|
|
1017
|
+
const picks = Object.entries(PROVIDERS)
|
|
1018
|
+
.filter(([pk, p]) => p.enabled && pk !== key && !isCircuitOpen(pk) &&
|
|
1019
|
+
getHealth()[pk]?.status === 'up' && p.vision !== true)
|
|
1020
|
+
.map(([_, p]) => p);
|
|
1021
|
+
try {
|
|
1022
|
+
const verdict = await vetAnswer({ answer, callProvider, picks, config: VETTING_CONFIG });
|
|
1023
|
+
if (verdict.checked && !verdict.ok && verdict.note) {
|
|
1024
|
+
const msg = result.data.choices[0].message;
|
|
1025
|
+
msg.content = (msg.content || '') + '\n\n> ⚠️ Проверка второй моделью: ' + verdict.note;
|
|
1026
|
+
measure.vetted = 1;
|
|
1027
|
+
measure.vetNote = verdict.note;
|
|
1028
|
+
} else {
|
|
1029
|
+
measure.vetted = 0;
|
|
1030
|
+
}
|
|
1031
|
+
} catch (vetErr) {
|
|
1032
|
+
measure.vetted = 0;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
974
1036
|
commit(200);
|
|
975
|
-
cache.set(effectiveModel, body.messages, body.temperature, result.data);
|
|
1037
|
+
cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
|
|
976
1038
|
recordTokens(key, result.usage);
|
|
977
1039
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
978
1040
|
res.end(JSON.stringify(result.data));
|
|
@@ -1059,7 +1121,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
|
|
|
1059
1121
|
measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
|
|
1060
1122
|
measure.win = PROVIDERS[key]?.context_window || 0;
|
|
1061
1123
|
commit(200);
|
|
1062
|
-
cache.set(effectiveModel, body.messages, body.temperature, result.data);
|
|
1124
|
+
cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
|
|
1063
1125
|
recordTokens(key, result.usage);
|
|
1064
1126
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1065
1127
|
res.end(JSON.stringify(result.data));
|
|
@@ -1117,7 +1179,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
|
|
|
1117
1179
|
model: provider.model,
|
|
1118
1180
|
choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
|
|
1119
1181
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
1120
|
-
});
|
|
1182
|
+
}, key);
|
|
1121
1183
|
}
|
|
1122
1184
|
commit(200);
|
|
1123
1185
|
res.end();
|
|
@@ -1191,6 +1253,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
1191
1253
|
return;
|
|
1192
1254
|
}
|
|
1193
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
|
+
|
|
1194
1307
|
if (parsedUrl.pathname === '/v1/models-db' && req.method === 'GET') {
|
|
1195
1308
|
// Структурированная база моделей: паспорта + статистика + топ по скору.
|
|
1196
1309
|
if (AUTH_KEY) {
|
|
@@ -1346,6 +1459,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1346
1459
|
return [k, { status: v.status, score: v.score, latency_ms: v.latency, reason, reliability }];
|
|
1347
1460
|
})),
|
|
1348
1461
|
cache: cache.stats(),
|
|
1462
|
+
hourly: (() => { try { return getHourly(); } catch { return []; } })(),
|
|
1349
1463
|
limits,
|
|
1350
1464
|
pool: poolStats(),
|
|
1351
1465
|
last_selection: getLastSelection(),
|