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/.env.example CHANGED
@@ -1,7 +1,19 @@
1
- # Copy to .env and fill in your keys. Only keys marked free:true are required.
1
+ # Copy to .env and fill in your keys. Only free:true providers are required.
2
+ # Бесплатные провайдеры free-model (несколько ключей = больше пул + надёжность):
2
3
  PROVIDER_GROQ_APIKEY=
3
4
  PROVIDER_MISTRAL_APIKEY=
4
5
  PROVIDER_GEMINI_APIKEY=
5
6
  PROVIDER_NIM_APIKEY=
6
7
  PROVIDER_ZAI_APIKEY=
7
8
  PROVIDER_OPENROUTER_APIKEY=
9
+ PROVIDER_CEREBRAS_APIKEY=
10
+ PROVIDER_DEEPSEEK_APIKEY=
11
+ PROVIDER_HF_APIKEY=
12
+ # Розширений пул (бесплатные free-тиры, добавить по желанию):
13
+ PROVIDER_SAMBANOVA_APIKEY=
14
+ PROVIDER_SILICONFLOW_APIKEY=
15
+ PROVIDER_DEEPINFRA_APIKEY=
16
+ PROVIDER_HYPERBOLIC_APIKEY=
17
+ PROVIDER_COHERE_APIKEY=
18
+ PROVIDER_LLM7_APIKEY=
19
+ PROVIDER_NARA_APIKEY=
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  ## Why pay for LLMs when free ones exist?
15
15
 
16
16
  Your AI agent, bot, or script talks to a **single OpenAI-compatible endpoint**.
17
- Behind it, Freegate automatically routes requests across **34 free models**
17
+ Behind it, Freegate automatically routes requests across **50+ free models**
18
18
  from Groq, Mistral, Gemini, NVIDIA NIM, OpenRouter, ZAI, Cerebras, DeepSeek
19
19
  and local models. If one provider goes down, gets overloaded, or burns its
20
20
  daily limit — the request **instantly falls through to the next one**. You
@@ -29,26 +29,31 @@ is only yours — nobody else can connect to it, and you can't to theirs. Provid
29
29
  keys live in your local `.env`, history in local files. **No third-party server
30
30
  sees your keys or conversations.**
31
31
 
32
- ![Freegate Dashboard](assets/dashboard.png)
32
+ ![Freegate Dashboard](assets/dashboard-en.gif)
33
33
 
34
34
  ## Features
35
35
 
36
36
  | | |
37
37
  |---|---|
38
- | 🔀 **Auto-failover** | 34 providers in one chain. Provider down? The next one answers. |
38
+ | 🔀 **Auto-failover** | 50+ providers in one chain. Provider down? The next one answers. |
39
39
  | 🤖 **Self-managing models** | Auto-discovers new free models, tests them, adds working ones, disables dead ones — built-in scheduler, always-fresh model base. |
40
40
  | 🗂️ **Model database** | Structured passport per model (score, latency, context window, history) + sorting: best models get routing priority. |
41
41
  | 🏷️ **Model categories** | reasoning / coding / general / vision / local — the right model for the right job. |
42
42
  | 🖼️ **Two-stage vision** | Screenshot → vision model reads it → coding model answers the fix. |
43
43
  | 💰 **Free** | Free models only. The dashboard shows each provider's remaining limit. |
44
44
  | ⚡ **Smart routing** | Picks the fastest, most stable provider for every request. |
45
+ | 🎯 **Routing strategies** | `weighted` (default), `weighted-roundrobin`, `weighted-least` — burn limits more evenly. |
45
46
  | 🛡️ **Reliability** | Circuit breaker, request queue, auto-disable of dead providers, watchdog. |
46
- | 📊 **Dashboard** | Status, speed, limits, history, tokens, RPM chart, savings ($). RU/EN. |
47
+ | 📊 **Dashboard** | Status, speed, limits, history, tokens, RPM chart, savings ($). RU/EN. **4 themes** (PolyCopy/Warm/Cosmic/Paper) — switcher in the header. |
47
48
  | 💾 **Disk cache** | Repeat prompts don't consume limits at all. |
48
- | 🎓 **Methodologist** | Agent answers like an engineer: plan→test→code (coding), stepwise (reasoning). Prompts customizable in `config.json`. |
49
+ | 🎓 **Methodologist** | Agent answers like an engineer: plan→test→code (coding), stepwise (reasoning), like a frontend designer (design). Prompts customizable in `config.json`. |
50
+ | 🌐 **Web search** | For question/answer it fetches current facts from the web (DuckDuckGo, keyless) — answers accurately instead of hallucinating. |
51
+ | 🧪 **Self-check** | Optional: complex answers are vetted by a second model (vetting) and flagged on error. Enable: `config.vetting.enabled`. |
52
+ | ✂️ **Prompt compression** | Optional: strips politeness/fillers (Caveman-style) to save tokens. `config.compress.enabled`. |
53
+ | 📈 **Sparkline 24h** | Hourly success in the dashboard + history filters (search by model/provider, OK/Errors). |
49
54
  | 🔌 **Compatible** | Any OpenAI client: opencode, Cursor, your scripts. |
50
55
 
51
- ![Freegate Models base](assets/dashboard-models.png)
56
+ ![Freegate Models base](assets/dashboard-models-en.png)
52
57
 
53
58
  ## Quick start — 30 seconds
54
59
 
@@ -134,15 +139,21 @@ to watch for new releases. The model manager runs every 6 hours.
134
139
 
135
140
  ### Methodologist (Productive Agent Layer)
136
141
 
137
- Freegate classifies the task (coding / reasoning / search / chat) and injects a
142
+ Freegate classifies the task (coding / design / reasoning / search / chat) and injects a
138
143
  short system-prompt methodologist **without any client-side changes**. Any
139
144
  OpenAI-compatible client (opencode, Cursor, chat) gets engineer-grade answers:
140
145
 
141
146
  - **coding** — brief plan before code, a suggested test, where to verify.
147
+ - **design** — bold aesthetic idea, distinctive fonts, cohesive palette via CSS variables, choreographed motion, responsive. Inspired by Anthropic `frontend-design`.
142
148
  - **reasoning** — reason stepwise, show assumptions.
143
149
  - **search** — short factual answer, don't invent sources.
144
150
  - **chat** — to the point, concise.
145
151
 
152
+ A shared **"don't give up"** block is prepended to every category: if the model
153
+ can't find a tool/file/skill where it expected one, it doesn't refuse — it looks
154
+ for a workaround (reads files directly, checks node_modules, tries alternatives).
155
+ This makes free models act resourcefully instead of ending with "I can't".
156
+
146
157
  The task category also nudges routing toward matching provider categories
147
158
  (`coding`→coding models, `reasoning`→reasoning models) without dropping fallback.
148
159
  Category distribution is visible on the dashboard and via
@@ -213,6 +224,7 @@ put it in `config.json` → `providers` (same format as `providers.json`).
213
224
  ```bash
214
225
  npx freegate init # create config
215
226
  npx freegate init -i # interactive wizard (keys, password)
227
+ npx freegate doctor # diagnostics: keys, models, "what to check"
216
228
  npx freegate start # start proxy
217
229
  npx freegate status # diagnostics: providers, limits, errors
218
230
  npx freegate test # verify it works
package/README.ru.md CHANGED
@@ -13,9 +13,10 @@
13
13
  ## Зачем платить за LLM, когда есть бесплатные?
14
14
 
15
15
  Твой AI-агент, бот или скрипт использует **один OpenAI-совместимый endpoint**.
16
- За ним Freegate автоматически распределяет запросы между **34 бесплатными
16
+ За ним Freegate автоматически распределяет запросы между **50+ бесплатными
17
17
  моделями** — Groq, Mistral, Gemini, NVIDIA NIM, OpenRouter, ZAI, Cerebras,
18
- DeepSeek и локальные модели. Если один провайдер упал, перегружен или сжёг
18
+ DeepSeek, SambaNova, SiliconFlow, DeepInfra, Hyperbolic, Cohere, LLM7, Nara
19
+ и локальные модели. Если один провайдер упал, перегружен или сжёг
19
20
  дневной лимит — запрос **мгновенно уходит на следующий**. Ты никогда не
20
21
  видишь «rate limit», и никогда не платишь.
21
22
 
@@ -28,23 +29,28 @@ Freegate крутится **на твоей машине** (`http://localhost:40
28
29
  провайдеров лежат в твоём локальном `.env`, история — в локальных файлах. **Ни
29
30
  один чужой сервер не видит твои ключи и переписки.**
30
31
 
31
- ![Дашборд Freegate](assets/dashboard.png)
32
+ ![Дашборд Freegate](assets/dashboard.gif)
32
33
 
33
34
  ## Возможности
34
35
 
35
36
  | | |
36
37
  |---|---|
37
- | 🔀 **Автопереключение** | 34 провайдера в одной цепочке. Провайдер упал? Следующий уже отвечает. |
38
+ | 🔀 **Автопереключение** | 50+ провайдеров в одной цепочке. Провайдер упал? Следующий уже отвечает. |
38
39
  | 🤖 **Автоуправление моделями** | Сам находит новые бесплатные модели, тестирует, добавляет рабочие, отключает мёртвые — встроенный планировщик, всегда актуальная база. |
39
40
  | 🗂️ **База моделей** | Структурированный паспорт каждой модели (скор, латентность, окно, история) + сортировка: лучшие модели получают приоритет в роутинге. |
40
41
  | 🏷️ **Категории моделей** | reasoning / coding / general / vision / local — правильная модель для каждой задачи. |
41
42
  | 🖼️ **Vision-конвейер** | Скриншот → vision-модель читает → кодинг-модель отвечает на вопрос. |
42
43
  | 💰 **Бесплатно** | Только free-модели. Дашборд показывает остаток лимита каждого провайдера. |
43
44
  | ⚡ **Умный выбор** | Прокси сам находит самый быстрый и стабильный провайдер для каждого запроса. |
45
+ | 🎯 **Стратегии роутинга** | `weighted` (по умолчанию), `weighted-roundrobin`, `weighted-least` — ровнее расходуют лимиты. |
44
46
  | 🛡️ **Надёжность** | Circuit breaker, очередь запросов, автоотключение мёртвых провайдеров, watchdog. |
45
- | 📊 **Дашборд** | Статус, скорость, лимиты, история, токены, RPM-график, экономия ($). RU/EN. |
47
+ | 📊 **Дашборд** | Статус, скорость, лимиты, история, токены, RPM-график, экономия ($). RU/EN. **4 темы** (PolyCopy/Тёплый/Космос/Бумага) — переключатель в шапке. |
46
48
  | 💾 **Кэш на диске** | Повторные промпты не тратят лимиты вообще. |
47
- | 🎓 **Методолог** | Агент отвечает как инженер: план→тест→код (coding), пошагово (reasoning). Промпты настраиваются в `config.json`. |
49
+ | 🎓 **Методолог** | Агент отвечает как инженер: план→тест→код (coding), пошагово (reasoning), **как фронтенд-дизайнер** (design). Промпты настраиваются в `config.json`. |
50
+ | 🌐 **Веб-поиск** | Для вопросов-поиска Freegate находит актуальные факты в интернете (DuckDuckGo, без ключа) — отвечает по существу, а не галлюцинирует. |
51
+ | 🧪 **Самопроверка** | Опционально: сложные ответы проверяет второй моделью (vetting) и помечает ошибки. Вкл: `config.vetting.enabled`. |
52
+ | ✂️ **Сжатие промптов** | Опционально: убирает вежливость/заполнители (Caveman-стиль), экономя токены. `config.compress.enabled`. |
53
+ | 📈 **Sparkline 24ч** | Успех по часам в дашборде + фильтры истории (поиск по модели/провайдеру, OK/Ошибки). |
48
54
  | 🔌 **Совместимость** | Любой OpenAI-клиент: opencode, Cursor, ChatGPT-аналоги, твои скрипты. |
49
55
 
50
56
  ![База моделей Freegate](assets/dashboard-models.png)
@@ -63,7 +69,7 @@ npx freegate test # проверить, что всё работает
63
69
 
64
70
  **Подключить к Cursor в 2 клика:** Cursor → Settings → Models → «OpenAI-compatible» → Base URL `http://localhost:4000/v1`, API Key = твой пароль.
65
71
 
66
- Дашборд: `http://localhost:4000/?key=твой_пароль`
72
+ Дашборд: `http://localhost:4000/?key=твой_пароль`. Темы: `?theme=cosmic` или кнопка «Тема» в шапке; `npx freegate themes` — список.
67
73
 
68
74
  Или через Docker:
69
75
 
@@ -139,10 +145,17 @@ Freegate определяет тип задачи (кодинг / рассужд
139
145
  ответы как от опытного инженера:
140
146
 
141
147
  - **coding** — краткий план перед кодом, предложенный тест, где проверять.
148
+ - **design** — смелая эстетическая идея, характерные шрифты, единая палитра через CSS-переменные, срежиссированные анимации, адаптив. По мотивам Anthropic `frontend-design`.
142
149
  - **reasoning** — рассуждать пошагово, показывать допущения.
143
150
  - **search** — короткий фактологичный ответ, не выдумывать источник.
144
151
  - **chat** — по существу и кратко.
145
152
 
153
+ Ко всем категориям подмешивается общий блок **«не сдавайся»**: если модель не
154
+ находит нужный инструмент/файл/скилл там, где ожидал — не отказывается, а ищет
155
+ обходной путь (читает файлы напрямую, проверяет node_modules, пробует
156
+ альтернативы). Вследствие этого free-модели ведут себя находчивее, а не
157
+ заканчивают ответ «не могу».
158
+
146
159
  Дополнительно: категория задачи даёт буст моделям подходящей категории
147
160
  (`coding`→coding-модели, `reasoning`→reasoning-модели), не исключая fallback.
148
161
  Распределение категорий видно в дашборде и через
@@ -233,6 +246,7 @@ PROVIDER_ZAI_APIKEY=... # ZAI
233
246
  ```bash
234
247
  npx freegate init # создать конфиг
235
248
  npx freegate init -i # интерактивный мастер (ключи, пароль)
249
+ npx freegate doctor # диагностика: ключи, модели, «что проверить»
236
250
  npx freegate start # запустить прокси
237
251
  npx freegate status # диагностика: провайдеры, лимиты, ошибки
238
252
  npx freegate test # проверить, что работает
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/bin/freegate.js CHANGED
@@ -125,6 +125,78 @@ if (cmd === 'init') {
125
125
  console.log('\nГотово! Заполни .env ключами, затем: npx freegate start');
126
126
  console.log('Совет: npx freegate init -i — интерактивный мастер с вопросами.');
127
127
  }
128
+ } else if (cmd === 'doctor') {
129
+ const doctor = require(path.join(ROOT, 'lib', 'doctor'));
130
+ const setup = require(path.join(ROOT, 'lib', 'setup'));
131
+
132
+ console.log('\n🩺 Freegate doctor');
133
+ console.log('────────────────────');
134
+
135
+ const snap = doctor.snapshot();
136
+
137
+ // 1. Ключи — что задано.
138
+ const keyGroups = Object.values(snap.keyState);
139
+ const withKey = keyGroups.filter((k) => k.hasKey);
140
+ const noKey = keyGroups.filter((k) => !k.hasKey);
141
+ console.log(`Провайдеров в каталоге: ${snap.catalogCount}, групп провайдеров: ${keyGroups.length}`);
142
+ console.log(`Ключей задано: ${withKey.length}/${keyGroups.length}`);
143
+ if (withKey.length) {
144
+ console.log('\n✅ Ключи установлены:');
145
+ for (const k of withKey) {
146
+ const masked = k.key.slice(0, 4) + '…' + (k.key.length > 8 ? k.key.slice(-4) : '');
147
+ console.log(` ${k.name.padEnd(18)} (${k.count.toString().padStart(2)} мод.) ${masked}`);
148
+ }
149
+ }
150
+ if (noKey.length) {
151
+ console.log('\n⬜ Ключи не заданы (модели недоступны):');
152
+ for (const k of noKey) console.log(` ${k.name.padEnd(18)} (${k.count.toString().padStart(2)} мод.)`);
153
+ }
154
+
155
+ // 2. Модели — живое.
156
+ const models = Object.keys(snap.models).length;
157
+ console.log(`\nМоделей в базе: ${models}`);
158
+ console.log(` активных: ${snap.byStatus.active} · отключено: ${snap.byStatus.disabled} · неизвестно: ${snap.byStatus.unknown}`);
159
+ if (snap.byStatus.active === 0 && models === 0) {
160
+ console.log(' ⚠️ База пуста — запусти сервер, чтобы автопоиск нашёл модели.');
161
+ }
162
+
163
+ // 3. Рекомендации «что включить».
164
+ const rec = doctor.recommend(snap.models);
165
+ if (rec.disabledByUser.length || rec.paid.length || rec.local.length || rec.dead.length) {
166
+ console.log('\n🔮 Что проверить:');
167
+ if (rec.disabledByUser.length) console.log(' ✅ Выключенные free (можно вернуть): ' + rec.disabledByUser.join(', '));
168
+ if (rec.paid.length) console.log(' 🟠 Платные/малый лимит (держать выключенными): ' + rec.paid.join(', '));
169
+ if (rec.local.length) console.log(' 🏠 Локальные (слабый Mac): ' + rec.local.join(', '));
170
+ if (rec.dead.length) console.log(' ⚠️ Мёртвые/непроверенные (чистить): ' + rec.dead.join(', '));
171
+ }
172
+
173
+ // 4. Быстрый старт.
174
+ const easy = doctor.easyStartKey();
175
+ const easyHas = snap.keyState[easy.envVar] && snap.keyState[easy.envVar].hasKey;
176
+ if (!easyHas) {
177
+ console.log(`\n💡 Быстрый старт: добавь ключ ${easy.name} — сразу ${easy.count}+ бесплатных моделей.`);
178
+ console.log(' `npx freegate init -i` или дашборд → Настройки.');
179
+ } else {
180
+ console.log(`\n💡 Всё настроено. Запуск: npx freegate start · дашборд: http://localhost:4000`);
181
+ }
182
+
183
+ // Опциональная валидация ключей (медленная — по флагу).
184
+ if (process.argv.includes('--validate')) {
185
+ console.log('\n⏳ Проверяю ключи (может занять ~10с)...');
186
+ let n = 0;
187
+ (async () => {
188
+ for (const k of keyGroups) {
189
+ if (!k.hasKey) continue;
190
+ const r = await setup.validateKey(k.envVar, k.key);
191
+ n++;
192
+ if (r.valid) console.log(` ✅ ${k.name}`);
193
+ else console.log(` ❌ ${k.name}: ${r.error || 'неверный'}`);
194
+ }
195
+ process.exit(0);
196
+ })();
197
+ } else if (cmd === 'doctor') {
198
+ // (validate-ветка уже вышла через process.exit выше — здесь просто тишина)
199
+ }
128
200
  } else if (cmd === 'start') {
129
201
  // Spawn from the user's cwd (not package dir) so server.js picks up their
130
202
  // config.json / .env created by `init`.
@@ -185,6 +257,56 @@ WantedBy=default.target
185
257
  }
186
258
  } else if (cmd === 'dashboard') {
187
259
  console.log('Открой http://localhost:4000/ (запусти start сначала)');
260
+ } else if (cmd === 'themes') {
261
+ console.log('🎨 Темы дашборда Freegate');
262
+ console.log('-----------------------------');
263
+ const themes = [
264
+ ['poly', 'PolyCopy — тёмный, фиолетовый акцент (по умолчанию)'],
265
+ ['warm', 'Тёплый — янтарный, уютный ламповый'],
266
+ ['cosmic', 'Космос — глубокий синий, мягкий голубой'],
267
+ ['paper', 'Бумага — светлый, инженерный чертёж'],
268
+ ];
269
+ for (const [id, desc] of themes) console.log(' ' + id.padEnd(8) + desc);
270
+ console.log('-----------------------------');
271
+ console.log('Как включить:');
272
+ console.log(' ?theme=cosmic — открыть дашборд в теме вручную');
273
+ console.log(' Кнопка «Тема» в шапке дашборда — переключать по кругу');
274
+ console.log(' Памятка: `--theme` не нужен, тема хранится в localStorage браузера.');
275
+ } else if (cmd === 'connect') {
276
+ const onboarding = require(path.join(ROOT, 'lib', 'onboarding'));
277
+ const setup = require(path.join(ROOT, 'lib', 'setup'));
278
+ const { keys } = setup.readKeys();
279
+ let AUTH = keys.AUTH || process.env.AUTH || '';
280
+ if (!AUTH) {
281
+ try { AUTH = (JSON.parse(fs.readFileSync(path.join(process.cwd(), 'config.json'), 'utf8')).auth) || ''; } catch {}
282
+ }
283
+ const port = process.env.PORT || '4000';
284
+ const baseUrl = `http://localhost:${port}`;
285
+ const apiKey = AUTH || 'твой_пароль';
286
+
287
+ console.log('\n🔌 Подключить Freegate к клиенту');
288
+ console.log('────────────────────────────────');
289
+ console.log(`Base URL: ${baseUrl}/v1`);
290
+
291
+ // Определяем, какой профиль выбрать: смотрим, какие модели уже активны.
292
+ const doctor = require(path.join(ROOT, 'lib', 'doctor'));
293
+ const snap = doctor.snapshot();
294
+ const rec = doctor.recommend(snap.models);
295
+ const activeCount = snap.byStatus.active || 0;
296
+
297
+ console.log('\nВыбери профиль (подсказка по активным моделям):');
298
+ for (const p of onboarding.profiles()) {
299
+ console.log(` ${p.id.padEnd(11)} ${p.name} — ${p.hint}`);
300
+ }
301
+ console.log('\nПрофиль не влияет на прокси — это подсказка, какую модель выбрать в клиенте.');
302
+
303
+ console.log('\n─── Cursor ───');
304
+ console.log(onboarding.snippetCursor(baseUrl, apiKey));
305
+ console.log('\n─── opencode ───');
306
+ console.log(onboarding.snippetOpencode(baseUrl, apiKey));
307
+ console.log('\n─── Cline (VS Code) ───');
308
+ console.log(onboarding.snippetCline(baseUrl, apiKey));
309
+ console.log('\nСовет: модели с категорией coding → coder, design → designer и т.д.');
188
310
  } else if (cmd === 'test') {
189
311
  const http = require('http');
190
312
  const base = `http://127.0.0.1:${process.env.PORT || 4000}`;
@@ -16,6 +16,25 @@
16
16
  "reasoning": "Рассуждай пошагово, показывай допущения, затем вывод."
17
17
  }
18
18
  },
19
+ "websearch": {
20
+ "enabled": true,
21
+ "limit": 5,
22
+ "timeout": 8000,
23
+ "queryMinChars": 6
24
+ },
25
+ "vetting": {
26
+ "enabled": false,
27
+ "minAnswerLen": 120,
28
+ "maxChecksPerMin": 4,
29
+ "complexityOnly": true
30
+ },
31
+ "routing": {
32
+ "strategy": "weighted"
33
+ },
34
+ "compress": {
35
+ "enabled": false,
36
+ "minLen": 60
37
+ },
19
38
  "providers": {
20
39
  "or-nemotron-550b": {
21
40
  "endpoint": "https://openrouter.ai/api/v1/chat/completions",
@@ -25,6 +44,26 @@
25
44
  "keyHint": "openrouter.ai → Keys",
26
45
  "envVar": "PROVIDER_OPENROUTER_APIKEY",
27
46
  "free": true
47
+ },
48
+ "sambanova-gpt-oss-120b": {
49
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
50
+ "model": "gpt-oss-120b",
51
+ "priority": 12,
52
+ "dailyLimit": 100000,
53
+ "keyHint": "cloud.sambanova.ai → API Keys",
54
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
55
+ "free": true,
56
+ "category": "general"
57
+ },
58
+ "siliconflow-deepseek-r1": {
59
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
60
+ "model": "deepseek-ai/DeepSeek-R1",
61
+ "priority": 12,
62
+ "dailyLimit": 100000,
63
+ "keyHint": "cloud.siliconflow.com → API Keys",
64
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
65
+ "free": true,
66
+ "category": "reasoning"
28
67
  }
29
68
  }
30
69
  }
package/lib/cache.js CHANGED
@@ -45,7 +45,7 @@ class LRUCache {
45
45
  const now = Date.now();
46
46
  for (const e of data.entries) {
47
47
  if (now - e.created > this.ttl) continue;
48
- this.cache.set(e.key, { value: e.value, created: e.created, uses: e.uses || 0, model: e.model, temperature: e.temperature, grams: e.grams || null });
48
+ this.cache.set(e.key, { value: e.value, created: e.created, uses: e.uses || 0, model: e.model, temperature: e.temperature, grams: e.grams || null, providerKey: e.providerKey || null });
49
49
  }
50
50
  // Restore hit/miss counters so hitRate survives restarts
51
51
  if (typeof data.hits === 'number') this.hits = data.hits;
@@ -63,7 +63,7 @@ if (now - e.created > this.ttl) continue;
63
63
  try {
64
64
  const size = Buffer.byteLength(JSON.stringify(entry.value));
65
65
  if (size > MAX_ENTRY_BYTES) continue;
66
- entries.push({ key, value: entry.value, created: entry.created, uses: entry.uses || 0, model: entry.model, temperature: entry.temperature, grams: entry.grams || null });
66
+ entries.push({ key, value: entry.value, created: entry.created, uses: entry.uses || 0, model: entry.model, temperature: entry.temperature, grams: entry.grams || null, providerKey: entry.providerKey || null });
67
67
  } catch {}
68
68
  }
69
69
  // Keep only the most recent 300 on disk to bound file size
@@ -141,7 +141,7 @@ if (now - e.created > this.ttl) continue;
141
141
  return { value: entry.value, similarity: Math.round(bestSim * 1000) / 1000 };
142
142
  }
143
143
 
144
- set(model, messages, temperature, value) {
144
+ set(model, messages, temperature, value, providerKey) {
145
145
  const key = this._key(model, messages, temperature);
146
146
 
147
147
  // Delete if exists (to update order)
@@ -167,10 +167,24 @@ if (now - e.created > this.ttl) continue;
167
167
  grams = new Set(trigrams(norm));
168
168
  }
169
169
 
170
- this.cache.set(key, { value, created: Date.now(), uses: 0, model, temperature, grams: grams ? [...grams] : null });
170
+ this.cache.set(key, { value, created: Date.now(), uses: 0, model, temperature, grams: grams ? [...grams] : null, providerKey: providerKey || null });
171
+ }
172
+
173
+ // Вернуть провайдер-источник кэш-записи (для диагностики: кэш-хит может
174
+ // скрывать реальный источник ответа). null → записи нет/без провайдера.
175
+ getProvider(model, messages, temperature) {
176
+ const key = this._key(model, messages, temperature);
177
+ const entry = this.cache.get(key);
178
+ return entry ? (entry.providerKey || null) : null;
171
179
  }
172
180
 
173
181
  stats() {
182
+ // Распределение кэш-записей по провайдерам-источникам (для дашборда).
183
+ const byProvider = {};
184
+ for (const entry of this.cache.values()) {
185
+ const pk = entry.providerKey || 'unknown';
186
+ byProvider[pk] = (byProvider[pk] || 0) + 1;
187
+ }
174
188
  return {
175
189
  hits: this.hits,
176
190
  misses: this.misses,
@@ -180,6 +194,7 @@ if (now - e.created > this.ttl) continue;
180
194
  hitRate: this.hits + this.misses > 0
181
195
  ? Math.round((this.hits / (this.hits + this.misses)) * 100)
182
196
  : 0,
197
+ byProvider,
183
198
  };
184
199
  }
185
200
 
@@ -0,0 +1,85 @@
1
+ // lib/compress.js — лёгкое сжатие промпта (по мотивам OmniRoute Caveman).
2
+ // Удаляет вежливость/хеджирование/заполнители из ПОСЛЕДНЕГО user-сообщения,
3
+ // экономя токены free-лимитов. Только для plain-text сообщений; код/контент-
4
+ // массивы/инструменты не трогаем. Опционально: config.compress.enabled.
5
+ const logger = require('./logger');
6
+
7
+ const COMPRESS_DEFAULTS = { enabled: false, minLen: 60 };
8
+
9
+ // Пары «регэксп → замена». Намеренно бережно: не ломаем смысл, только пустые
10
+ // фразы. Артикли EN убираем только когда после них буква (не ломать код/URL).
11
+ const RULES = [
12
+ // English pleasantries / polite framing / hedging / fillers
13
+ [/^(?:sure|certainly|of course|happy to|absolutely)\s*[,!.]?\s+/i, ''],
14
+ [/^(?:thanks|thank you|thanks in advance|i really appreciate)\s*[,!.]?\s+/i, ''],
15
+ [/^(?:please|kindly|could you please|would you please|can you please)\s+/i, ''],
16
+ [/^(?:i want you to|i need you to|i'd like you to)\s+/i, ''],
17
+ [/^(?:i am trying to|i am working on|i have been)\s+/i, ''],
18
+ [/\b(?:it seems like|it appears that|i think that|i believe that|probably|possibly|maybe it)\s+/gi, ''],
19
+ [/\b(?:basically|essentially|actually|literally|simply|currently|just)\s+/gi, ''],
20
+ [/\b(?:i want to|i need to|i'd like to|i'm looking for)\s+/gi, ''],
21
+ [/^(?:hi there|hello|hey|good morning|good afternoon)\s*[,!.]?\s+/i, ''],
22
+ [/\b(?:a bit|a little|somewhat|kind of|sort of)\s+/gi, ''],
23
+ // English hedgy / filler requests
24
+ [/\b(?:i was wondering|would it be possible|if possible|when you get a chance|at your convenience)\s*,?\s+/gi, ''],
25
+ // English articles (only plain prose, not URLs/code)
26
+ [/\b(?:an|a|the)\s+(?=[a-z])/gi, ''],
27
+ // Russian fillers and polite framing
28
+ [/^(?:пожалуйста|будьте добры|если можно|не могли бы вы)\s*[,!.]?\s+/i, ''],
29
+ [/^(?:привет|здравствуйте|добрый день|добрый вечер)\s*[,!.]?\s+/i, ''],
30
+ [/\b(?:просто|кстати|вообще|скажем|по сути|по-хорошему|честно говоря)\s+/gi, ''],
31
+ [/\b(?:как бы|вроде бы|в принципе|наверное|возможно|пожалуй)\s+/gi, ''],
32
+ [/^(?:я хочу|мне нужно|я бы хотел|я пытаюсь|я работаю)\s+/i, ''],
33
+ [/\b(?:спасибо|заранее спасибо|очень благодарен)\s*[,!.]?\s+/gi, ''],
34
+ ];
35
+
36
+ // Не сжимаем, если сообщение похоже на код.
37
+ function looksLikeCode(text) {
38
+ return /[{}\[\]]|=>|\bdef\b|\bfunction\b|\bconst\b|\bimport\b|\bfrom\b\s+["']|<\/?[a-z][^>]*>/i.test(text);
39
+ }
40
+
41
+ // Убрать «шапку» polite-prose из текста.
42
+ function compressText(text) {
43
+ if (!text || typeof text !== 'string') return text;
44
+ let out = text;
45
+ for (const [re, rep] of RULES) {
46
+ out = out.replace(re, rep);
47
+ }
48
+ // подчистить возможные двойные пробелы после сокращений
49
+ out = out.replace(/[ \t]{2,}/g, ' ').trim();
50
+ return out;
51
+ }
52
+
53
+ function shouldCompress(cfg, text) {
54
+ if (!cfg || cfg.enabled === false) return false;
55
+ if (!text || typeof text !== 'string') return false;
56
+ if (text.trim().length < (cfg.minLen || COMPRESS_DEFAULTS.minLen)) return false;
57
+ if (looksLikeCode(text)) return false;
58
+ return true;
59
+ }
60
+
61
+ // Сжать последний user-message (только plain string content).
62
+ function compressMessages(messages, options = {}) {
63
+ const cfg = Object.assign({}, COMPRESS_DEFAULTS, options);
64
+ if (!messages || !Array.isArray(messages) || messages.length === 0) return messages;
65
+ if (cfg.enabled === false) return messages;
66
+
67
+ let mutated = false;
68
+ const copy = messages.slice();
69
+ for (let i = copy.length - 1; i >= 0; i--) {
70
+ const m = copy[i];
71
+ if (!m || m.role !== 'user') continue;
72
+ if (typeof m.content !== 'string') continue; // не трогаем массивы/инструменты
73
+ if (!shouldCompress(cfg, m.content)) continue;
74
+ const compressed = compressText(m.content);
75
+ if (compressed !== m.content && compressed.length > 0) {
76
+ copy[i] = { ...m, content: compressed };
77
+ mutated = true;
78
+ break; // только последнее user-сообщение
79
+ }
80
+ }
81
+ if (mutated) logger.info('prompt compressed', { freed: 0 });
82
+ return mutated ? copy : messages;
83
+ }
84
+
85
+ module.exports = { compressText, compressMessages, shouldCompress, looksLikeCode, RULES, COMPRESS_DEFAULTS };
@@ -40,7 +40,7 @@ function emptyBucket(hour, provider) {
40
40
  status429: 0,
41
41
  statusOther: 0,
42
42
  sysShareSum: 0,
43
- tasks: { coding: 0, reasoning: 0, search: 0, chat: 0 },
43
+ tasks: { coding: 0, reasoning: 0, search: 0, chat: 0, design: 0 },
44
44
  };
45
45
  }
46
46
 
@@ -163,7 +163,7 @@ class ContextStats {
163
163
  let sysShareRequests = 0;
164
164
  const providers = {};
165
165
  let taskTotal = 0;
166
- const tasks = { coding: 0, reasoning: 0, search: 0, chat: 0 };
166
+ const tasks = { coding: 0, reasoning: 0, search: 0, chat: 0, design: 0 };
167
167
 
168
168
  for (const b of this.buckets.values()) {
169
169
  if (b.ts < cutoff) continue;