feat(assistant): поддержка keyless-шлюзов + пресет Pollinations

Pollinations (text.pollinations.ai/openai, модель openai) даёт бесплатный
инференс БЕЗ ключа — проверено: 98% чистый русский. Чтобы такой провайдер
считался рабочим (раньше ключ требовался всем, кроме localhost):
- _noKeyNeeded/_aNoKey: localhost ИЛИ pollinations.ai → ключ не обязателен
  (используется в providersOrdered, pingLLM, active-check, testAssistant)
- пресет «Pollinations (без ключа)» в ASSISTANT_PRESETS
- бейдж провайдера: «без ключа» (зелёный) вместо «нет ключа» для keyless

Кейд-провайдеры (Kilo/Gemini/HF/…) по-прежнему требуют ключ — затронуты
только URL с pollinations.ai (спуф в пути отвергается).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-24 14:35:56 +03:00
parent 4d2d02f080
commit 5b4d9324a4
3 changed files with 11 additions and 6 deletions
+6 -3
View File
@@ -940,6 +940,7 @@ const ASSISTANT_PRESETS = [
{ name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile' },
{ name: 'OpenRouter', url: 'https://openrouter.ai/api/v1/chat/completions', model: 'meta-llama/llama-3.3-70b-instruct:free' },
{ name: 'HuggingFace Router', url: 'https://router.huggingface.co/v1/chat/completions', model: 'Qwen/Qwen2.5-72B-Instruct' },
{ name: 'Pollinations (без ключа)', url: 'https://text.pollinations.ai/openai', model: 'openai' },
{ name: 'Ollama (локально)', url: 'http://localhost:11434/v1/chat/completions', model: 'qwen2.5:3b' },
];
// Проверенные бесплатные модели шлюза Kilo (отдают чистый русский). Порядок — от мощных к лёгким.
@@ -967,6 +968,8 @@ function _kiloModels() {
function _aProviders() { try { return JSON.parse(_aset('assistant_providers') || '[]') || []; } catch (e) { return []; } }
function _aSetProviders(arr) { db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_providers', ?)").run(JSON.stringify(arr)); }
function _aIsLocal(u) { return /\/\/(localhost|127\.0\.0\.1)/.test(u || ''); }
// Шлюзы с бесплатным инференсом без ключа (как localhost): ключ не обязателен.
function _aNoKey(u) { return _aIsLocal(u) || /\/\/[^/]*\bpollinations\.ai\b/i.test(u || ''); }
function getAssistant(_req, res) {
// Миграция legacy-настроек в список провайдеров (один раз)
@@ -980,10 +983,10 @@ function getAssistant(_req, res) {
}
}
const list = _aProviders();
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) }));
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, noKey: _aNoKey(p.url), ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) }));
const activeId = _aset('assistant_active') || (providers[0] && providers[0].id) || null;
const ap = list.find(p => p.id === activeId);
const active = !!(ap && (ap.key || _aIsLocal(ap.url)));
const active = !!(ap && (ap.key || _aNoKey(ap.url)));
let chunks = 0, usage = { model_calls: 0, cache_hits: 0, faq: 0 }, usage30 = { model_calls: 0, cache_hits: 0, faq: 0 };
try { chunks = db.prepare('SELECT COUNT(*) n FROM textbook_chunks').get().n; } catch (e) {}
@@ -1243,7 +1246,7 @@ async function testAssistant(req, res) {
};
}
override.local = _aIsLocal(override.url);
override.on = !!(override.key || override.local);
override.on = !!(override.key || _aNoKey(override.url));
const r = await a.pingLLM(override);
// Успешный тест активного провайдера снимает устаревший флаг failover
try { const activeId = _aset('assistant_active'); if (r && r.ok && (!b.id || b.id === activeId)) a.clearFailover(); } catch (e) {}