groove-dev 0.27.124 → 0.27.125
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +122 -0
- package/node_modules/@groove-dev/daemon/src/preview.js +28 -5
- package/node_modules/@groove-dev/daemon/src/process.js +21 -0
- package/node_modules/@groove-dev/daemon/src/providers/local.js +19 -20
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +66 -3
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
- package/node_modules/@groove-dev/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +169 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +8 -10
- package/node_modules/@groove-dev/gui/src/views/models.jsx +507 -236
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +122 -0
- package/packages/daemon/src/preview.js +28 -5
- package/packages/daemon/src/process.js +21 -0
- package/packages/daemon/src/providers/local.js +19 -20
- package/packages/daemon/src/providers/ollama.js +66 -3
- package/packages/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
- package/packages/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/packages/gui/src/stores/groove.js +169 -0
- package/packages/gui/src/views/agents.jsx +8 -10
- package/packages/gui/src/views/models.jsx +507 -236
|
@@ -493,6 +493,8 @@ export function createApi(app, daemon) {
|
|
|
493
493
|
|
|
494
494
|
// --- Ollama ---
|
|
495
495
|
|
|
496
|
+
const isValidModelId = (id) => typeof id === 'string' && id.length > 0 && id.length < 200 && /^[a-zA-Z0-9._:/-]+$/.test(id);
|
|
497
|
+
|
|
496
498
|
app.get('/api/providers/ollama/hardware', (req, res) => {
|
|
497
499
|
res.json(OllamaProvider.getSystemHardware());
|
|
498
500
|
});
|
|
@@ -507,6 +509,7 @@ export function createApi(app, daemon) {
|
|
|
507
509
|
app.post('/api/providers/ollama/pull', async (req, res) => {
|
|
508
510
|
const { model } = req.body;
|
|
509
511
|
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
512
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
510
513
|
if (!OllamaProvider.isInstalled()) {
|
|
511
514
|
const install = OllamaProvider.installCommand();
|
|
512
515
|
return res.status(400).json({ error: `Ollama is not installed. Install with: ${install.command}` });
|
|
@@ -541,6 +544,7 @@ export function createApi(app, daemon) {
|
|
|
541
544
|
});
|
|
542
545
|
|
|
543
546
|
app.delete('/api/providers/ollama/models/:model', (req, res) => {
|
|
547
|
+
if (!isValidModelId(req.params.model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
544
548
|
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
545
549
|
const success = OllamaProvider.deleteModel(req.params.model);
|
|
546
550
|
if (success) {
|
|
@@ -604,6 +608,65 @@ export function createApi(app, daemon) {
|
|
|
604
608
|
}
|
|
605
609
|
});
|
|
606
610
|
|
|
611
|
+
app.get('/api/providers/ollama/running', async (req, res) => {
|
|
612
|
+
if (!OllamaProvider.isInstalled()) return res.json({ models: [] });
|
|
613
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
614
|
+
if (!serverRunning) return res.json({ models: [] });
|
|
615
|
+
try {
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
618
|
+
const apiRes = await fetch('http://localhost:11434/api/ps', { signal: controller.signal });
|
|
619
|
+
clearTimeout(timeout);
|
|
620
|
+
if (!apiRes.ok) return res.json({ models: [] });
|
|
621
|
+
const data = await apiRes.json();
|
|
622
|
+
const models = (data.models || []).map((m) => ({
|
|
623
|
+
name: m.name || m.model || '',
|
|
624
|
+
size: m.size || 0,
|
|
625
|
+
vram: m.size_vram ?? m.size ?? 0,
|
|
626
|
+
expires: m.expires_at || null,
|
|
627
|
+
}));
|
|
628
|
+
res.json({ models });
|
|
629
|
+
} catch {
|
|
630
|
+
res.json({ models: OllamaProvider.getRunningModels() });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
app.post('/api/providers/ollama/load', async (req, res) => {
|
|
635
|
+
const { model } = req.body;
|
|
636
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
637
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
638
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
639
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
640
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
641
|
+
try {
|
|
642
|
+
await OllamaProvider.loadModel(model);
|
|
643
|
+
daemon.broadcast({ type: 'ollama:model:loaded', model });
|
|
644
|
+
daemon.audit.log('ollama.model.load', { model });
|
|
645
|
+
res.json({ ok: true, model });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
daemon.broadcast({ type: 'model:error', model, error: err.message });
|
|
648
|
+
res.status(500).json({ error: `Failed to load model: ${err.message}` });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
app.post('/api/providers/ollama/unload', async (req, res) => {
|
|
653
|
+
const { model } = req.body;
|
|
654
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
655
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
656
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
657
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
658
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
659
|
+
try {
|
|
660
|
+
await OllamaProvider.unloadModel(model);
|
|
661
|
+
daemon.broadcast({ type: 'ollama:model:unloaded', model });
|
|
662
|
+
daemon.audit.log('ollama.model.unload', { model });
|
|
663
|
+
res.json({ ok: true });
|
|
664
|
+
} catch (err) {
|
|
665
|
+
daemon.broadcast({ type: 'model:error', model, error: err.message });
|
|
666
|
+
res.status(500).json({ error: `Failed to unload model: ${err.message}` });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
607
670
|
// --- Provider Management (install, login, set-path, verify) ---
|
|
608
671
|
|
|
609
672
|
const MANAGEABLE_PROVIDERS = new Set(['claude-code', 'codex', 'gemini']);
|
|
@@ -967,6 +1030,59 @@ export function createApi(app, daemon) {
|
|
|
967
1030
|
res.json({ models: recommended, hardware });
|
|
968
1031
|
});
|
|
969
1032
|
|
|
1033
|
+
// --- Ollama Running Models ---
|
|
1034
|
+
|
|
1035
|
+
app.get('/api/models/status', async (req, res) => {
|
|
1036
|
+
const installed = OllamaProvider.isInstalled();
|
|
1037
|
+
if (!installed) return res.json({ serverRunning: false, runningModels: [], installedModels: [], hardware: OllamaProvider.getSystemHardware() });
|
|
1038
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
1039
|
+
const runningModels = serverRunning ? OllamaProvider.getRunningModels() : [];
|
|
1040
|
+
const installedModels = OllamaProvider.getInstalledModels();
|
|
1041
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
1042
|
+
res.json({ serverRunning, runningModels, installedModels, hardware });
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
app.get('/api/models/running', async (req, res) => {
|
|
1046
|
+
if (!OllamaProvider.isInstalled()) return res.json([]);
|
|
1047
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
1048
|
+
if (!serverRunning) return res.json([]);
|
|
1049
|
+
res.json(OllamaProvider.getRunningModels());
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
app.post('/api/models/:id/load', async (req, res) => {
|
|
1053
|
+
const modelId = req.params.id;
|
|
1054
|
+
if (!modelId) return res.status(400).json({ error: 'model id is required' });
|
|
1055
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
1056
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
1057
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await OllamaProvider.loadModel(modelId);
|
|
1060
|
+
daemon.broadcast({ type: 'model:loaded', model: modelId });
|
|
1061
|
+
daemon.audit.log('model.load', { model: modelId });
|
|
1062
|
+
res.json(result);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
|
|
1065
|
+
res.status(500).json({ error: `Failed to load model: ${err.message}` });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
app.post('/api/models/:id/unload', async (req, res) => {
|
|
1070
|
+
const modelId = req.params.id;
|
|
1071
|
+
if (!modelId) return res.status(400).json({ error: 'model id is required' });
|
|
1072
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
1073
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
1074
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
1075
|
+
try {
|
|
1076
|
+
const result = await OllamaProvider.unloadModel(modelId);
|
|
1077
|
+
daemon.broadcast({ type: 'model:unloaded', model: modelId });
|
|
1078
|
+
daemon.audit.log('model.unload', { model: modelId });
|
|
1079
|
+
res.json(result);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
|
|
1082
|
+
res.status(500).json({ error: `Failed to unload model: ${err.message}` });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
970
1086
|
app.get('/api/llama/status', (req, res) => {
|
|
971
1087
|
res.json(daemon.llamaServer.getStatus());
|
|
972
1088
|
});
|
|
@@ -3916,8 +4032,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3916
4032
|
// JS imports: from '/' and import('/')
|
|
3917
4033
|
out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3918
4034
|
out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
4035
|
+
// JS bare import: import '/path'
|
|
4036
|
+
out = out.replace(/(import\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3919
4037
|
// CSS url()
|
|
3920
4038
|
out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
4039
|
+
// CSS @import '/path'
|
|
4040
|
+
out = out.replace(/(@import\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
4041
|
+
// Vite base assignments: globalThis.__vite_base = "/" or window.__vite_base = "/"
|
|
4042
|
+
out = out.replace(/((?:globalThis|window)\.__vite_base\s*=\s*(["']))\/(?=["'])/g, `$1${proxyBase}/`);
|
|
3921
4043
|
return out;
|
|
3922
4044
|
}
|
|
3923
4045
|
|
|
@@ -131,7 +131,13 @@ export class PreviewService {
|
|
|
131
131
|
result = await this._launchStatic(teamId, baseDir, preview);
|
|
132
132
|
}
|
|
133
133
|
} else {
|
|
134
|
-
|
|
134
|
+
const distDir = resolve(baseDir, 'dist');
|
|
135
|
+
const openFile = (preview.openPath || 'index.html').replace(/^\/+/, '');
|
|
136
|
+
if (existsSync(resolve(distDir, openFile))) {
|
|
137
|
+
result = await this._launchStatic(teamId, distDir, { ...preview, openPath: openFile });
|
|
138
|
+
} else {
|
|
139
|
+
result = await this._launchStatic(teamId, baseDir, preview);
|
|
140
|
+
}
|
|
135
141
|
}
|
|
136
142
|
} else if (preview.kind === 'dev-server') {
|
|
137
143
|
if (this._needsPreBuild(baseDir)) {
|
|
@@ -250,14 +256,31 @@ export class PreviewService {
|
|
|
250
256
|
_runBuild(teamId, baseDir) {
|
|
251
257
|
const pkgPath = resolve(baseDir, 'package.json');
|
|
252
258
|
if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
|
|
259
|
+
let pkg;
|
|
253
260
|
try {
|
|
254
|
-
|
|
261
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
255
262
|
if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
|
|
256
263
|
} catch { return { failed: true, reason: 'malformed package.json' }; }
|
|
264
|
+
|
|
265
|
+
const isVite = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
266
|
+
.some((f) => existsSync(resolve(baseDir, f)));
|
|
267
|
+
|
|
268
|
+
let command = 'npm run build';
|
|
269
|
+
const buildScript = (pkg.scripts.build || '').trim();
|
|
270
|
+
|
|
271
|
+
if (isVite && /^(tsc\s*&&\s*)?vite\s+build\s*$/.test(buildScript)) {
|
|
272
|
+
command = `npm run build -- --base=./`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const env = { ...process.env };
|
|
276
|
+
if (isVite && command === 'npm run build') {
|
|
277
|
+
env.VITE_BASE = './';
|
|
278
|
+
}
|
|
279
|
+
|
|
257
280
|
try {
|
|
258
|
-
console.log(`[Groove:Preview] Running
|
|
259
|
-
this.daemon.audit?.log('preview.build', { teamId, baseDir });
|
|
260
|
-
execSync(
|
|
281
|
+
console.log(`[Groove:Preview] Running ${command} in ${baseDir}`);
|
|
282
|
+
this.daemon.audit?.log('preview.build', { teamId, baseDir, command });
|
|
283
|
+
execSync(command, { cwd: baseDir, timeout: 120_000, stdio: 'pipe', env });
|
|
261
284
|
return null;
|
|
262
285
|
} catch (err) {
|
|
263
286
|
return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
|
|
@@ -6,6 +6,8 @@ import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, writ
|
|
|
6
6
|
import { resolve, dirname, isAbsolute } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { getProvider, getInstalledProviders, resolveProviderCommand } from './providers/index.js';
|
|
9
|
+
import { LocalProvider } from './providers/local.js';
|
|
10
|
+
import { OllamaProvider } from './providers/ollama.js';
|
|
9
11
|
import { AgentLoop } from './agent-loop.js';
|
|
10
12
|
import { validateAgentConfig } from './validate.js';
|
|
11
13
|
|
|
@@ -735,6 +737,25 @@ export class ProcessManager {
|
|
|
735
737
|
);
|
|
736
738
|
}
|
|
737
739
|
|
|
740
|
+
// Pre-flight for local model providers: ensure Ollama server is running and model is installed
|
|
741
|
+
if (providerName === 'local' || providerName === 'ollama') {
|
|
742
|
+
try {
|
|
743
|
+
await LocalProvider.ensureServerRunning();
|
|
744
|
+
} catch (err) {
|
|
745
|
+
const agent = registry.add({ ...config, provider: providerName, status: 'error' });
|
|
746
|
+
registry.update(agent.id, { status: 'error', error: 'Ollama server failed to start' });
|
|
747
|
+
this.daemon.broadcast({ type: 'model:error', agentId: agent.id, error: err.message });
|
|
748
|
+
throw new Error('Ollama server failed to start: ' + err.message);
|
|
749
|
+
}
|
|
750
|
+
if (config.model && config.model !== 'auto') {
|
|
751
|
+
const installed = OllamaProvider.getInstalledModels();
|
|
752
|
+
const modelInstalled = installed.some((m) => m.id === config.model || config.model.startsWith(m.id.split(':')[0]));
|
|
753
|
+
if (!modelInstalled) {
|
|
754
|
+
throw new Error(`Model '${config.model}' is not installed. Pull it first with: ollama pull ${config.model}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
738
759
|
// Validate explicit model against provider's supported models
|
|
739
760
|
if (config.model && config.model !== 'auto' && provider.constructor.models) {
|
|
740
761
|
const valid = provider.constructor.models.some(m => m.id === config.model);
|
|
@@ -59,27 +59,13 @@ export class LocalProvider extends Provider {
|
|
|
59
59
|
|
|
60
60
|
// Only return models that are actually installed and ready to use
|
|
61
61
|
static get models() {
|
|
62
|
-
|
|
62
|
+
if (!LocalProvider._hasOllama()) return [];
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
installed.push({
|
|
70
|
-
id: m.id, name: m.name || m.id,
|
|
71
|
-
tier: m.tier || 'medium', category: m.category || 'general',
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
} catch { /* Ollama not running */ }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// If nothing installed, show a hint instead of a blank list
|
|
78
|
-
if (installed.length === 0) {
|
|
79
|
-
return [{ id: '_none', name: 'No models installed — pull one with: ollama pull qwen2.5-coder:7b', tier: 'medium', disabled: true }];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return installed;
|
|
64
|
+
const ollamaModels = OllamaProvider.getInstalledModels();
|
|
65
|
+
return ollamaModels.map((m) => ({
|
|
66
|
+
id: m.id, name: m.name || m.id,
|
|
67
|
+
tier: m.tier || 'medium', category: m.category || 'general',
|
|
68
|
+
}));
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
// Full catalog for the Models browser (includes uninstalled)
|
|
@@ -123,6 +109,19 @@ export class LocalProvider extends Provider {
|
|
|
123
109
|
return OllamaProvider.getInstalledModels();
|
|
124
110
|
}
|
|
125
111
|
|
|
112
|
+
static async ensureServerRunning() {
|
|
113
|
+
if (!LocalProvider._hasOllama()) {
|
|
114
|
+
throw new Error('Ollama binary not found. Install with: ' + OllamaProvider.installCommand().command);
|
|
115
|
+
}
|
|
116
|
+
if (await OllamaProvider.isServerRunning()) return true;
|
|
117
|
+
OllamaProvider.startServer();
|
|
118
|
+
for (let i = 0; i < 20; i++) {
|
|
119
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
120
|
+
if (await OllamaProvider.isServerRunning()) return true;
|
|
121
|
+
}
|
|
122
|
+
throw new Error('Ollama server failed to start within 10s');
|
|
123
|
+
}
|
|
124
|
+
|
|
126
125
|
/**
|
|
127
126
|
* Get configuration for the agent loop runtime.
|
|
128
127
|
* Called by ProcessManager when useAgentLoop is true.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// GROOVE — Ollama Provider (Local Models)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
import { execFile } from 'child_process';
|
|
4
|
+
import { execSync, execFile, execFileSync } from 'child_process';
|
|
6
5
|
import os from 'os';
|
|
7
6
|
import { Provider } from './base.js';
|
|
8
7
|
|
|
@@ -246,9 +245,73 @@ export class OllamaProvider extends Provider {
|
|
|
246
245
|
});
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
static getRunningModels() {
|
|
249
|
+
try {
|
|
250
|
+
const output = execSync('ollama ps', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
251
|
+
const lines = output.split('\n').slice(1).filter(Boolean);
|
|
252
|
+
return lines.map((line) => {
|
|
253
|
+
const parts = line.split(/\s+/);
|
|
254
|
+
return {
|
|
255
|
+
id: parts[0] || '',
|
|
256
|
+
name: parts[0] || '',
|
|
257
|
+
size: parts[1] || '',
|
|
258
|
+
vram: parts[2] || '',
|
|
259
|
+
processor: parts[3] || '',
|
|
260
|
+
until: parts.slice(4).join(' ') || '',
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
} catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static async loadModel(modelId) {
|
|
269
|
+
const controller = new AbortController();
|
|
270
|
+
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({ model: modelId, prompt: '', keep_alive: '10m' }),
|
|
276
|
+
signal: controller.signal,
|
|
277
|
+
});
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
const text = await res.text();
|
|
281
|
+
throw new Error(`Ollama API ${res.status}: ${text.slice(0, 200)}`);
|
|
282
|
+
}
|
|
283
|
+
return { loaded: true, model: modelId };
|
|
284
|
+
} catch (err) {
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
static async unloadModel(modelId) {
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
293
|
+
try {
|
|
294
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ model: modelId, prompt: '', keep_alive: 0 }),
|
|
298
|
+
signal: controller.signal,
|
|
299
|
+
});
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const text = await res.text();
|
|
303
|
+
throw new Error(`Ollama API ${res.status}: ${text.slice(0, 200)}`);
|
|
304
|
+
}
|
|
305
|
+
return { unloaded: true, model: modelId };
|
|
306
|
+
} catch (err) {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
249
312
|
static deleteModel(modelId) {
|
|
250
313
|
try {
|
|
251
|
-
|
|
314
|
+
execFileSync('ollama', ['rm', modelId], { encoding: 'utf8', timeout: 30000 });
|
|
252
315
|
return true;
|
|
253
316
|
} catch {
|
|
254
317
|
return false;
|