freegate 0.6.14 → 0.6.16
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/README.md +72 -7
- package/README.ru.md +93 -7
- package/assets/dashboard-models.png +0 -0
- package/assets/dashboard.png +0 -0
- package/bin/freegate.js +32 -5
- package/config.example.json +13 -0
- package/lib/bandit.js +8 -1
- package/lib/cache.js +54 -5
- package/lib/clean.js +7 -2
- package/lib/compactor.js +220 -0
- package/lib/contextstats.js +224 -0
- package/lib/dashboard.js +782 -127
- package/lib/economics.js +43 -0
- package/lib/health.js +56 -9
- package/lib/logger.js +1 -1
- package/lib/memory-store.js +68 -0
- package/lib/memory.js +167 -0
- package/lib/methodology.js +70 -0
- package/lib/modeldb.js +125 -0
- package/lib/modelmanager.js +335 -0
- package/lib/modelscan.js +190 -0
- package/lib/normalize.js +14 -4
- package/lib/providers.js +68 -5
- package/lib/routing.js +14 -2
- package/lib/semcache.js +31 -0
- package/lib/setup.js +309 -0
- package/lib/taskclassify.js +59 -0
- package/package.json +1 -1
- package/providers.json +113 -137
- package/server.js +728 -99
package/README.md
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.npmjs.com/package/freegate)
|
|
7
7
|
[](https://github.com/Artur21101965/freegate)
|
|
8
|
+
[](/#features)
|
|
9
|
+
[](/#why-free)
|
|
10
|
+
[](/#private)
|
|
8
11
|
|
|
9
12
|
**[Русская версия](README.ru.md) · Russian version**
|
|
10
13
|
|
|
11
14
|
## Why pay for LLMs when free ones exist?
|
|
12
15
|
|
|
13
|
-
Your AI agent, bot, or script talks to a single OpenAI-compatible endpoint
|
|
14
|
-
Behind it, Freegate automatically routes requests across **
|
|
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**
|
|
15
18
|
from Groq, Mistral, Gemini, NVIDIA NIM, OpenRouter, ZAI, Cerebras, DeepSeek
|
|
16
19
|
and local models. If one provider goes down, gets overloaded, or burns its
|
|
17
20
|
daily limit — the request **instantly falls through to the next one**. You
|
|
@@ -19,31 +22,48 @@ never see "rate limit", and you never pay.
|
|
|
19
22
|
|
|
20
23
|
**Result:** full LLM access for everyday work at the price of **$0**.
|
|
21
24
|
|
|
25
|
+
### 🔒 Runs locally — your conversations never leave your machine
|
|
26
|
+
|
|
27
|
+
Freegate runs **on your machine** (`http://localhost:4000`). Your `localhost`
|
|
28
|
+
is only yours — nobody else can connect to it, and you can't to theirs. Provider
|
|
29
|
+
keys live in your local `.env`, history in local files. **No third-party server
|
|
30
|
+
sees your keys or conversations.**
|
|
31
|
+
|
|
22
32
|

|
|
23
33
|
|
|
24
34
|
## Features
|
|
25
35
|
|
|
26
36
|
| | |
|
|
27
37
|
|---|---|
|
|
28
|
-
| 🔀 **Auto-failover** |
|
|
29
|
-
| 🤖 **Self-managing models** | Auto-discovers new free models, tests them, adds working ones, disables dead ones —
|
|
38
|
+
| 🔀 **Auto-failover** | 34 providers in one chain. Provider down? The next one answers. |
|
|
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
|
+
| 🗂️ **Model database** | Structured passport per model (score, latency, context window, history) + sorting: best models get routing priority. |
|
|
30
41
|
| 🏷️ **Model categories** | reasoning / coding / general / vision / local — the right model for the right job. |
|
|
31
42
|
| 🖼️ **Two-stage vision** | Screenshot → vision model reads it → coding model answers the fix. |
|
|
32
43
|
| 💰 **Free** | Free models only. The dashboard shows each provider's remaining limit. |
|
|
33
44
|
| ⚡ **Smart routing** | Picks the fastest, most stable provider for every request. |
|
|
34
45
|
| 🛡️ **Reliability** | Circuit breaker, request queue, auto-disable of dead providers, watchdog. |
|
|
35
|
-
| 📊 **Dashboard** | Status, speed, limits, history, tokens, RPM chart. |
|
|
46
|
+
| 📊 **Dashboard** | Status, speed, limits, history, tokens, RPM chart, savings ($). RU/EN. |
|
|
36
47
|
| 💾 **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`. |
|
|
37
49
|
| 🔌 **Compatible** | Any OpenAI client: opencode, Cursor, your scripts. |
|
|
38
50
|
|
|
51
|
+

|
|
52
|
+
|
|
39
53
|
## Quick start — 30 seconds
|
|
40
54
|
|
|
41
55
|
```bash
|
|
42
|
-
npx freegate init -i # wizard:
|
|
56
|
+
npx freegate init -i # wizard: quick (1 OpenRouter key) or full (all keys)
|
|
43
57
|
npx freegate start # proxy on http://localhost:4000
|
|
44
58
|
npx freegate test # verify everything works
|
|
45
59
|
```
|
|
46
60
|
|
|
61
|
+
**init modes:**
|
|
62
|
+
- **quick** — one OpenRouter key → 15+ free models instantly. Add more keys later in Dashboard → Settings.
|
|
63
|
+
- **full** — all provider keys → 8 sources, max speed and reliability (auto-failover).
|
|
64
|
+
|
|
65
|
+
**Connect to Cursor in 2 clicks:** Cursor → Settings → Models → "OpenAI-compatible" → Base URL `http://localhost:4000/v1`, API Key = your password.
|
|
66
|
+
|
|
47
67
|
Dashboard: `http://localhost:4000/?key=your_password`
|
|
48
68
|
|
|
49
69
|
Or via Docker:
|
|
@@ -112,10 +132,55 @@ to watch for new releases. The model manager runs every 6 hours.
|
|
|
112
132
|
3. If it fails — instantly tries the next one in the chain.
|
|
113
133
|
4. The response returns in the same format — the client never notices.
|
|
114
134
|
|
|
135
|
+
### Methodologist (Productive Agent Layer)
|
|
136
|
+
|
|
137
|
+
Freegate classifies the task (coding / reasoning / search / chat) and injects a
|
|
138
|
+
short system-prompt methodologist **without any client-side changes**. Any
|
|
139
|
+
OpenAI-compatible client (opencode, Cursor, chat) gets engineer-grade answers:
|
|
140
|
+
|
|
141
|
+
- **coding** — brief plan before code, a suggested test, where to verify.
|
|
142
|
+
- **reasoning** — reason stepwise, show assumptions.
|
|
143
|
+
- **search** — short factual answer, don't invent sources.
|
|
144
|
+
- **chat** — to the point, concise.
|
|
145
|
+
|
|
146
|
+
The task category also nudges routing toward matching provider categories
|
|
147
|
+
(`coding`→coding models, `reasoning`→reasoning models) without dropping fallback.
|
|
148
|
+
Category distribution is visible on the dashboard and via
|
|
149
|
+
`node tools/context-diag.js`.
|
|
150
|
+
|
|
151
|
+
> Methodologist prompts are a condensed derived text inspired by
|
|
152
|
+
> [superpowers](https://github.com/obra/superpowers) (MIT). Full agentic cycle
|
|
153
|
+
> (tools, subagents) runs on the client side.
|
|
154
|
+
|
|
155
|
+
### Self-updating model database
|
|
156
|
+
|
|
157
|
+
The scheduler lives inside the server — always on, no cron/launchd needed,
|
|
158
|
+
works for every npm/Docker user. Every 6 hours (configurable) Freegate:
|
|
159
|
+
|
|
160
|
+
1. **Checks existing** models: dead ones (404/402) get disabled.
|
|
161
|
+
2. **Re-checks dead** models after 7 days — if the provider restores a model, it's re-enabled automatically.
|
|
162
|
+
3. **Scans 8 sources**: OpenRouter, HuggingFace + native model lists of Groq, Mistral, Gemini, Cerebras, DeepSeek, NVIDIA NIM.
|
|
163
|
+
4. **Tests new candidates** in parallel and adds the working ones.
|
|
164
|
+
5. **Computes a score** (success rate + latency + context window + freshness) and sorts: best models get routing priority.
|
|
165
|
+
|
|
166
|
+
Manual catalog entries (your hand-set priorities in `providers.json`) are never
|
|
167
|
+
overwritten — sorting applies only to auto-added models.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
node tools/models-db.js # model base report
|
|
171
|
+
node scripts/auto-manage-models.js # manual cycle run
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Configuration (`config.json`):
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{ "modelManager": { "enabled": true, "intervalHours": 6, "autoAdd": true, "recheckDisabledDays": 7 } }
|
|
178
|
+
```
|
|
179
|
+
|
|
115
180
|
## Development
|
|
116
181
|
|
|
117
182
|
```bash
|
|
118
|
-
npm test # unit tests (
|
|
183
|
+
npm test # unit tests (187)
|
|
119
184
|
node server.js # run from source
|
|
120
185
|
```
|
|
121
186
|
|
package/README.ru.md
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
[](https://hub.docker.com/r/nik951751/freegate)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.npmjs.com/package/freegate)
|
|
7
|
+
[](/#features)
|
|
8
|
+
[](/#why-free)
|
|
9
|
+
[](/#private)
|
|
7
10
|
|
|
8
11
|
**English version: [README.md](README.md)**
|
|
9
12
|
|
|
10
13
|
## Зачем платить за LLM, когда есть бесплатные?
|
|
11
14
|
|
|
12
|
-
Твой AI-агент, бот или скрипт использует
|
|
13
|
-
За ним Freegate автоматически распределяет запросы между **
|
|
15
|
+
Твой AI-агент, бот или скрипт использует **один OpenAI-совместимый endpoint**.
|
|
16
|
+
За ним Freegate автоматически распределяет запросы между **34 бесплатными
|
|
14
17
|
моделями** — Groq, Mistral, Gemini, NVIDIA NIM, OpenRouter, ZAI, Cerebras,
|
|
15
18
|
DeepSeek и локальные модели. Если один провайдер упал, перегружен или сжёг
|
|
16
19
|
дневной лимит — запрос **мгновенно уходит на следующий**. Ты никогда не
|
|
@@ -18,31 +21,48 @@ DeepSeek и локальные модели. Если один провайде
|
|
|
18
21
|
|
|
19
22
|
**Результат:** полноценный LLM-доступ для повседневной работы по цене $0.
|
|
20
23
|
|
|
24
|
+
### 🔒 Работает локально — переписки никуда не уходят
|
|
25
|
+
|
|
26
|
+
Freegate крутится **на твоей машине** (`http://localhost:4000`). Твой `localhost` —
|
|
27
|
+
только твой: никто другой не может к нему подключиться, и наоборот. Ключи
|
|
28
|
+
провайдеров лежат в твоём локальном `.env`, история — в локальных файлах. **Ни
|
|
29
|
+
один чужой сервер не видит твои ключи и переписки.**
|
|
30
|
+
|
|
21
31
|

|
|
22
32
|
|
|
23
33
|
## Возможности
|
|
24
34
|
|
|
25
35
|
| | |
|
|
26
36
|
|---|---|
|
|
27
|
-
| 🔀 **Автопереключение** |
|
|
28
|
-
| 🤖 **Автоуправление моделями** | Сам находит новые бесплатные модели, тестирует, добавляет рабочие, отключает мёртвые —
|
|
37
|
+
| 🔀 **Автопереключение** | 34 провайдера в одной цепочке. Провайдер упал? Следующий уже отвечает. |
|
|
38
|
+
| 🤖 **Автоуправление моделями** | Сам находит новые бесплатные модели, тестирует, добавляет рабочие, отключает мёртвые — встроенный планировщик, всегда актуальная база. |
|
|
39
|
+
| 🗂️ **База моделей** | Структурированный паспорт каждой модели (скор, латентность, окно, история) + сортировка: лучшие модели получают приоритет в роутинге. |
|
|
29
40
|
| 🏷️ **Категории моделей** | reasoning / coding / general / vision / local — правильная модель для каждой задачи. |
|
|
30
41
|
| 🖼️ **Vision-конвейер** | Скриншот → vision-модель читает → кодинг-модель отвечает на вопрос. |
|
|
31
42
|
| 💰 **Бесплатно** | Только free-модели. Дашборд показывает остаток лимита каждого провайдера. |
|
|
32
43
|
| ⚡ **Умный выбор** | Прокси сам находит самый быстрый и стабильный провайдер для каждого запроса. |
|
|
33
44
|
| 🛡️ **Надёжность** | Circuit breaker, очередь запросов, автоотключение мёртвых провайдеров, watchdog. |
|
|
34
|
-
| 📊 **Дашборд** | Статус, скорость, лимиты, история, токены, RPM
|
|
45
|
+
| 📊 **Дашборд** | Статус, скорость, лимиты, история, токены, RPM-график, экономия ($). RU/EN. |
|
|
35
46
|
| 💾 **Кэш на диске** | Повторные промпты не тратят лимиты вообще. |
|
|
47
|
+
| 🎓 **Методолог** | Агент отвечает как инженер: план→тест→код (coding), пошагово (reasoning). Промпты настраиваются в `config.json`. |
|
|
36
48
|
| 🔌 **Совместимость** | Любой OpenAI-клиент: opencode, Cursor, ChatGPT-аналоги, твои скрипты. |
|
|
37
49
|
|
|
50
|
+

|
|
51
|
+
|
|
38
52
|
## Быстрый старт — 30 секунд
|
|
39
53
|
|
|
40
54
|
```bash
|
|
41
|
-
npx freegate init -i #
|
|
55
|
+
npx freegate init -i # мастер: режим quick (1 ключ OpenRouter) или full (все ключи)
|
|
42
56
|
npx freegate start # прокси на http://localhost:4000
|
|
43
57
|
npx freegate test # проверить, что всё работает
|
|
44
58
|
```
|
|
45
59
|
|
|
60
|
+
**Режимы init:**
|
|
61
|
+
- **quick** — вставь один ключ OpenRouter → сразу 15+ бесплатных моделей. Остальные ключи добавишь позже в дашборде → «Настройки».
|
|
62
|
+
- **full** — все ключи провайдеров → 8 источников, максимум скорости и надёжности (авто-failover).
|
|
63
|
+
|
|
64
|
+
**Подключить к Cursor в 2 клика:** Cursor → Settings → Models → «OpenAI-compatible» → Base URL `http://localhost:4000/v1`, API Key = твой пароль.
|
|
65
|
+
|
|
46
66
|
Дашборд: `http://localhost:4000/?key=твой_пароль`
|
|
47
67
|
|
|
48
68
|
Или через Docker:
|
|
@@ -111,10 +131,58 @@ docker run -d --name freegate -p 4000:4000 \
|
|
|
111
131
|
3. Если запрос не прошёл — мгновенно пробует следующий из цепочки.
|
|
112
132
|
4. Ответ возвращается клиенту в том же формате — клиент ничего не замечает.
|
|
113
133
|
|
|
134
|
+
### Методолог (Productive Agent Layer)
|
|
135
|
+
|
|
136
|
+
Freegate определяет тип задачи (кодинг / рассуждение / поиск / болтовня) и
|
|
137
|
+
подмешивает короткий системный промпт-методолог **без изменений на стороне
|
|
138
|
+
клиента**. Любой OpenAI-совместимый клиент (opencode, Cursor, чат) получает
|
|
139
|
+
ответы как от опытного инженера:
|
|
140
|
+
|
|
141
|
+
- **coding** — краткий план перед кодом, предложенный тест, где проверять.
|
|
142
|
+
- **reasoning** — рассуждать пошагово, показывать допущения.
|
|
143
|
+
- **search** — короткий фактологичный ответ, не выдумывать источник.
|
|
144
|
+
- **chat** — по существу и кратко.
|
|
145
|
+
|
|
146
|
+
Дополнительно: категория задачи даёт буст моделям подходящей категории
|
|
147
|
+
(`coding`→coding-модели, `reasoning`→reasoning-модели), не исключая fallback.
|
|
148
|
+
Распределение категорий видно в дашборде и через
|
|
149
|
+
`node tools/context-diag.js`.
|
|
150
|
+
|
|
151
|
+
> Методолог-промпты — производный сжатый текст по мотивам
|
|
152
|
+
> [superpowers](https://github.com/obra/superpowers) (MIT). Полный агентный цикл
|
|
153
|
+
> (инструменты, субагенты) выполняется на стороне клиента.
|
|
154
|
+
|
|
155
|
+
### Самообновляющаяся база моделей
|
|
156
|
+
|
|
157
|
+
Планировщик встроен в сервер — работает всегда, без cron/launchd, у всех
|
|
158
|
+
пользователей пакета. Каждые 6 часов (настраивается) Freegate:
|
|
159
|
+
|
|
160
|
+
1. **Проверяет существующие** модели: мёртвые (404/402) отключаются.
|
|
161
|
+
2. **Перепроверяет мёртвых** через 7 дней — если провайдер вернул модель, она автоматически реактивируется.
|
|
162
|
+
3. **Сканирует 8 источников**: OpenRouter, HuggingFace + нативные списки Groq, Mistral, Gemini, Cerebras, DeepSeek, NVIDIA NIM.
|
|
163
|
+
4. **Тестирует новых кандидатов** параллельно и добавляет рабочие.
|
|
164
|
+
5. **Считает скор** (success-rate + латентность + контекст-окно + свежесть) и сортирует: лучшие модели получают приоритет в роутинге.
|
|
165
|
+
|
|
166
|
+
Ручные записи каталога (твои приоритеты в `providers.json`) не перезаписываются
|
|
167
|
+
— сортировка применяется только к автодобавленным моделям.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
node tools/models-db.js # отчёт по базе
|
|
171
|
+
curl -s "localhost:4000/v1/models-db?key=пароль" | python3 -m json.tool | head -30
|
|
172
|
+
node scripts/auto-manage-models.js # ручной прогон цикла
|
|
173
|
+
AUTO_ADD=false node scripts/auto-manage-models.js # только отчёт, без записи
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Конфигурация (`config.json`):
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{ "modelManager": { "enabled": true, "intervalHours": 6, "autoAdd": true, "recheckDisabledDays": 7 } }
|
|
180
|
+
```
|
|
181
|
+
|
|
114
182
|
## Разработка
|
|
115
183
|
|
|
116
184
|
```bash
|
|
117
|
-
npm test # unit-тесты (
|
|
185
|
+
npm test # unit-тесты (187 шт.)
|
|
118
186
|
node server.js # запуск из исходников
|
|
119
187
|
```
|
|
120
188
|
|
|
@@ -142,6 +210,24 @@ PROVIDER_ZAI_APIKEY=... # ZAI
|
|
|
142
210
|
**Провайдеры** — каталог в `providers.json` (18 моделей). Добавить свой:
|
|
143
211
|
впиши его в `config.json` → `providers` (формат как в `providers.json`).
|
|
144
212
|
|
|
213
|
+
**Методолог** — в `config.json`:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"methodology": {
|
|
218
|
+
"enabled": true,
|
|
219
|
+
"prompts": {
|
|
220
|
+
"coding": "Перед кодом — краткий план. Следи, чтобы тест описывал поведение.",
|
|
221
|
+
"reasoning": "Рассуждай пошагово, показывай допущения, затем вывод."
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `enabled: false` — полностью выключает методолог и роутинг-буст.
|
|
228
|
+
- `prompts` — переопределяет текст для конкретной категории; остальные
|
|
229
|
+
остаются в дефолтах.
|
|
230
|
+
|
|
145
231
|
## Команды CLI
|
|
146
232
|
|
|
147
233
|
```bash
|
|
Binary file
|
package/assets/dashboard.png
CHANGED
|
Binary file
|
package/bin/freegate.js
CHANGED
|
@@ -31,14 +31,26 @@ function makeReader() {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Виртуальный провайдер категорий моделей. Вся полезная инфа для онбординга.
|
|
35
|
+
const EASY_START_PROVIDER = 'PROVIDER_OPENROUTER_APIKEY';
|
|
36
|
+
const EASY_START_NAME = 'OpenRouter';
|
|
37
|
+
|
|
34
38
|
async function initWizard() {
|
|
35
39
|
const ask = makeReader();
|
|
36
40
|
|
|
41
|
+
// 0. Объяснение + выбор режима (quick / full).
|
|
42
|
+
console.log('\n=== Freegate — настройка ===');
|
|
43
|
+
console.log('Почему Freegate бесплатный? Он маршрутизирует запросы между бесплатными моделями.');
|
|
44
|
+
console.log(' • Минимум: 1 ключ OpenRouter → сразу 15+ бесплатных моделей (быстрый старт).');
|
|
45
|
+
console.log(' • Максимум: ключи всех провайдеров → 8 источников, максимум скорости и надёжности.\n');
|
|
46
|
+
const mode = (await ask('Режим [q]uick (1 ключ OpenRouter) или [f]ull (все ключи)? (q/f): ')).trim().toLowerCase();
|
|
47
|
+
const full = mode === 'f' || mode === 'full';
|
|
48
|
+
|
|
37
49
|
// 1. Auth key for the proxy
|
|
38
50
|
const auth = await ask('Пароль для доступа к прокси (Enter = сгенерировать): ');
|
|
39
51
|
const authKey = auth.trim() || 'dc_' + Math.random().toString(36).slice(2, 14);
|
|
40
52
|
|
|
41
|
-
// 2. Collect provider keys from the catalog (unique env vars)
|
|
53
|
+
// 2. Collect provider keys from the catalog (unique env vars).
|
|
42
54
|
const envVars = new Map(); // envVar -> [providerNames]
|
|
43
55
|
for (const [name, p] of Object.entries(CATALOG)) {
|
|
44
56
|
if (!p.envVar) continue;
|
|
@@ -50,7 +62,15 @@ async function initWizard() {
|
|
|
50
62
|
for (const [envVar, providers] of envVars) {
|
|
51
63
|
const sample = providers[0];
|
|
52
64
|
const hint = CATALOG[sample].keyHint || '';
|
|
53
|
-
|
|
65
|
+
// В quick-режиме OpenRouter просим обязательно-первым, остальные пропускаем.
|
|
66
|
+
if (!full && envVar !== EASY_START_PROVIDER) continue;
|
|
67
|
+
if (!full && envVar === EASY_START_PROVIDER) {
|
|
68
|
+
console.log(`\n👉 ${EASY_START_NAME} — самый большой источник бесплатных моделей.`);
|
|
69
|
+
console.log(` Не хватает мощности? Позже добавь остальные ключи в дашборде (Настройки).`);
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`\n${providers.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
const answer = await ask(` Ключ (${hint}) — пусто = пропустить: `);
|
|
54
74
|
if (answer.trim()) keys[envVar] = answer.trim();
|
|
55
75
|
}
|
|
56
76
|
|
|
@@ -73,8 +93,15 @@ async function initWizard() {
|
|
|
73
93
|
console.log('✓ config.json создан');
|
|
74
94
|
|
|
75
95
|
const filled = Object.values(keys).filter(Boolean).length;
|
|
76
|
-
|
|
96
|
+
if (full) {
|
|
97
|
+
console.log(`\nГотово! Провайдеров с ключами: ${filled} из ${envVars.size} (полный режим)`);
|
|
98
|
+
} else {
|
|
99
|
+
const hasEasy = !!keys[EASY_START_PROVIDER];
|
|
100
|
+
console.log(`\nГотово! Режим quick: ${hasEasy ? 'OpenRouter добавлен' : 'ключей не введено'}.`);
|
|
101
|
+
console.log(' Провайдеров с ключами: ' + filled + '. Добавить больше ключей можно в дашборде → Настройки.');
|
|
102
|
+
}
|
|
77
103
|
console.log(`Запуск: npx freegate start (пароль: ${authKey})`);
|
|
104
|
+
console.log('Подключить к Cursor: Настройки Cursor → Models → OpenAI-compatible → http://localhost:4000/v1');
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
function copyExample(name, example) {
|
|
@@ -108,7 +135,7 @@ if (cmd === 'init') {
|
|
|
108
135
|
const { execSync } = require('child_process');
|
|
109
136
|
const isMac = process.platform === 'darwin';
|
|
110
137
|
const launchDir = process.cwd();
|
|
111
|
-
const label = 'com.
|
|
138
|
+
const label = 'com.freegate.proxy';
|
|
112
139
|
|
|
113
140
|
if (isMac) {
|
|
114
141
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -220,7 +247,7 @@ WantedBy=default.target
|
|
|
220
247
|
} else {
|
|
221
248
|
console.log('Freegate — бесплатный LLM-прокси с failover');
|
|
222
249
|
console.log('Команды:');
|
|
223
|
-
console.log(' npx freegate init интерактивная настройка (
|
|
250
|
+
console.log(' npx freegate init интерактивная настройка (quick/full режим, пароль)');
|
|
224
251
|
console.log(' npx freegate start запустить прокси');
|
|
225
252
|
console.log(' npx freegate status диагностика: провайдеры, лимиты, ошибки');
|
|
226
253
|
console.log(' npx freegate test проверить, что прокси работает');
|
package/config.example.json
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
"port": 4000,
|
|
4
4
|
"auth": "your-secret-key-here",
|
|
5
5
|
"rateLimit": { "maxRequests": 100, "windowMs": 60000 },
|
|
6
|
+
"modelManager": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"intervalHours": 6,
|
|
9
|
+
"autoAdd": true,
|
|
10
|
+
"recheckDisabledDays": 7
|
|
11
|
+
},
|
|
12
|
+
"methodology": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"prompts": {
|
|
15
|
+
"coding": "Перед кодом — краткий план. Следи, чтобы тест описывал поведение.",
|
|
16
|
+
"reasoning": "Рассуждай пошагово, показывай допущения, затем вывод."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
6
19
|
"providers": {
|
|
7
20
|
"or-nemotron-550b": {
|
|
8
21
|
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
|
package/lib/bandit.js
CHANGED
|
@@ -69,4 +69,11 @@ function recordOutcome(bucketPriors, key, success) {
|
|
|
69
69
|
else bucketPriors[key].b += 1;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// Временные лимиты (429/403/402/401) — это НЕ качество провайдера, а временное
|
|
73
|
+
// состояние (лимит запросов, квота, баланс). Bandit не должен наказывать за них.
|
|
74
|
+
const TRANSIENT_LIMITS = new Set([401, 402, 403, 408, 429]);
|
|
75
|
+
function isTransientLimit(status) {
|
|
76
|
+
return typeof status === 'number' && TRANSIENT_LIMITS.has(status);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { bucket, sampleBeta, pick, recordOutcome, BUCKET_THRESHOLDS, isTransientLimit };
|
package/lib/cache.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { normalizeMessages } = require('./normalize');
|
|
5
|
+
const { normalizeMessages, looksLikeCode } = require('./normalize');
|
|
6
|
+
const { trigrams, dice } = require('./semcache');
|
|
6
7
|
|
|
7
8
|
const MAX_SIZE = 500;
|
|
8
9
|
const DEFAULT_TTL = 3600000; // 1 hour in ms
|
|
@@ -16,6 +17,7 @@ class LRUCache {
|
|
|
16
17
|
this.cache = new Map();
|
|
17
18
|
this.hits = 0;
|
|
18
19
|
this.misses = 0;
|
|
20
|
+
this.semHits = 0;
|
|
19
21
|
this.useNormalize = useNormalize;
|
|
20
22
|
if (!skipLoad) this.load();
|
|
21
23
|
}
|
|
@@ -42,8 +44,8 @@ class LRUCache {
|
|
|
42
44
|
if (data && Array.isArray(data.entries)) {
|
|
43
45
|
const now = Date.now();
|
|
44
46
|
for (const e of data.entries) {
|
|
45
|
-
|
|
46
|
-
|
|
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 });
|
|
47
49
|
}
|
|
48
50
|
// Restore hit/miss counters so hitRate survives restarts
|
|
49
51
|
if (typeof data.hits === 'number') this.hits = data.hits;
|
|
@@ -61,7 +63,7 @@ class LRUCache {
|
|
|
61
63
|
try {
|
|
62
64
|
const size = Buffer.byteLength(JSON.stringify(entry.value));
|
|
63
65
|
if (size > MAX_ENTRY_BYTES) continue;
|
|
64
|
-
entries.push({ key, value: entry.value, created: entry.created, uses: entry.uses || 0 });
|
|
66
|
+
entries.push({ key, value: entry.value, created: entry.created, uses: entry.uses || 0, model: entry.model, temperature: entry.temperature, grams: entry.grams || null });
|
|
65
67
|
} catch {}
|
|
66
68
|
}
|
|
67
69
|
// Keep only the most recent 300 on disk to bound file size
|
|
@@ -102,6 +104,43 @@ class LRUCache {
|
|
|
102
104
|
return entry.value;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
// Семантический кэш: на промахе точного ключа ищем закэшированный диалог с
|
|
108
|
+
// похожим нормализованным текстом (Dice по символьным триграммам). Отдаём
|
|
109
|
+
// только при совпадении model+temperature и similarity >= minSimilarity.
|
|
110
|
+
// Безопасность: код, сообщения с нестроковым content и role:'tool' никогда
|
|
111
|
+
// не матчатся семантически (разный интент/состояние инструментов).
|
|
112
|
+
getSemantic(model, messages, temperature, minSimilarity = 0.85) {
|
|
113
|
+
// Guards mirror set()'s grams computation — anything that gets grams:null
|
|
114
|
+
// in set() must not match here either.
|
|
115
|
+
if (!Array.isArray(messages)) { this.misses++; return null; }
|
|
116
|
+
const norm = normalizeMessages(messages);
|
|
117
|
+
if (!norm || looksLikeCode(messages)) { this.misses++; return null; }
|
|
118
|
+
if (messages.some(m => m && (typeof m.content !== 'string' || m.role === 'tool'))) { this.misses++; return null; }
|
|
119
|
+
|
|
120
|
+
const qgrams = new Set(trigrams(norm));
|
|
121
|
+
if (qgrams.size === 0) { this.misses++; return null; }
|
|
122
|
+
|
|
123
|
+
let bestKey = null;
|
|
124
|
+
let bestSim = 0;
|
|
125
|
+
for (const [key, entry] of this.cache) {
|
|
126
|
+
if (entry.model !== model || entry.temperature !== temperature) continue;
|
|
127
|
+
if (!entry.grams || entry.grams.length === 0) continue;
|
|
128
|
+
const sim = dice(qgrams, new Set(entry.grams));
|
|
129
|
+
if (sim > bestSim) { bestSim = sim; bestKey = key; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!bestKey || bestSim < minSimilarity) { this.misses++; return null; }
|
|
133
|
+
|
|
134
|
+
const entry = this.cache.get(bestKey);
|
|
135
|
+
// Bump TTL + usage like a regular hit so hot semantic entries stay cached.
|
|
136
|
+
entry.created = Date.now();
|
|
137
|
+
entry.uses = (entry.uses || 0) + 1;
|
|
138
|
+
this.cache.delete(bestKey);
|
|
139
|
+
this.cache.set(bestKey, entry);
|
|
140
|
+
this.semHits++;
|
|
141
|
+
return { value: entry.value, similarity: Math.round(bestSim * 1000) / 1000 };
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
set(model, messages, temperature, value) {
|
|
106
145
|
const key = this._key(model, messages, temperature);
|
|
107
146
|
|
|
@@ -119,13 +158,23 @@ class LRUCache {
|
|
|
119
158
|
if (victim) this.cache.delete(victim);
|
|
120
159
|
}
|
|
121
160
|
|
|
122
|
-
|
|
161
|
+
// Семантический индекс: триграммы нормализованного диалога + параметры
|
|
162
|
+
// запроса. Для кода/content-массивов grams не строим (их и так не ищем).
|
|
163
|
+
let grams = null;
|
|
164
|
+
const norm = normalizeMessages(messages);
|
|
165
|
+
if (norm && !looksLikeCode(messages) &&
|
|
166
|
+
!(Array.isArray(messages) && messages.some(m => m && (typeof m.content !== 'string' || m.role === 'tool')))) {
|
|
167
|
+
grams = new Set(trigrams(norm));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.cache.set(key, { value, created: Date.now(), uses: 0, model, temperature, grams: grams ? [...grams] : null });
|
|
123
171
|
}
|
|
124
172
|
|
|
125
173
|
stats() {
|
|
126
174
|
return {
|
|
127
175
|
hits: this.hits,
|
|
128
176
|
misses: this.misses,
|
|
177
|
+
semHits: this.semHits,
|
|
129
178
|
size: this.cache.size,
|
|
130
179
|
maxSize: this.maxSize,
|
|
131
180
|
hitRate: this.hits + this.misses > 0
|
package/lib/clean.js
CHANGED
|
@@ -67,12 +67,17 @@ function hasContent(data) {
|
|
|
67
67
|
|
|
68
68
|
// Ответ короче MIN_ANSWER_LEN символов считается мусором (обрыв/один токен).
|
|
69
69
|
const MIN_ANSWER_LEN = 5;
|
|
70
|
-
|
|
70
|
+
// Короткий вопрос допускает короткий ответ: «ok» на «reply ok» — валидный
|
|
71
|
+
// ответ, а не обрыв. Порог масштабируется: чем короче вопрос, тем короче
|
|
72
|
+
// допустимый ответ, но никогда не ниже 1 символа.
|
|
73
|
+
function isTooShort(data, askedText) {
|
|
71
74
|
if (!data || !Array.isArray(data.choices)) return true;
|
|
72
75
|
const msg = data.choices[0]?.message;
|
|
73
76
|
const content = typeof msg?.content === 'string' ? msg.content.trim() : '';
|
|
74
77
|
const reasoning = typeof msg?.reasoning === 'string' ? msg.reasoning.trim() : '';
|
|
75
|
-
|
|
78
|
+
const ask = typeof askedText === 'string' ? askedText.trim().length : 0;
|
|
79
|
+
const threshold = ask > 0 && ask < 40 ? Math.max(1, Math.min(MIN_ANSWER_LEN, Math.ceil(ask / 8))) : MIN_ANSWER_LEN;
|
|
80
|
+
return (content + reasoning).length < threshold;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
module.exports = { stripThink, cleanMessage, cleanDelta, fixReasoningMessage, hasContent, isTooShort, MIN_ANSWER_LEN };
|