freegate 0.6.22 → 0.6.24

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.
@@ -40,6 +40,10 @@
40
40
  "enabled": false,
41
41
  "intervalHours": 12
42
42
  },
43
+ "diagMonitor": {
44
+ "enabled": true,
45
+ "intervalMs": 1800000
46
+ },
43
47
  "providers": {
44
48
  "or-nemotron-550b": {
45
49
  "endpoint": "https://openrouter.ai/api/v1/chat/completions",
package/lib/cache.js CHANGED
@@ -22,7 +22,7 @@ class LRUCache {
22
22
  if (!skipLoad) this.load();
23
23
  }
24
24
 
25
- _key(model, messages, temperature) {
25
+ _key(model, messages, temperature, tools) {
26
26
  let raw;
27
27
  if (this.useNormalize) {
28
28
  const norm = normalizeMessages(messages);
@@ -34,6 +34,9 @@ class LRUCache {
34
34
  } else {
35
35
  raw = `${model}|${JSON.stringify(messages)}|${temperature || 0}`;
36
36
  }
37
+ // Инструменты (tools/tool_choice) влияют на ответ (tool_calls vs текст) —
38
+ // без этого tool-запрос попадал на текстовый кэш и агент «останавливался».
39
+ if (tools) raw += `|tools=${JSON.stringify(tools)}`;
37
40
  return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
38
41
  }
39
42
 
@@ -77,8 +80,8 @@ if (now - e.created > this.ttl) continue;
77
80
  } catch {}
78
81
  }
79
82
 
80
- get(model, messages, temperature) {
81
- const key = this._key(model, messages, temperature);
83
+ get(model, messages, temperature, tools) {
84
+ const key = this._key(model, messages, temperature, tools);
82
85
  const entry = this.cache.get(key);
83
86
 
84
87
  if (!entry) {
@@ -141,8 +144,8 @@ if (now - e.created > this.ttl) continue;
141
144
  return { value: entry.value, similarity: Math.round(bestSim * 1000) / 1000 };
142
145
  }
143
146
 
144
- set(model, messages, temperature, value, providerKey) {
145
- const key = this._key(model, messages, temperature);
147
+ set(model, messages, temperature, value, providerKey, tools) {
148
+ const key = this._key(model, messages, temperature, tools);
146
149
 
147
150
  // Delete if exists (to update order)
148
151
  if (this.cache.has(key)) this.cache.delete(key);
package/lib/clean.js CHANGED
@@ -62,6 +62,7 @@ function hasContent(data) {
62
62
  if (!msg) return false;
63
63
  const content = typeof msg.content === 'string' ? msg.content.trim() : '';
64
64
  const reasoning = typeof msg.reasoning === 'string' ? msg.reasoning.trim() : '';
65
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) return true;
65
66
  return content.length > 0 || reasoning.length > 0;
66
67
  }
67
68
 
@@ -75,6 +76,10 @@ function isTooShort(data, askedText) {
75
76
  const msg = data.choices[0]?.message;
76
77
  const content = typeof msg?.content === 'string' ? msg.content.trim() : '';
77
78
  const reasoning = typeof msg?.reasoning === 'string' ? msg.reasoning.trim() : '';
79
+ // Ответ через tool-вызов (агент вызвал инструмент) НЕ пустой — это осмысленный
80
+ // ход, а не «мусор». Без этой проверки прокси считал tool_calls-ответ пустым,
81
+ // отбрасывал его и агент «останавливался», не получив инструмента.
82
+ if (Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0) return false;
78
83
  const ask = typeof askedText === 'string' ? askedText.trim().length : 0;
79
84
  const threshold = ask > 0 && ask < 40 ? Math.max(1, Math.min(MIN_ANSWER_LEN, Math.ceil(ask / 8))) : MIN_ANSWER_LEN;
80
85
  return (content + reasoning).length < threshold;
package/lib/compactor.js CHANGED
@@ -17,14 +17,24 @@ const KEEP_RECENT_TOKENS = 30000; // tokens — recent messages kept untouched (
17
17
  const MAX_COMPACT_THRESHOLD = 100000; // est tokens — cap for window-aware thresholds (≈200k real,
18
18
  // sits safely inside even 512k/1M windows without compacting too early)
19
19
  const SUMMARY_MAX_TOKENS = 2000; // tokens — summary length cap (detailed enough to not lose the work)
20
- const CHARS_PER_TOKEN = 1.5; // heuristic chars→tokens, DOUBLY conservative: real sessions
21
- // (with tool outputs, files, big system prompts) average ~0.75
22
- // chars/token, not ~4. Using 1.5 keeps us from compacting too late.
23
-
24
- // Reliability-ordered long-context summarizers (OpenRouter + HF).
20
+ const CHARS_PER_TOKEN = 3.0; // heuristic chars→tokens. БОЛЬШЕ делитель = МЕНЬШЕ оценка.
21
+ // Реальная плотность смешанного кода/текста ~4-6 chars на
22
+ // токен (не 1.5-2). chars/3.0 приближает оценку к реальным
23
+ // токенам, поэтому порог 60000 достигается только на реально
24
+ // больших диалогах (10+ компактированных страниц), а не на
25
+ // каждом рабочем чате с агентами.
26
+ // Раньше (1.5) оценка завышалась в ~4-6 раз → компакции шли
27
+ // на 86% запросов. Каждая компакция — отдельный LLM-вызов
28
+ // перед ответом = агент «замирает» / виснет на середине.
29
+ // window-aware routing всё равно исключает малые окна (or-lfm
30
+ // 65k) для больших запросов, так что запас к окнам сохранён.
31
+
32
+ // Reliability-ordered long-context summarizers. Порядок важен: getSummary идёт
33
+ // по списку и ждёт ПЕРВЫЙ отвечающий (до 30с). Медленные/непроверенные в начале
34
+ // = «думает и замирает» (компакция тормозит каждый вызов). Бытрые и надёжные вперёд.
25
35
  const SUMMARIZERS = [
26
- 'or-nemotron-35', 'or-dots-3', 'or-minimax-m2-7-free',
27
- 'or-lfm', 'deepseek',
36
+ 'or-dots-3', 'deepseek', 'or-minimax-m2-7-free',
37
+ 'or-lfm', 'or-nemotron-35',
28
38
  ];
29
39
 
30
40
  // Summary cache: hash of compacted messages → summary string.
@@ -205,7 +215,7 @@ async function summarizeViaChain(messages) {
205
215
  { role: 'user', content: 'Сожми следующий диалог:\n\n' + textToSummarize },
206
216
  ],
207
217
  max_tokens: SUMMARY_MAX_TOKENS,
208
- }, 30000, 1);
218
+ }, 15000, 1); // 15с на summarizer — быстрое переползание, без «замирания»
209
219
  const summary = result.data?.choices?.[0]?.message?.content;
210
220
  if (summary && summary.trim().length >= 10) {
211
221
  return summary.trim();
package/lib/dashboard.js CHANGED
@@ -1112,7 +1112,7 @@ const HTML = `<!DOCTYPE html>
1112
1112
  var st = health[k] || {};
1113
1113
  return '<tr><td class="mono">' + esc(k) + '</td><td>' + errors[k] + '</td><td>' + esc(st.status || '?') + '</td><td>' + esc(st.reason || st.latency_ms || '') + '</td></tr>';
1114
1114
  }).sort(function (a, b) {
1115
- var ma = a.match(/<td>(\d+)<\/td>/), mb = b.match(/<td>(\d+)<\/td>/);
1115
+ var ma = a.match(/<td>(\\d+)<\\/td>/), mb = b.match(/<td>(\\d+)<\\/td>/);
1116
1116
  return (mb ? parseInt(mb[1], 10) : 0) - (ma ? parseInt(ma[1], 10) : 0);
1117
1117
  });
1118
1118
  document.getElementById('diagErrorsTable').innerHTML = rows.length
@@ -0,0 +1,92 @@
1
+ // Снятие периодических срезов метрик для сравнения «до/после».
2
+ // Каждые INTERVAL_MS пишет точку {ts, successRate, requests, errors, compactCount,
3
+ // poolActive, poolTotal, errorsByClass, version} в diag_history.json. Данные
4
+ // переживают перезапуск (пишутся на диск) — можно строить тренд по времени.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { getStats, getHealth, getContextStats } = require('./health');
8
+
9
+ const HISTORY_PATH = process.env.DIAG_HISTORY_PATH || path.join(__dirname, '..', 'diag_history.json');
10
+ const INTERVAL_MS = 30 * 60 * 1000; // 30 мин
11
+ const MAX_POINTS = 2000;
12
+
13
+ function classifyErrors(errors) {
14
+ // Грубая классификация по статусу провайдера в health (без сетевых вызовов):
15
+ // 429 → limit; 401/402/403 → auth; прочее/нет данных → invalid.
16
+ const health = getHealth();
17
+ const out = { limit: 0, auth: 0, invalid: 0, timeout: 0 };
18
+ for (const [key, cnt] of Object.entries(errors || {})) {
19
+ const h = health[key] || {};
20
+ if (h.statusCode === 429) out.limit += cnt;
21
+ else if ([401, 402, 403].includes(h.statusCode)) out.auth += cnt;
22
+ else if (h.statusCode === 0) out.timeout += cnt;
23
+ else out.invalid += cnt;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function snapshot() {
29
+ const stats = getStats() || {};
30
+ const ctx = getContextStats() || {};
31
+ const health = getHealth() || {};
32
+ const errors = stats.errors || {};
33
+ const today = todayStat(stats);
34
+ const active = Object.values(health).filter((h) => h.status === 'up' || h.status === 'ratelimited').length;
35
+ return {
36
+ ts: Date.now(),
37
+ version: process.env.FREEGATE_VERSION || currentVersion(),
38
+ requests: today.requests,
39
+ success: today.success,
40
+ failed: today.failed,
41
+ successRate: today.successRate,
42
+ compactCount: ctx.compactCount || 0,
43
+ upgradedCount: ctx.upgradedCount || 0,
44
+ poolActive: active,
45
+ poolTotal: Object.keys(health).length,
46
+ errorsByClass: classifyErrors(errors),
47
+ };
48
+ }
49
+
50
+ function todayStat(stats) {
51
+ // Дневные счётчики из stats.today (вычисляется сервером) с fallback на общие.
52
+ const day = stats.today || {};
53
+ const total = day.requests || 0;
54
+ return {
55
+ requests: total,
56
+ success: day.success || 0,
57
+ failed: day.failed || 0,
58
+ successRate: total > 0 ? Math.round(((day.success || 0) / total) * 100) : null,
59
+ };
60
+ }
61
+
62
+ function currentVersion() {
63
+ try { return require('../package.json').version; } catch { return 'dev'; }
64
+ }
65
+
66
+ function loadHistory() {
67
+ try { return JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf8')); }
68
+ catch { return []; }
69
+ }
70
+
71
+ function recordSnapshot() {
72
+ const hist = loadHistory();
73
+ hist.push(snapshot());
74
+ while (hist.length > MAX_POINTS) hist.shift();
75
+ try {
76
+ fs.writeFileSync(HISTORY_PATH, JSON.stringify(hist, null, 2));
77
+ } catch {}
78
+ return hist.length;
79
+ }
80
+
81
+ let _timer = null;
82
+ function startMonitor(intervalMs = INTERVAL_MS) {
83
+ stopMonitor();
84
+ recordSnapshot(); // сразу фиксируем стартовую точку
85
+ _timer = setInterval(recordSnapshot, intervalMs);
86
+ }
87
+
88
+ function stopMonitor() {
89
+ if (_timer) { clearInterval(_timer); _timer = null; }
90
+ }
91
+
92
+ module.exports = { snapshot, recordSnapshot, loadHistory, startMonitor, stopMonitor, INTERVAL_MS };
package/lib/providers.js CHANGED
@@ -132,7 +132,7 @@ const MODEL_MAP = {
132
132
  'gemini-3.6-flash': 'gemini-flash',
133
133
  'codestral-latest': 'mistral-codestral', 'mistral-small-latest': 'mistral-small',
134
134
  'mistral-medium-latest': 'mistral-small', 'mistral-large-latest': 'mistral-small',
135
- 'tier-splus': 'or-minimax-m3-free', 'tier-s': 'mistral-codestral',
135
+ 'tier-splus': 'or-dots-3', 'tier-s': 'mistral-codestral',
136
136
  'tier-a': 'mistral-small', 'tier-b': 'groq-qwen',
137
137
  'tier-xl': 'or-dots-3', 'tier-l': 'or-minimax-m3-free',
138
138
  'openrouter-hermes': 'openrouter-hermes',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freegate",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
4
4
  "description": "Free multi-provider LLM gateway with automatic failover. One OpenAI-compatible endpoint routes to many free models (Groq, Mistral, Gemini, NIM, OpenRouter, ZAI, Cerebras, DeepSeek and more). Never pay for LLMs.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,7 +8,9 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node server.js",
11
- "test": "node --test --test-concurrency=1 test/proxy.test.js test/clean.test.js"
11
+ "test": "node --test --test-concurrency=1 test/proxy.test.js test/clean.test.js",
12
+ "set-model-version": "node scripts/set-model-version.js",
13
+ "release": "npm test && npm run set-model-version && npm publish"
12
14
  },
13
15
  "engines": {
14
16
  "node": ">=18"
package/providers.json CHANGED
@@ -646,7 +646,7 @@
646
646
  "gemini-gemma-4-26b-a4b-it": {
647
647
  "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
648
648
  "model": "gemma-4-26b-a4b-it",
649
- "priority": 5,
649
+ "priority": 7,
650
650
  "dailyLimit": 1500,
651
651
  "keyHint": "gemini → Keys (автодобавлено, general)",
652
652
  "envVar": "PROVIDER_GEMINI_APIKEY",
@@ -666,7 +666,7 @@
666
666
  "gemini-gemini-flash-lite-latest": {
667
667
  "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
668
668
  "model": "gemini-flash-lite-latest",
669
- "priority": 2,
669
+ "priority": 3,
670
670
  "dailyLimit": 1500,
671
671
  "keyHint": "gemini → Keys (автодобавлено, general)",
672
672
  "envVar": "PROVIDER_GEMINI_APIKEY",
@@ -676,7 +676,7 @@
676
676
  "gemini-gemini-3-1-flash-lite-preview": {
677
677
  "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
678
678
  "model": "gemini-3.1-flash-lite-preview",
679
- "priority": 6,
679
+ "priority": 2,
680
680
  "dailyLimit": 1500,
681
681
  "keyHint": "gemini → Keys (автодобавлено, general)",
682
682
  "envVar": "PROVIDER_GEMINI_APIKEY",
@@ -686,7 +686,7 @@
686
686
  "gemini-gemini-3-5-flash": {
687
687
  "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
688
688
  "model": "gemini-3.5-flash",
689
- "priority": 3,
689
+ "priority": 6,
690
690
  "dailyLimit": 1500,
691
691
  "keyHint": "gemini → Keys (автодобавлено, general)",
692
692
  "envVar": "PROVIDER_GEMINI_APIKEY",
@@ -706,7 +706,7 @@
706
706
  "gemini-gemini-3-1-flash-lite": {
707
707
  "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
708
708
  "model": "gemini-3.1-flash-lite",
709
- "priority": 7,
709
+ "priority": 5,
710
710
  "dailyLimit": 1500,
711
711
  "keyHint": "gemini → Keys (автодобавлено, general)",
712
712
  "envVar": "PROVIDER_GEMINI_APIKEY",
package/server.js CHANGED
@@ -162,6 +162,8 @@ const MODEL_MANAGER_CONFIG = Object.assign(
162
162
  );
163
163
  const autoUpdate = require('./lib/autoupdate');
164
164
  const AUTO_UPDATE = (config.autoUpdate && typeof config.autoUpdate === 'object') ? config.autoUpdate : {};
165
+ const diagMonitor = require('./lib/diagmonitor');
166
+ const DIAG_MONITOR = (config.diagMonitor && typeof config.diagMonitor === 'object') ? config.diagMonitor : { enabled: true, intervalMs: 30 * 60 * 1000 };
165
167
  const modelManager = new ModelManager({
166
168
  dbPath: path.join(__dirname, 'models-db.json'),
167
169
  catalogPath: path.join(__dirname, 'providers.json'),
@@ -557,8 +559,11 @@ async function handleChatCompletion(req, res, body) {
557
559
  }
558
560
  }
559
561
 
560
- // Check cache (works for both streaming and non-streaming)
561
- const cached = cache.get(effectiveModel, body.messages, body.temperature);
562
+ // Tool-запросы (агент вызывает инструмент) НЕ кэшируем: ответ зависит от tool_calls,
563
+ // а кэш по messages может отдать текстовый ответ вместо инструмента — агент
564
+ // «останавливается», не получив tool. Идём всегда к провайдеру.
565
+ const hasTools = !!(body.tools || body.tool_choice);
566
+ const cached = hasTools ? null : cache.get(effectiveModel, body.messages, body.temperature, body.tools || body.tool_choice);
562
567
  if (cached) {
563
568
  logger.request({ model: requestedModel, provider: 'cache', status: 200, cached: true });
564
569
  recordRecent({ model: requestedModel, provider: 'cache', status: 200, latency: 0, cached: true });
@@ -569,7 +574,7 @@ async function handleChatCompletion(req, res, body) {
569
574
  }
570
575
 
571
576
  // Semantic cache: same intent, rephrased wording → replay without a new LLM call.
572
- if (SEMCACHE_CONFIG.enabled) {
577
+ if (SEMCACHE_CONFIG.enabled && !hasTools) {
573
578
  const semantic = cache.getSemantic(effectiveModel, body.messages, body.temperature, SEMCACHE_CONFIG.minSimilarity);
574
579
  if (semantic) {
575
580
  logger.request({ model: requestedModel, provider: 'semcache', status: 200, cached: true });
@@ -595,6 +600,17 @@ async function handleChatCompletion(req, res, body) {
595
600
  .filter(([_, p]) => p.enabled && !isCircuitOpen(p.key) && p.vision !== true &&
596
601
  (getHealth()[p.key]?.status === 'up' || getHealth()[p.key]?.status === 'ratelimited'));
597
602
 
603
+ // Анти-«замирание»: провайдер, накопивший много ошибок сегодня (status остаётся
604
+ // 'up' — это не 429/401, а флап/долгие таймауты), по-прежнему попадает в цепочку
605
+ // и тормозит ответ. Исключаем его из активного пула до конца дня. Если так пул
606
+ // пустеет (все «ошибочные») — вернём их как крайний резерв, чтобы не отдать 503.
607
+ const todayErrors = getStats().errors || {};
608
+ const ERROR_POOL_THRESHOLD = 15;
609
+ const lowErrorProviders = healthyProviders.filter(
610
+ ([k]) => (todayErrors[k] || 0) < ERROR_POOL_THRESHOLD
611
+ );
612
+ const healthy2 = lowErrorProviders.length > 0 ? lowErrorProviders : healthyProviders;
613
+
598
614
  // Window-aware routing: estimate the request size and only consider providers
599
615
  // whose context window can actually hold it. This stops large requests from
600
616
  // burning time falling through lfm (65k) / groq (131k) providers that reject
@@ -603,10 +619,10 @@ async function handleChatCompletion(req, res, body) {
603
619
  // requests keep the full fast pool.
604
620
  const requestTokens = estimateTokens(body.messages);
605
621
  const MIN_WINDOW = 50000; // below this we don't filter (typical requests)
606
- let windowPool = healthyProviders;
622
+ let windowPool = healthy2;
607
623
  let upgradeNoCapable = false; // апгрейд, но ни один здоровый провайдер не держит запрос
608
624
  if (requestTokens > MIN_WINDOW || windowUpgraded) {
609
- const capable = healthyProviders.filter(([_, p]) => {
625
+ const capable = healthy2.filter(([_, p]) => {
610
626
  const win = p.context_window || 0;
611
627
  // Unknown/0 window providers are kept (heuristic) — better to try than drop.
612
628
  return win === 0 || win >= requestTokens;
@@ -894,7 +910,7 @@ async function handleChatCompletion(req, res, body) {
894
910
  model: provider.model,
895
911
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
896
912
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
897
- }, key);
913
+ }, key, body.tools || body.tool_choice);
898
914
  }
899
915
  commit(200);
900
916
  res.end();
@@ -1004,7 +1020,7 @@ async function handleChatCompletion(req, res, body) {
1004
1020
  model: provider.model,
1005
1021
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
1006
1022
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1007
- }, key);
1023
+ }, key, body.tools || body.tool_choice);
1008
1024
  }
1009
1025
  commit(200);
1010
1026
  res.end();
@@ -1053,7 +1069,7 @@ async function handleChatCompletion(req, res, body) {
1053
1069
  }
1054
1070
  }
1055
1071
  commit(200);
1056
- cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
1072
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key, body.tools || body.tool_choice);
1057
1073
  recordTokens(key, result.usage);
1058
1074
  res.writeHead(200, { 'Content-Type': 'application/json' });
1059
1075
  res.end(JSON.stringify(result.data));
@@ -1150,7 +1166,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1150
1166
  measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
1151
1167
  measure.win = PROVIDERS[key]?.context_window || 0;
1152
1168
  commit(200);
1153
- cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
1169
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key, body.tools || body.tool_choice);
1154
1170
  recordTokens(key, result.usage);
1155
1171
  res.writeHead(200, { 'Content-Type': 'application/json' });
1156
1172
  res.end(JSON.stringify(result.data));
@@ -1208,7 +1224,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1208
1224
  model: provider.model,
1209
1225
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
1210
1226
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1211
- }, key);
1227
+ }, key, body.tools || body.tool_choice);
1212
1228
  }
1213
1229
  commit(200);
1214
1230
  res.end();
@@ -1745,10 +1761,16 @@ server.listen(PORT, process.env.HOST || '127.0.0.1', () => {
1745
1761
  autoUpdate.startAutoUpdate({ root: __dirname, log: (m) => logger.info(m) });
1746
1762
  logger.info('AutoUpdate enabled', { intervalMs: AUTO_UPDATE.intervalMs });
1747
1763
  }
1764
+ // Мониторинг метрик: снапшот каждые 30 мин → можно сравнить «до/после» фиксов.
1765
+ if (DIAG_MONITOR.enabled) {
1766
+ diagMonitor.startMonitor(DIAG_MONITOR.intervalMs);
1767
+ logger.info('DiagMonitor started', { intervalMs: DIAG_MONITOR.intervalMs });
1768
+ }
1748
1769
  });
1749
1770
 
1750
1771
  const _shutdown = () => {
1751
1772
  if (memStore) { memStore.stopTimer(); memStore.save(); }
1773
+ diagMonitor.stopMonitor();
1752
1774
  require('./lib/health').saveState();
1753
1775
  cache.persist();
1754
1776
  try { modelManager.stop(); } catch {}