fix(assistant): таймаут генерации тестов/карточек 15с был мал → 502
Симптом: POST /api/assistant/questions отдавал 502 «Не удалось сгенерировать вопросы» ровно через ~15с. Причина: callLLM имел жёсткий таймаут 15с, а бесплатная модель (owl-alpha) на генерацию 2200 токенов JSON порой тратит больше — abort по таймауту, failover не выручал. Чат-ответам 15с хватает, а генерации — нет. callLLM/callLLMFailover получили опц. параметр timeoutMs (деф. 15с — чат не тронут). questionsFromText → 45с, flashcardsFromText → 40с. Клиент req() без своего таймаута, дождётся ответа. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -421,11 +421,11 @@ function bumpUsage(field) {
|
||||
|
||||
/* Низкоуровневый вызов OpenAI-совместимого chat/completions. */
|
||||
/* Возвращает { text, error } — error: 'off'|'rate_limit'|'http'|'timeout'|'network'|'empty'|null. */
|
||||
async function callLLM(messages, maxTokens, override) {
|
||||
async function callLLM(messages, maxTokens, override, timeoutMs) {
|
||||
const cfg = override || llmConfig();
|
||||
if (typeof fetch !== 'function' || !cfg.on) return { text: null, error: 'off' };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 15000);
|
||||
const timer = setTimeout(() => ctrl.abort(), timeoutMs || 15000);
|
||||
try {
|
||||
const r = await fetch(cfg.url, {
|
||||
method: 'POST',
|
||||
@@ -451,12 +451,12 @@ function _recordFailover(failed, served, reason) {
|
||||
}
|
||||
function _clearFailover() { try { db.prepare("DELETE FROM app_settings WHERE key = 'assistant_failover'").run(); } catch (e) {} }
|
||||
|
||||
async function callLLMFailover(messages, maxTokens) {
|
||||
async function callLLMFailover(messages, maxTokens, timeoutMs) {
|
||||
const cfgs = providersOrdered();
|
||||
if (!cfgs.length) return { text: null, error: 'off' };
|
||||
let last = { text: null, error: 'off' }, firstErr = null;
|
||||
for (let i = 0; i < cfgs.length; i++) {
|
||||
last = await callLLM(messages, maxTokens, cfgs[i]);
|
||||
last = await callLLM(messages, maxTokens, cfgs[i], timeoutMs);
|
||||
if (i === 0) firstErr = last.error;
|
||||
if (last.text) {
|
||||
if (i === 0) _clearFailover(); // активный работает — снимаем флаг
|
||||
@@ -760,7 +760,7 @@ async function flashcardsFromText(req, res) {
|
||||
'Верни СТРОГО JSON-массив из ' + count + ' объектов вида {"front":"...","back":"..."} без markdown и пояснений. ' +
|
||||
'front — короткий вопрос, back — краткий ответ (1–2 предложения). По-русски. Формулы в LaTeX между $...$. Никакого текста вне JSON.';
|
||||
let rr;
|
||||
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 1600); }
|
||||
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 1600, 40000); }
|
||||
catch (e) { return res.status(502).json({ error: 'Не удалось обратиться к ИИ' }); }
|
||||
const raw = rr && rr.text;
|
||||
let cards = [];
|
||||
@@ -800,7 +800,7 @@ async function questionsFromText(req, res) {
|
||||
'РОВНО 4 варианта; correct — индекс правильного (0..3); ровно один правильный. ' +
|
||||
'По-русски, формулы в LaTeX между $...$. Никакого текста вне JSON, без markdown.';
|
||||
let rr;
|
||||
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 2200); }
|
||||
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 2200, 45000); }
|
||||
catch (e) { return res.status(502).json({ error: 'Не удалось обратиться к ИИ' }); }
|
||||
const raw = rr && rr.text;
|
||||
let questions = [];
|
||||
|
||||
Reference in New Issue
Block a user