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.
- package/config.example.json +4 -0
- package/lib/cache.js +8 -5
- package/lib/clean.js +5 -0
- package/lib/compactor.js +18 -8
- package/lib/dashboard.js +1 -1
- package/lib/diagmonitor.js +92 -0
- package/lib/providers.js +1 -1
- package/package.json +4 -2
- package/providers.json +5 -5
- package/server.js +32 -10
package/config.example.json
CHANGED
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 =
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
//
|
|
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-
|
|
27
|
-
'or-lfm', '
|
|
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
|
-
},
|
|
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>(
|
|
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-
|
|
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.
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
-
//
|
|
561
|
-
|
|
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 =
|
|
622
|
+
let windowPool = healthy2;
|
|
607
623
|
let upgradeNoCapable = false; // апгрейд, но ни один здоровый провайдер не держит запрос
|
|
608
624
|
if (requestTokens > MIN_WINDOW || windowUpgraded) {
|
|
609
|
-
const capable =
|
|
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 {}
|