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/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(),