feat(assistant): несколько провайдеров ИИ + выбор активного + авто-перехват при лимите

Конфиг стал списком провайдеров (assistant_providers) + активный (assistant_active).
llmConfig берёт активного; providersOrdered — активный первым, затем остальные с
ключом; callLLMFailover перебирает их при 429/сетевой ошибке (второй ключ подхватывает
при исчерпании квоты). Legacy мигрируется в список. Админ-раздел: список провайдеров
(радио-активный, Тест/Изменить/Удалить) + форма с пресетами. Эндпоинты
POST/DELETE /admin/assistant/provider(/:id), POST /admin/assistant/active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 20:21:06 +03:00
parent 78300845ed
commit e2bff24b5b
5 changed files with 227 additions and 98 deletions
+4
View File
@@ -1052,6 +1052,7 @@ window.LS = {
createMaterialCollection, updateMaterialCollection, deleteMaterialCollection,
assistantContext, assistantSeen, assistantDismiss, assistantSettings, assistantAsk, assistantFlashcards, assistantFeedback,
adminGetAssistant, adminSaveAssistant, adminTestAssistant, adminReindexTextbooks,
adminSaveProvider, adminDeleteProvider, adminSetActiveProvider,
fcListDecks, fcCreateDeck, fcAddCard,
escapeHtml, esc,
parseDate, fmtRelTime, safeHref,
@@ -1280,6 +1281,9 @@ async function adminGetAssistant() { return req('GET', '/admin/assistant'); }
async function adminSaveAssistant(d) { return req('PUT', '/admin/assistant', d); }
async function adminTestAssistant(d) { return req('POST', '/admin/assistant/test', d || {}); }
async function adminReindexTextbooks() { return req('POST', '/admin/assistant/reindex', {}); }
async function adminSaveProvider(d) { return req('POST', '/admin/assistant/provider', d); }
async function adminDeleteProvider(id) { return req('DELETE', `/admin/assistant/provider/${id}`); }
async function adminSetActiveProvider(id) { return req('POST', '/admin/assistant/active', { id }); }
async function fcListDecks() { return req('GET', '/flashcards/decks'); }
async function fcCreateDeck(d) { return req('POST', '/flashcards/decks', d); }
async function fcAddCard(deckId, d) { return req('POST', `/flashcards/decks/${deckId}/cards`, d); }