0fe04d4d99
- Сервисы: testAttemptService, testAccess, document import/gen/extract, LLM, assignment, aiEditor - Конфиг: devAuthor, featureFlags; messages/ru; интеграция V.9 (skip без БД) - API/роуты: app, auth, server; Dockerfile и env example - Фронт: TestAttempt, TestAttemptReview, AttemptReviewBlock, стили, правки App/api/login/vite - compose и README; смоук-тесты расширены Закрывает отсутствие модулей в origin после клона. Made-with: Cursor
99 lines
2.7 KiB
JavaScript
99 lines
2.7 KiB
JavaScript
/**
|
||
* 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<string>} 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;
|
||
}
|