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:
@@ -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) {}
|
||||
|
||||
@@ -339,6 +339,8 @@ function searchFaq(q, n) {
|
||||
* на ENV и дефолты. Если ключа нет и URL не локальный — работаем как FAQ. */
|
||||
function _setting(k) { try { const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get(k); return r && r.value != null ? r.value : null; } catch (e) { return null; } }
|
||||
function _isLocal(url) { return /\/\/(localhost|127\.0\.0\.1)/.test(url || ''); }
|
||||
// Шлюзы с бесплатным инференсом БЕЗ ключа (наряду с localhost): ключ не обязателен.
|
||||
function _noKeyNeeded(url) { return _isLocal(url) || /\/\/[^/]*\bpollinations\.ai\b/i.test(url || ''); }
|
||||
|
||||
/* Список провайдеров (несколько ключей/моделей). Хранится JSON в app_settings.
|
||||
* Если списка нет — синтезируем из legacy-настроек/ENV, чтобы ничего не сломать. */
|
||||
@@ -357,7 +359,7 @@ function _providers() {
|
||||
/* Конфиги в порядке использования: активный первым, затем остальные с ключом
|
||||
* (для авто-перехвата при лимите/ошибке). */
|
||||
function providersOrdered() {
|
||||
const arr = _providers().filter(p => p && (p.key || _isLocal(p.url)));
|
||||
const arr = _providers().filter(p => p && (p.key || _noKeyNeeded(p.url)));
|
||||
const activeId = _setting('assistant_active');
|
||||
const active = arr.filter(p => p.id === activeId);
|
||||
const rest = arr.filter(p => p.id !== activeId);
|
||||
@@ -455,7 +457,7 @@ async function callLLMFailover(messages, maxTokens) {
|
||||
async function pingLLM(override) {
|
||||
const cfg = override || llmConfig();
|
||||
if (!cfg.url) return { ok: false, error: 'URL не задан' };
|
||||
if (!cfg.key && !/\/\/(localhost|127\.0\.0\.1)/.test(cfg.url)) return { ok: false, error: 'Ключ не задан' };
|
||||
if (!cfg.key && !_noKeyNeeded(cfg.url)) return { ok: false, error: 'Ключ не задан' };
|
||||
if (typeof fetch !== 'function') return { ok: false, error: 'fetch недоступен' };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 15000);
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
'<div class="asst-pcic">' + SPARK + '</div>' +
|
||||
'<div class="asst-pcb"><div class="asst-pcn">' + esc(p.name || 'Провайдер') +
|
||||
(act ? '<span class="asst-bdg act">активен</span>' : '') +
|
||||
'<span class="asst-bdg ' + (p.hasKey ? 'key' : 'nokey') + '">' + (p.hasKey ? 'ключ есть' : 'нет ключа') + '</span></div>' +
|
||||
(p.hasKey ? '<span class="asst-bdg key">ключ есть</span>' : p.noKey ? '<span class="asst-bdg key">без ключа</span>' : '<span class="asst-bdg nokey">нет ключа</span>') + '</div>' +
|
||||
'<div class="asst-pcs">' + esc(p.model || '') + '</div>' + ksel + lim + '</div>' +
|
||||
'<div class="asst-pca">' +
|
||||
(act ? '' : '<button class="asst-ib primary" data-act="activate" data-id="' + p.id + '">Сделать активным</button>') +
|
||||
|
||||
Reference in New Issue
Block a user