feat: полный бэк и фронт (попытки, разбор, импорт, ИИ, назначения)
- Сервисы: 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
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user