odant-work 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/#system/package-manifest.json +16 -16
- package/$server/$folder/handlers/pages/form/$handler/form.js +2 -1
- package/MODELS/$ai/$folder/$class/$ai/class.js +1 -1
- package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/class.js +6 -4
- package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/trainer.js +344 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/class.js +3 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/class.js +3 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/trainer.js +530 -920
- package/package.json +1 -1
- package/sources/data/progress.md +18 -0
- package/sources/modules/trainer/index.js +41 -1
- package/sources/modules/trainer/local-engine.js +67 -1
- package/sources/modules/trainer/runner.js +47 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odant-work",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "ODANT-WORK — платформа цифровой организации (встроенная система учёта, документооборот, управление процессами и приватный мультимодальный ИИ)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "sources/work.js",
|
package/sources/data/progress.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Прогресс: sources/data
|
|
2
2
|
|
|
3
3
|
## Последние изменения
|
|
4
|
+
- [22:05, 13.11] Новая коммерческая презентация **«ODANT-WORK — для клиентов и региональных партнёров (франчайзи)»** (12 слайдов 16:9) и её интеграция в лендинг `work.odant.org` (`docs/WORK-presentation.{html,pdf}`, `license-server/assets/ODANT-WORK-presentation.{html,pdf}`, `license-server/src/{server,landing}.js`, `license-server/assets/{landing.js,landing.css}`, `tests/license-and-deploy.test.js`):
|
|
5
|
+
- **12 широкоэкранных слайдов (16:9, `297mm × 167mm`) в 3 разделах:**
|
|
6
|
+
1. **Раздел I. Платформа и выгода для предприятия (Слайды 1–7):** отказ от «зоопарка» ИТ-систем и облачных чат-ботов в пользу единого контура ODANT-WORK (учёт двойной записи с планом счетов 01–99 и сторно, документооборот и встроенный локальный офис `.xlsx/.docx/.pptx`, структура подразделений и роли `BOSS`/`USER`, ИИ-консультант `?` в каждой форме, 13 ролевых ИИ-агентов и 26+ навыков, 4 класса локальных моделей ИИ с адаптивным управлением памятью + российские провайдеры `GigaChat 2`, `Yandex Alice AI`, `T-Bank`, `MTS AI` и двухшаговое дообучение `LoRA`, старт за 1 минуту через `npx odant-work`, миграция по OData, MCP, федерация серверов, реестровая запись СУБД Платформа Odant **№ 3505 от 03.05.2017** и реальные проекты внедрения из портфолио `infostandart.com`: ПАО «Россети», Минцифры/ГЖИ/ФКР РО, Администрация и АРС г. Рязани, РГРТУ, РязГМУ, ЮЗГУ, ГБУ «МОЦВС» Москомспорта).
|
|
7
|
+
2. **Раздел II. Комплектация и условия для клиентов (Слайды 8–9):** два формата приобретения (`WORK-PAK-495-192-4T` за **5 000 000 ₽** с сервером AMD Ryzen™ AI Max+ PRO 495 · Radeon™ 8065S · 192 ГБ Unified RAM · SSD 4 ТБ или базовая вечная лицензия `WORK-LIC-BASE-10` без оборудования за **4 000 000 ₽** — оба включают **10 вечных конкурентных мест** и 1 год поддержки Enterprise), годовые места `Start` (10 000 ₽), `Business` (15 000 ₽), `Enterprise` (20 000 ₽, реакция до 15 мин 24/7), объёмные скидки от **−5% до −25%** и пакеты внедрения «под ключ» (перенос баз 450 тыс. ₽, NAS 350 тыс. ₽, дообучение ИИ 600 тыс. ₽).
|
|
8
|
+
3. **Раздел III. Региональная франшиза для партнёров (Слайды 10–12):** защита территории (квота 1 партнёр на 1 млн жителей субъекта РФ), базовый паушальный взнос 500 000 ₽ × индекс экономики региона, моментальная бронь за залог **100 000 ₽** с рассрочкой на **12 месяцев**, рост партнёрской скидки от **10% до 50%** (до 2,5 млн ₽ маржи с одного ПАК), 6 потоков дохода франчайзи (**100% дохода партнёра** на дообучении ИИ, собственном платном региональном API моделей ИИ по подписке ARR, поставке GPU/NAS и техподдержке SLA), 4 программы сертификации (`ODANT-ADM`, `ODANT-ACC`, `ODANT-AI`, `ODANT-DEV`) и пошаговый старт на `work.odant.org`.
|
|
9
|
+
- **Интеграция в лендинг `work.odant.org`:**
|
|
10
|
+
- Встроено интерактивное модальное окно-плеер `#presentationModal` с быстрым переключением по 3 разделам (**«1. О платформе»**, **«2. Клиентам»**, **«3. Франшиза»**), навигацией стрелками клавиатуры и кнопками перехода к калькулятору клиентов и карте франшиз РФ.
|
|
11
|
+
- Кнопки **«Смотреть презентацию»** и **«Скачать PDF»** добавлены на вкладках «О платформе» (открывает со слайда 1), «Клиентам» (открывает со слайда 8), «Франшиза» (открывает со слайда 10) и в подвале сайта; прямые маршруты `/presentation` и `/assets/ODANT-WORK-presentation.pdf` отдаются сервером лицензий.
|
|
12
|
+
- [21:20, 13.11] Переработка представления **«Модели ИИ»** (`form/trainer` / `form/provider`), интеграция локального ИИ-сервера внутрь WORK с адаптивным управлением памятью и чистый минималистичный интерфейс (`MODELS/$ai/**`, `sources/modules/trainer/{local-engine,runner,index}.js`, `tests/trainer-unsloth.test.js`, `odant-work@1.0.17`):
|
|
13
|
+
- **Единое название представления — «Модели ИИ» (`carbon:chip`):**
|
|
14
|
+
- Представление на `/MODELS/Local`, на облачных провайдерах (`/MODELS/GigaChat`, `/MODELS/Yandex`, `/MODELS/T-Bank`, `/MODELS/MTS-AI`, `/MODELS/OpenAI` и др.) и на конкретных узлах моделей (`$ai`) единообразно называется **«Модели ИИ»**.
|
|
15
|
+
- При клике на конкретную модель `$ai` в дереве вместо сырого JSON-кода открывается чистая карточка модели (статус активности, тумблер Вкл/Выкл, удаление с диска для локальных моделей, параметры контекста/возможностей и быстрая проверка ответа).
|
|
16
|
+
- **Встроенный сервер локального ИИ с адаптивным управлением памятью (`local-engine.js`, `runner.js`):**
|
|
17
|
+
- Локальный ИИ-движок полностью управляется самим сервером WORK без ручного запуска в консоли: на обычных ПК (< 120 ГБ ОЗУ) запускается автоматически по запросу и выгружается из памяти через 30 минут простоя (+ кнопка **«Освободить память»** в шапке), а на серверных машинках Halo (≥ 128 ГБ ОЗУ) удерживается в памяти постоянно.
|
|
18
|
+
- **Разгруженный интерфейс «Модели ИИ» на `/MODELS/Local` в системной палитре ODA:**
|
|
19
|
+
- Убраны двухколоночные сплит-панели, разноцветный спам бейджей и консольные простыни команд; оставлены **2 спокойные вкладки**:
|
|
20
|
+
1. **«Модели»**: список установленных локальных моделей (статус, объём на диске, тумблер «Вкл» и кнопка **«Удалить»** для полной очистки модели и `#weights` с диска) + каталог моделей для установки в 1 клик с автоподбором формата под память ПК и встроенным прогресс-баром прямо внутри карточки. При выборе фильтра **«Комплект Halo (128–192 ГБ)»** отображается краткая памятка по настройке BIOS (выделение UMA Frame Buffer 128 ГБ / 96 ГБ и включение NPU) и кнопка установки комплекта GPU+NPU в 1 клик.
|
|
21
|
+
2. **«Дообучение»**: двухшаговый мастер (*Шаг 1. Собрать обучающий набор из организации → Шаг 2. Дообучить модель LoRA*).
|
|
4
22
|
- [20:55, 13.11] Архитектура провайдеров и моделей ИИ в `/MODELS`: чистота дистрибутива (исключён `odant`, пустой единый `/MODELS/Local` вместе с контуром машинок Halo), предустановленные российские провайдеры (`GigaChat`, `Yandex`, `T-Bank`, `MTS-AI`), быстрое добавление любых внешних провайдеров и цветовая индикация активности моделей в дереве (`sources/modules/trainer/{providers,halogen,huggingface,index}.js`, `MODELS/$ai/**`, `MODELS/{GigaChat,Yandex,T-Bank,MTS-AI,Local}/**`, `$server/$folder/handlers/methods/create/$handler/class.js`, `$server/$folder/lib/{node/node,icon/icon,tree/tree}.js`, `sources/server/class.js`, `.npmignore`, `odant-work@1.0.16`):
|
|
5
23
|
- **Чистота `/MODELS` в поставке и объединение `Local` + `Halogen`:**
|
|
6
24
|
- Внутренний провайдер разработчика `/MODELS/odant` исключён из npm-дистрибутива (`.npmignore`: `/MODELS/odant/`), а `defaultModel()` в `sources/modules/agent/index.js` автоматически переключается на первую доступную активную модель в `/MODELS`.
|
|
@@ -26,13 +26,19 @@ import {
|
|
|
26
26
|
computeAdaptiveDefaults,
|
|
27
27
|
detectHardware,
|
|
28
28
|
listLocalModelsAndAdapters,
|
|
29
|
+
deleteLocalModel,
|
|
29
30
|
startTrainingJob,
|
|
30
31
|
getTrainingJob,
|
|
31
32
|
listTrainingJobs,
|
|
32
33
|
stopTrainingJob,
|
|
33
34
|
publishTrainedAdapter,
|
|
34
35
|
} from './runner.js';
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
localStreamChat,
|
|
38
|
+
getLocalEngineStatus,
|
|
39
|
+
releaseLocalEngineMemory,
|
|
40
|
+
touchLocalEngineActivity,
|
|
41
|
+
} from './local-engine.js';
|
|
36
42
|
import {
|
|
37
43
|
HALOGEN_DEFAULT_PORT,
|
|
38
44
|
HALOGEN_DEFAULT_BASE_URL,
|
|
@@ -144,6 +150,10 @@ export {
|
|
|
144
150
|
scrubSecrets,
|
|
145
151
|
detectHardware,
|
|
146
152
|
listLocalModelsAndAdapters,
|
|
153
|
+
deleteLocalModel,
|
|
154
|
+
getLocalEngineStatus,
|
|
155
|
+
releaseLocalEngineMemory,
|
|
156
|
+
touchLocalEngineActivity,
|
|
147
157
|
startTrainingJob,
|
|
148
158
|
getTrainingJob,
|
|
149
159
|
listTrainingJobs,
|
|
@@ -167,8 +177,12 @@ export const TRAINER_METHODS = Object.freeze({
|
|
|
167
177
|
totalRamGb: hardware?.totalRamGb >= 120 ? hardware.totalRamGb : 192,
|
|
168
178
|
platform: hardware?.platform,
|
|
169
179
|
});
|
|
180
|
+
const runtime = getLocalEngineStatus({
|
|
181
|
+
totalRamGb: hardware?.totalRamGb,
|
|
182
|
+
});
|
|
170
183
|
return {
|
|
171
184
|
hardware,
|
|
185
|
+
runtime,
|
|
172
186
|
models: local.models,
|
|
173
187
|
adapters: local.adapters,
|
|
174
188
|
datasets,
|
|
@@ -186,6 +200,32 @@ export const TRAINER_METHODS = Object.freeze({
|
|
|
186
200
|
};
|
|
187
201
|
},
|
|
188
202
|
},
|
|
203
|
+
trainer_delete_model: {
|
|
204
|
+
level: 'admin',
|
|
205
|
+
fn: async function (p = {}) {
|
|
206
|
+
const modelId = p.modelId || p.id || p.path || '';
|
|
207
|
+
const res = await deleteLocalModel({ modelId });
|
|
208
|
+
try {
|
|
209
|
+
const localNode = await globalThis.WORK?.get_item?.('/MODELS/Local');
|
|
210
|
+
localNode?.reset?.();
|
|
211
|
+
const modelsRoot = await globalThis.WORK?.get_item?.('/MODELS');
|
|
212
|
+
modelsRoot?.reset?.();
|
|
213
|
+
}
|
|
214
|
+
catch { /* ignore */ }
|
|
215
|
+
return res;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
trainer_release_memory: {
|
|
219
|
+
level: 'admin',
|
|
220
|
+
fn: async function () {
|
|
221
|
+
const res = releaseLocalEngineMemory();
|
|
222
|
+
const hw = await detectHardware().catch(() => null);
|
|
223
|
+
return {
|
|
224
|
+
...res,
|
|
225
|
+
runtime: getLocalEngineStatus({ totalRamGb: hw?.totalRamGb }),
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
},
|
|
189
229
|
halogen_status: {
|
|
190
230
|
level: 'read',
|
|
191
231
|
fn: async function (p = {}) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* подмешивает точные факты из живой БД `work.db`), обеспечивая мгновенный отклик без внешних серверов.
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import crypto from 'node:crypto';
|
|
12
13
|
import { spawn } from 'node:child_process';
|
|
@@ -14,7 +15,7 @@ import { dataStore } from '../../data/api.js';
|
|
|
14
15
|
import { classesOfType } from '../../data/classes.js';
|
|
15
16
|
import { classSchema } from '../../data/schema.js';
|
|
16
17
|
import { trialBalance } from '../../data/reports.js';
|
|
17
|
-
import { recordHalogenSessionHandle } from './halogen.js';
|
|
18
|
+
import { recordHalogenSessionHandle, stopHalogenBackend } from './halogen.js';
|
|
18
19
|
|
|
19
20
|
const money = v => Number(v || 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
20
21
|
|
|
@@ -23,6 +24,70 @@ let _workerProc = null;
|
|
|
23
24
|
let _workerReady = false;
|
|
24
25
|
const _pendingRequests = new Map();
|
|
25
26
|
|
|
27
|
+
export const LOCAL_IDLE_TIMEOUT_MS = Number(process.env.WORK_LOCAL_AI_IDLE_MS) || (30 * 60 * 1000);
|
|
28
|
+
let _lastActivityAt = 0;
|
|
29
|
+
let _idleTimer = null;
|
|
30
|
+
|
|
31
|
+
function isHaloHighMemHost(totalRamGb) {
|
|
32
|
+
const ram = Number(totalRamGb) || (os.totalmem() / (1024 ** 3));
|
|
33
|
+
return ram >= 120;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function touchLocalEngineActivity(totalRamGb) {
|
|
37
|
+
_lastActivityAt = Date.now();
|
|
38
|
+
if (_idleTimer) {
|
|
39
|
+
clearTimeout(_idleTimer);
|
|
40
|
+
_idleTimer = null;
|
|
41
|
+
}
|
|
42
|
+
if (!isHaloHighMemHost(totalRamGb)) {
|
|
43
|
+
_idleTimer = setTimeout(() => {
|
|
44
|
+
_idleTimer = null;
|
|
45
|
+
if (_pendingRequests.size === 0) {
|
|
46
|
+
releaseLocalEngineMemory();
|
|
47
|
+
}
|
|
48
|
+
}, LOCAL_IDLE_TIMEOUT_MS);
|
|
49
|
+
_idleTimer.unref?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getLocalEngineStatus({ totalRamGb, halogenOnline = false } = {}) {
|
|
54
|
+
const highMem = isHaloHighMemHost(totalRamGb);
|
|
55
|
+
const workerAlive = Boolean(_workerProc && !_workerProc.killed);
|
|
56
|
+
const running = workerAlive || Boolean(halogenOnline);
|
|
57
|
+
const idleTimeoutMin = Math.round(LOCAL_IDLE_TIMEOUT_MS / 60000);
|
|
58
|
+
return {
|
|
59
|
+
running,
|
|
60
|
+
workerAlive,
|
|
61
|
+
halogenOnline: Boolean(halogenOnline),
|
|
62
|
+
mode: highMem ? 'always-on' : 'on-demand',
|
|
63
|
+
modeLabel: highMem
|
|
64
|
+
? (running ? 'В памяти постоянно (Halo 128–192 ГБ)' : 'Готов к запуску (Halo 128–192 ГБ)')
|
|
65
|
+
: (running ? `В памяти (автовыгрузка через ${idleTimeoutMin} мин простоя)` : `Автозапуск по запросу (выгрузка через ${idleTimeoutMin} мин)`),
|
|
66
|
+
idleTimeoutMin: highMem ? 0 : idleTimeoutMin,
|
|
67
|
+
lastActivityAt: _lastActivityAt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function releaseLocalEngineMemory() {
|
|
72
|
+
if (_idleTimer) {
|
|
73
|
+
clearTimeout(_idleTimer);
|
|
74
|
+
_idleTimer = null;
|
|
75
|
+
}
|
|
76
|
+
closeInferWorker();
|
|
77
|
+
_tokenizerCache.clear();
|
|
78
|
+
let haloStopped = false;
|
|
79
|
+
try {
|
|
80
|
+
const res = stopHalogenBackend();
|
|
81
|
+
haloStopped = Boolean(res?.stopped);
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
released: true,
|
|
87
|
+
haloStopped,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
26
91
|
function resolveWeightsDir(ai, rootDir = process.cwd()) {
|
|
27
92
|
const modelId = String(ai?.id || ai?.label || ai?.model || 'Local-Model').split('/').pop();
|
|
28
93
|
const weightsRel = ai?.weightsDir || ai?.DATA?.weightsDir || `MODELS/Local/${modelId}/#weights`;
|
|
@@ -216,6 +281,7 @@ export function closeInferWorker() {
|
|
|
216
281
|
* Потоковая генерация ответа локальной модели (`streamChat` для `/MODELS/Local/*`).
|
|
217
282
|
*/
|
|
218
283
|
export async function* localStreamChat(ai, body = {}, options = {}) {
|
|
284
|
+
touchLocalEngineActivity();
|
|
219
285
|
const weightsDir = resolveWeightsDir(ai);
|
|
220
286
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
221
287
|
const turns = extractConversationTurns(messages);
|
|
@@ -230,22 +230,68 @@ export async function listLocalModelsAndAdapters(rootDir = process.cwd()) {
|
|
|
230
230
|
const aiClassFile = path.join(localRoot, e.name, '$ai', 'class.js');
|
|
231
231
|
const weightsDir = path.join(localRoot, e.name, '#weights');
|
|
232
232
|
let weightFiles = [];
|
|
233
|
+
let sizeBytes = 0;
|
|
233
234
|
if (fs.existsSync(weightsDir)) {
|
|
234
235
|
const wf = await fsp.readdir(weightsDir).catch(() => []);
|
|
235
|
-
weightFiles = wf.filter(f => !f.endsWith('.part'));
|
|
236
|
+
weightFiles = wf.filter(f => !f.endsWith('.part') && f !== 'hf-manifest.json');
|
|
237
|
+
for (const f of weightFiles) {
|
|
238
|
+
try {
|
|
239
|
+
const st = await fsp.stat(path.join(weightsDir, f));
|
|
240
|
+
sizeBytes += st.size || 0;
|
|
241
|
+
}
|
|
242
|
+
catch { /* ignore */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let meta = {};
|
|
246
|
+
if (fs.existsSync(aiClassFile)) {
|
|
247
|
+
try {
|
|
248
|
+
const raw = await fsp.readFile(aiClassFile, 'utf-8');
|
|
249
|
+
const jsonPart = raw.replace(/^[^{]*/, '').replace(/;\s*$/, '');
|
|
250
|
+
meta = JSON.parse(jsonPart);
|
|
251
|
+
}
|
|
252
|
+
catch { /* non-json class.js */ }
|
|
236
253
|
}
|
|
237
254
|
if (fs.existsSync(aiClassFile) || weightFiles.length) {
|
|
255
|
+
const enabled = meta.enabled !== false;
|
|
256
|
+
const hasWeights = weightFiles.length > 0;
|
|
238
257
|
models.push({
|
|
239
258
|
id: e.name,
|
|
259
|
+
label: meta.label || e.name,
|
|
260
|
+
icon: meta.icon || 'ai:ollama',
|
|
261
|
+
modality: meta.modality || 'llm',
|
|
262
|
+
model: meta.model || meta.hfRepo || e.name,
|
|
263
|
+
capabilities: Array.isArray(meta.capabilities) ? meta.capabilities : ['chat', 'stream'],
|
|
264
|
+
maxTokens: meta.maxTokens || 16384,
|
|
265
|
+
enabled,
|
|
266
|
+
active: enabled && hasWeights,
|
|
240
267
|
path: `/MODELS/Local/${e.name}`,
|
|
241
268
|
hasClass: fs.existsSync(aiClassFile),
|
|
242
269
|
weightFiles,
|
|
270
|
+
sizeGb: +(sizeBytes / (1024 ** 3)).toFixed(2),
|
|
243
271
|
});
|
|
244
272
|
}
|
|
245
273
|
}
|
|
246
274
|
return { models, adapters };
|
|
247
275
|
}
|
|
248
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Полностью удалить локальную модель `/MODELS/Local/<modelId>` вместе с её весами `#weights` и узлом `$ai`.
|
|
279
|
+
*/
|
|
280
|
+
export async function deleteLocalModel({ rootDir = process.cwd(), modelId } = {}) {
|
|
281
|
+
const cleanId = String(modelId || '').replace(/^\/?MODELS\/Local\//i, '').replace(/[<>:"/\\|?*]/g, '').trim();
|
|
282
|
+
if (!cleanId || cleanId.startsWith('$') || cleanId.startsWith('.'))
|
|
283
|
+
throw new Error('Некорректное имя удаляемой модели: ' + modelId);
|
|
284
|
+
const targetDir = path.join(rootDir, 'MODELS', 'Local', cleanId);
|
|
285
|
+
const localRoot = path.join(rootDir, 'MODELS', 'Local');
|
|
286
|
+
const rel = path.relative(localRoot, targetDir);
|
|
287
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
288
|
+
throw new Error('Запрещённый путь удаления: ' + targetDir);
|
|
289
|
+
if (fs.existsSync(targetDir)) {
|
|
290
|
+
await fsp.rm(targetDir, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
return { ok: true, deletedId: cleanId, deletedPath: `/MODELS/Local/${cleanId}` };
|
|
293
|
+
}
|
|
294
|
+
|
|
249
295
|
/**
|
|
250
296
|
* Запустить задачу дообучения Unsloth (`mode: 'sft' | 'grpo'`), адаптируя параметры по умолчанию под реальное железо.
|
|
251
297
|
*/
|