/** * OpenAI-совместимый Chat Completions. Общий для импорта и редактора. */ /** * @returns {null | { provider: string, apiKey: string, baseUrl: string, model: string }} */ export function getLlmConfig() { if (process.env.DEEPSEEK_API_KEY) { return { provider: 'deepseek', apiKey: process.env.DEEPSEEK_API_KEY, baseUrl: (process.env.LLM_BASE_URL || 'https://api.deepseek.com/v1').replace( /\/+$/, '' ), model: process.env.LLM_MODEL || 'deepseek-chat', }; } if (process.env.OPENAI_API_KEY) { return { provider: 'openai', apiKey: process.env.OPENAI_API_KEY, baseUrl: (process.env.LLM_BASE_URL || 'https://api.openai.com/v1').replace( /\/+$/, '' ), model: process.env.LLM_MODEL || 'gpt-4o-mini', }; } return null; } /** * @param {{ baseUrl: string, apiKey: string, model: string }} cfg * @param {string} system * @param {string} user * @param {number} [temperature] * @returns {Promise} raw assistant message */ export async function chatCompletionTextContent(cfg, system, user, temperature = 0.25) { const url = `${cfg.baseUrl}/chat/completions`; const body = { model: cfg.model, messages: [ { role: 'system', content: system }, { role: 'user', content: user }, ], temperature, }; if (process.env.LLM_NO_JSON !== '1') { body.response_format = { type: 'json_object' }; } const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 120000); let res; try { res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}`, }, body: JSON.stringify(body), signal: ac.signal, }); } catch (e) { if (e.name === 'AbortError') { const err = new Error('Превышен таймаут ожидания ответа LLM (120 с).'); err.code = 'llm_timeout'; throw err; } const err = new Error( e instanceof Error ? e.message : 'Сбой сети при обращении к LLM' ); err.code = 'llm_network'; throw err; } finally { clearTimeout(t); } if (!res.ok) { const errText = await res.text(); const err = new Error( `LLM ${res.status}: ${errText.replace(/\s+/g, ' ').slice(0, 280)}` ); err.code = 'llm_http'; err.status = res.status; throw err; } const data = await res.json(); const content = data?.choices?.[0]?.message?.content; if (typeof content !== 'string' || !content.trim()) { const e = new Error('Пустой content в ответе API.'); e.code = 'llm_empty'; throw e; } return content; }