freegate 0.6.17 → 0.6.20

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,11 +4,12 @@ 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');
11
11
  const { aggregateSavings } = require('./lib/economics');
12
+ const websearch = require('./lib/websearch');
12
13
  const { stripThink, cleanDelta, cleanMessage, fixReasoningMessage, isTooShort, MIN_ANSWER_LEN } = require('./lib/clean');
13
14
  const { classifyComplexity, maybeUpgradeTier, classifyVisionComplexity, needsWindowUpgrade } = require('./lib/routing');
14
15
  const { bucket, pick: banditPick, isTransientLimit } = require('./lib/bandit');
@@ -42,7 +43,10 @@ const warmed = warmBanditPriors(
42
43
  );
43
44
  if (warmed > 0) logger.info('Warmed bandit priors', { warmed });
44
45
 
45
- const cache = new LRUCache(500, 3600000, false, true); // 4th arg: semantic normalize ON
46
+ // Диск-кэш: повторы промптов не жгут free-лимиты. TTL 24ч (чаты/агенты часто
47
+ // переотправляют одинаковые запросы — ретраи, повторные вопросы; дневной кэш
48
+ // снимает лишнюю нагрузку). Семантический normalize ON (4-й арг).
49
+ const cache = new LRUCache(500, 24 * 3600 * 1000, false, true);
46
50
  require('./lib/cache')._activeCache = cache;
47
51
 
48
52
  // Load config (with fallback so a corrupt config never crashes the server)
@@ -95,6 +99,39 @@ const METHODOLOGY_CONFIG = Object.assign(
95
99
  (config.methodology && typeof config.methodology === 'object') ? config.methodology : {}
96
100
  );
97
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
+
126
+ // --- Веб-поиск для search-задач ---
127
+ // Бесплатный поиск фактов (DuckDuckGo, без ключа) для запросов-поиска, чтобы
128
+ // модель не галлюцинировала («что такое минимакс дизайн» → реальная инфа про
129
+ // MiniMax, а не «минимализм»). Только для search-задач. Сбои не ломают запрос.
130
+ const WEBSEARCH_CONFIG = Object.assign(
131
+ { enabled: true, limit: 5, timeout: 8000, queryMinChars: 6 },
132
+ (config.websearch && typeof config.websearch === 'object') ? config.websearch : {}
133
+ );
134
+
98
135
  // --- Самообновляющаяся база моделей (Model Discovery Engine) ---
99
136
  // Планировщик живёт внутри сервера: работает «всегда» у всех пользователей
100
137
  // пакета без cron/launchd. config.modelManager.enabled=false отключает.
@@ -446,6 +483,15 @@ async function handleChatCompletion(req, res, body) {
446
483
  if (METHODOLOGY_CONFIG.enabled && Array.isArray(body.messages)) {
447
484
  try {
448
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
+ }
449
495
  const injected = injectMethodology(body.messages, taskCategory, METHODOLOGY_CONFIG);
450
496
  if (injected !== body.messages) {
451
497
  body.messages = injected;
@@ -456,6 +502,35 @@ async function handleChatCompletion(req, res, body) {
456
502
  }
457
503
  }
458
504
 
505
+ // --- Веб-поиск для search-задач ---
506
+ // Нашли факты из интернета (DuckDuckGo, без ключа) и подмешали как контекст,
507
+ // чтобы модель отвечала по существу, а не галлюцинировала. Только search.
508
+ // Любой сбой (нет сети/таймаут/пусто) = просто пропускаем, запрос идёт как есть.
509
+ let searchContext = '';
510
+ if (WEBSEARCH_CONFIG.enabled && (taskCategory === 'search' || taskCategory === 'chat')) {
511
+ try {
512
+ const userTexts = (body.messages || [])
513
+ .filter(m => m && m.role === 'user' && typeof m.content === 'string')
514
+ .map(m => m.content.trim());
515
+ const query = userTexts[userTexts.length - 1] || '';
516
+ if (query.length >= WEBSEARCH_CONFIG.queryMinChars) {
517
+ const results = await websearch.search(query, {
518
+ fetchImpl: (url, opts) => fetch(url, opts),
519
+ limit: WEBSEARCH_CONFIG.limit,
520
+ timeout: WEBSEARCH_CONFIG.timeout,
521
+ });
522
+ if (results.length > 0) {
523
+ searchContext = websearch.toContext(results, query);
524
+ measure.websearch = true;
525
+ body.messages.push({ role: 'system', content: searchContext });
526
+ logger.info('Websearch', { query: query.slice(0, 80), results: results.length });
527
+ }
528
+ }
529
+ } catch (err) {
530
+ logger.error('Websearch error', { message: err.message });
531
+ }
532
+ }
533
+
459
534
  // Replays a previously cached completion (exact or semantic hit), preserving
460
535
  // the stream/non-stream shape the client asked for.
461
536
  function serveCached(res, cached, isStreaming) {
@@ -566,6 +641,9 @@ async function handleChatCompletion(req, res, body) {
566
641
  .sort((a, b) => (b.provider.context_window || 0) - (a.provider.context_window || 0))
567
642
  .slice(0, 1);
568
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 });
569
647
  const scored = pool.map(([key, provider]) => {
570
648
  const h = getHealth()[key];
571
649
  let score = h.score || 50;
@@ -578,12 +656,19 @@ async function handleChatCompletion(req, res, body) {
578
656
  // не исключая fallback. coding→coding, reasoning→reasoning, chat/search→general.
579
657
  const cat = provider.category || 'general';
580
658
  if (taskCategory === 'coding' && cat === 'coding') weight *= 1.5;
659
+ else if (taskCategory === 'design' && cat === 'coding') weight *= 1.6;
581
660
  else if (taskCategory === 'reasoning' && cat === 'reasoning') weight *= 1.5;
582
661
  else if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'general') weight *= 1.2;
662
+ // Дизайн — это UI-код: визуальные рекомендации идут от coding-моделей.
663
+ else if (taskCategory === 'design' && cat === 'reasoning') weight *= 0.4;
583
664
  // Простой факт/поиск не должен «думать вслух» на reasoning-модели:
584
665
  // это дорого по лимитам и даёт размышления вместо краткого ответа.
585
666
  if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'reasoning') weight *= 0.15;
586
667
  else if ((taskCategory === 'chat' || taskCategory === 'search') && cat === 'vision') weight *= 0.3;
668
+ // Поиск фактов — не кодинг-задача: codestral (coding) на вопросе
669
+ // «что такое минимакс дизайн» выдаёт рассуждение про «минимализм».
670
+ // Отдаём search на general-модели (minimax/small), а не на кодеров.
671
+ if (taskCategory === 'search' && cat === 'coding') weight *= 0.3;
587
672
  const dailyLimit = provider.dailyLimit || 0;
588
673
  // Провайдер, исчерпавший дневной лимит (попал в пул лишь как крайний
589
674
  // резерв, когда живы все выгорели), — сильно штрафуем, чтобы выбрать
@@ -591,6 +676,9 @@ async function handleChatCompletion(req, res, body) {
591
676
  const usedToday = usedTodayFor(key);
592
677
  if (dailyLimit > 0 && usedToday >= dailyLimit) weight *= 0.03;
593
678
  else if (dailyLimit > 0 && usedToday >= dailyLimit * 0.9) weight *= 0.4;
679
+ // Стратегия роутинга: равномерность (round-robin / least-used) как
680
+ // лёгкий модификатор к базовому weight — не ломает основной скоринг.
681
+ weight *= strategyModifier(key);
594
682
  return { key, provider, weight };
595
683
  });
596
684
 
@@ -634,13 +722,19 @@ async function handleChatCompletion(req, res, body) {
634
722
  const tLimit = (getStats().dailyUsage?.[targetProviderKey]?.[today]) || 0;
635
723
  const tCap = PROVIDERS[targetProviderKey].dailyLimit || 0;
636
724
  const targetBurned = tHealth?.status === 'ratelimited' || (tCap > 0 && tLimit >= tCap * 0.9);
637
- if (!targetBurned) {
725
+ // Кодинг-target (codestral) не подходит для поиска фактов: на вопросе
726
+ // «что такое минимакс дизайн» он отвечает «минимализм», а не про нейросеть.
727
+ // Для search/chat-задач НЕ ставим кодинг-модель первой — пусть выберётся
728
+ // general-модель (minimax/small), которая отвечает по существу.
729
+ const targetIsCoding = (PROVIDERS[targetProviderKey].category || '') === 'coding';
730
+ const targetMismatch = (taskCategory === 'search' || taskCategory === 'chat') && targetIsCoding;
731
+ if (!targetBurned && !targetMismatch) {
638
732
  enabledProviders = [
639
733
  [targetProviderKey, PROVIDERS[targetProviderKey]],
640
734
  ...enabledProviders.filter(([k]) => k !== targetProviderKey),
641
735
  ];
642
736
  } else {
643
- logger.info('Target-first skip', { key: targetProviderKey, status: tHealth?.status, used: tLimit, cap: tCap });
737
+ logger.info('Target-first skip', { key: targetProviderKey, status: tHealth?.status, used: tLimit, cap: tCap, mismatch: targetMismatch });
644
738
  }
645
739
  }
646
740
 
@@ -781,7 +875,7 @@ async function handleChatCompletion(req, res, body) {
781
875
  model: provider.model,
782
876
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
783
877
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
784
- });
878
+ }, key);
785
879
  }
786
880
  commit(200);
787
881
  res.end();
@@ -891,7 +985,7 @@ async function handleChatCompletion(req, res, body) {
891
985
  model: provider.model,
892
986
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
893
987
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
894
- });
988
+ }, key);
895
989
  }
896
990
  commit(200);
897
991
  res.end();
@@ -916,8 +1010,31 @@ async function handleChatCompletion(req, res, body) {
916
1010
  measure.provider = key;
917
1011
  measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
918
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
+ }
919
1036
  commit(200);
920
- cache.set(effectiveModel, body.messages, body.temperature, result.data);
1037
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
921
1038
  recordTokens(key, result.usage);
922
1039
  res.writeHead(200, { 'Content-Type': 'application/json' });
923
1040
  res.end(JSON.stringify(result.data));
@@ -1004,7 +1121,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1004
1121
  measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
1005
1122
  measure.win = PROVIDERS[key]?.context_window || 0;
1006
1123
  commit(200);
1007
- cache.set(effectiveModel, body.messages, body.temperature, result.data);
1124
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
1008
1125
  recordTokens(key, result.usage);
1009
1126
  res.writeHead(200, { 'Content-Type': 'application/json' });
1010
1127
  res.end(JSON.stringify(result.data));
@@ -1062,7 +1179,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1062
1179
  model: provider.model,
1063
1180
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
1064
1181
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1065
- });
1182
+ }, key);
1066
1183
  }
1067
1184
  commit(200);
1068
1185
  res.end();
@@ -1291,6 +1408,7 @@ const server = http.createServer(async (req, res) => {
1291
1408
  return [k, { status: v.status, score: v.score, latency_ms: v.latency, reason, reliability }];
1292
1409
  })),
1293
1410
  cache: cache.stats(),
1411
+ hourly: (() => { try { return getHourly(); } catch { return []; } })(),
1294
1412
  limits,
1295
1413
  pool: poolStats(),
1296
1414
  last_selection: getLastSelection(),