diff --git a/backend/.env.example b/backend/.env.example index 9157af9..ca9339c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,3 +18,6 @@ LLM_API_KEY= # Базовый URL API LLM (опционально). По умолчанию https://api.openai.com # Примеры: Ollama — http://localhost:11434/v1, Azure — https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT LLM_API_BASE_URL= + +# Имя модели LLM (опционально). Для OpenAI: gpt-4o-mini. Для Ollama: qwen2.5:7b, qwen3:7b +LLM_MODEL= diff --git a/backend/package.json b/backend/package.json index 2800bf4..99e30be 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "start": "node dist/app.js", "migrate": "tsx src/db/migrate.ts", "migrate:prod": "node dist/db/migrate.js", - "migrate:prod": "node dist/db/migrate.js" + "test:llm": "tsx src/scripts/testLlm.ts" }, "dependencies": { "@family-budget/shared": "*", diff --git a/backend/src/config.ts b/backend/src/config.ts index 376ba6a..984eb48 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -24,4 +24,7 @@ export const config = { /** Базовый URL API LLM. По умолчанию https://api.openai.com. Для Ollama: http://localhost:11434/v1 */ llmApiBaseUrl: process.env.LLM_API_BASE_URL || undefined, + + /** Имя модели LLM. Для OpenAI: gpt-4o-mini. Для Ollama: qwen2.5:7b, qwen3:7b и т.п. */ + llmModel: process.env.LLM_MODEL || 'gpt-4o-mini', }; diff --git a/backend/src/scripts/testLlm.ts b/backend/src/scripts/testLlm.ts new file mode 100644 index 0000000..5a4fecf --- /dev/null +++ b/backend/src/scripts/testLlm.ts @@ -0,0 +1,42 @@ +/** + * Тестовый запрос к LLM серверу. + * Запуск: npm run test:llm + */ +import OpenAI from 'openai'; +import { config } from '../config'; + +async function main() { + if (!config.llmApiKey || config.llmApiKey.trim() === '') { + console.error('Ошибка: LLM_API_KEY не задан в .env'); + process.exit(1); + } + + const openai = new OpenAI({ + apiKey: config.llmApiKey, + ...(config.llmApiBaseUrl && { baseURL: config.llmApiBaseUrl }), + }); + + const url = config.llmApiBaseUrl || 'https://api.openai.com/v1'; + console.log('LLM сервер:', url); + console.log('Модель:', config.llmModel); + console.log('---'); + + try { + const completion = await openai.chat.completions.create({ + model: config.llmModel, + messages: [{ role: 'user', content: 'Ответь одним словом: какой цвет у неба?' }], + temperature: 0, + max_tokens: 50, + }); + + const content = completion.choices[0]?.message?.content; + console.log('Ответ:', content || '(пусто)'); + console.log('---'); + console.log('OK'); + } catch (err) { + console.error('Ошибка:', err instanceof Error ? err.message : err); + process.exit(1); + } +} + +main(); diff --git a/backend/src/services/pdfToStatement.ts b/backend/src/services/pdfToStatement.ts index 120f160..def9b65 100644 --- a/backend/src/services/pdfToStatement.ts +++ b/backend/src/services/pdfToStatement.ts @@ -105,12 +105,14 @@ export async function convertPdfToStatement( try { const completion = await openai.chat.completions.create({ - model: 'gpt-4o-mini', + model: config.llmModel, messages: [ { role: 'system', content: PDF2JSON_PROMPT }, { role: 'user', content: `Текст выписки:\n\n${text}` }, ], temperature: 0, + max_tokens: 32768, + response_format: { type: 'json_object' }, }); const content = completion.choices[0]?.message?.content?.trim(); diff --git a/package-lock.json b/package-lock.json index c6b16d1..9b60560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,7 +89,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1557,7 +1556,6 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -1636,7 +1634,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1776,7 +1773,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2926,7 +2922,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz", "integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.12.0", @@ -3024,7 +3019,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3174,7 +3168,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3184,7 +3177,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4296,7 +4288,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4",