Ferrell Synthetic Intelligence commited on
Commit
4407241
·
1 Parent(s): 05d169b

Add managed local model runtime

Browse files
app.js CHANGED
@@ -159,6 +159,22 @@ async function testRuntime() {
159
  }
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  function sendChat() {
163
  const input = $('#input');
164
  const value = input.value.trim();
@@ -237,7 +253,7 @@ async function boot() {
237
  openFile('agent.ts');
238
  document.querySelectorAll('[data-file]').forEach(button => button.onclick = () => openFile(button.dataset.file));
239
  $('#review-button').onclick = runReview;
240
- $('#connection-button').onclick = testRuntime;
241
  $('#send-button').onclick = sendChat;
242
  $('#node-button').onclick = () => { localNodeId(); renderNode(); appendLog('NODE', 'Local identity created. No network connection or private data was shared.'); };
243
  $('#case-button').onclick = () => {
 
159
  }
160
  }
161
 
162
+ async function startRuntime() {
163
+ const model = state.selected;
164
+ if (!model) return appendLog('RUNTIME', 'Select a model pack first.', 'warning');
165
+ try {
166
+ const response = await fetch('http://127.0.0.1:4777/api/models/start', {
167
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: model.id })
168
+ });
169
+ const result = await response.json();
170
+ if (!response.ok) throw new Error(result.error || 'runtime start failed');
171
+ appendLog('RUNTIME', `${model.name} is starting at ${result.endpoint}.`);
172
+ setTimeout(testRuntime, 1500);
173
+ } catch (error) {
174
+ appendLog('RUNTIME', `Local daemon unavailable: ${error.message}. Start daemon/server.mjs first.`, 'warning');
175
+ }
176
+ }
177
+
178
  function sendChat() {
179
  const input = $('#input');
180
  const value = input.value.trim();
 
253
  openFile('agent.ts');
254
  document.querySelectorAll('[data-file]').forEach(button => button.onclick = () => openFile(button.dataset.file));
255
  $('#review-button').onclick = runReview;
256
+ $('#connection-button').onclick = startRuntime;
257
  $('#send-button').onclick = sendChat;
258
  $('#node-button').onclick = () => { localNodeId(); renderNode(); appendLog('NODE', 'Local identity created. No network connection or private data was shared.'); };
259
  $('#case-button').onclick = () => {
daemon/model-manager.mjs ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ export class ModelManager {
6
+ constructor({ manifestPath, modelDir, binaryPath, spawnProcess = spawn } = {}) {
7
+ this.manifestPath = manifestPath;
8
+ this.modelDir = path.resolve(modelDir);
9
+ this.binaryPath = binaryPath;
10
+ this.spawnProcess = spawnProcess;
11
+ this.models = new Map();
12
+ this.processes = new Map();
13
+ }
14
+
15
+ async load() {
16
+ const manifest = JSON.parse(await fs.readFile(this.manifestPath, 'utf8'));
17
+ this.models = new Map(manifest.models.map(model => [model.id, model]));
18
+ return manifest;
19
+ }
20
+
21
+ status() {
22
+ return [...this.models.values()].map(model => ({
23
+ id: model.id,
24
+ name: model.name,
25
+ status: this.processes.has(model.id) ? 'running' : model.status,
26
+ endpoint: model.endpoint
27
+ }));
28
+ }
29
+
30
+ async start(id) {
31
+ const model = this.models.get(id);
32
+ if (!model) throw new Error('model is not allowlisted');
33
+ if (this.processes.has(id)) return { id, status: 'running', endpoint: model.endpoint };
34
+ if (!this.binaryPath) throw new Error('llama-server binary is not configured');
35
+ await fs.access(this.binaryPath).catch(() => { throw new Error('llama-server binary is unavailable'); });
36
+ const file = path.resolve(this.modelDir, path.basename(model.artifact_uri.replace('local://', '')));
37
+ if (!file.startsWith(`${this.modelDir}${path.sep}`)) throw new Error('model path escaped model directory');
38
+ await fs.access(file);
39
+ await this.stopAll();
40
+ const endpoint = new URL(model.endpoint);
41
+ const args = ['-m', file, '--host', '127.0.0.1', '--port', String(endpoint.port || 8080), '--ctx-size', String(model.context_tokens || 2048), '--threads', '4', '--parallel', '1', '--log-disable'];
42
+ const child = this.spawnProcess(this.binaryPath, args, { stdio: 'ignore' });
43
+ this.processes.set(id, child);
44
+ child.once('exit', () => this.processes.delete(id));
45
+ return { id, status: 'starting', endpoint: model.endpoint };
46
+ }
47
+
48
+ async stop(id) {
49
+ const child = this.processes.get(id);
50
+ if (!child) return { id, status: 'stopped' };
51
+ child.kill('SIGTERM');
52
+ this.processes.delete(id);
53
+ return { id, status: 'stopped' };
54
+ }
55
+
56
+ async stopAll() {
57
+ for (const id of [...this.processes.keys()]) await this.stop(id);
58
+ }
59
+ }
daemon/server.mjs CHANGED
@@ -2,10 +2,15 @@ import http from 'node:http';
2
  import { execFile } from 'node:child_process';
3
  import { promises as fs } from 'node:fs';
4
  import path from 'node:path';
 
5
 
6
  const HOST = '127.0.0.1';
7
  const PORT = Number(process.env.AIDE_DAEMON_PORT || 4777);
8
  const WORKSPACE = path.resolve(process.env.AIDE_WORKSPACE || process.cwd());
 
 
 
 
9
 
10
  function json(response, status, body) {
11
  const payload = JSON.stringify(body);
@@ -13,12 +18,21 @@ function json(response, status, body) {
13
  'Content-Type': 'application/json; charset=utf-8',
14
  'Content-Length': Buffer.byteLength(payload),
15
  'Access-Control-Allow-Origin': 'http://127.0.0.1:4173',
16
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
17
  'Access-Control-Allow-Headers': 'Content-Type'
18
  });
19
  response.end(payload);
20
  }
21
 
 
 
 
 
 
 
 
 
 
22
  function runGit(args) {
23
  return new Promise((resolve, reject) => {
24
  execFile('git', args, { cwd: WORKSPACE, timeout: 5000, maxBuffer: 256 * 1024 }, (error, stdout, stderr) => {
@@ -52,6 +66,16 @@ const server = http.createServer(async (request, response) => {
52
  return json(response, 200, { workspace: WORKSPACE, status: '', unavailable: error.message });
53
  }
54
  }
 
 
 
 
 
 
 
 
 
 
55
  return json(response, 404, { error: 'not found' });
56
  } catch (error) {
57
  return json(response, 500, { error: 'local daemon error' });
 
2
  import { execFile } from 'node:child_process';
3
  import { promises as fs } from 'node:fs';
4
  import path from 'node:path';
5
+ import { ModelManager } from './model-manager.mjs';
6
 
7
  const HOST = '127.0.0.1';
8
  const PORT = Number(process.env.AIDE_DAEMON_PORT || 4777);
9
  const WORKSPACE = path.resolve(process.env.AIDE_WORKSPACE || process.cwd());
10
+ const MODEL_DIR = path.resolve(process.env.AIDE_MODEL_DIR || path.join(WORKSPACE, 'models'));
11
+ const MANIFEST = path.join(WORKSPACE, 'models', 'manifest.json');
12
+ const modelManager = new ModelManager({ manifestPath: MANIFEST, modelDir: MODEL_DIR, binaryPath: process.env.AIDE_LLAMA_SERVER || '/root/runtime/llama-b10333/llama-server' });
13
+ await modelManager.load().catch(() => {});
14
 
15
  function json(response, status, body) {
16
  const payload = JSON.stringify(body);
 
18
  'Content-Type': 'application/json; charset=utf-8',
19
  'Content-Length': Buffer.byteLength(payload),
20
  'Access-Control-Allow-Origin': 'http://127.0.0.1:4173',
21
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
22
  'Access-Control-Allow-Headers': 'Content-Type'
23
  });
24
  response.end(payload);
25
  }
26
 
27
+ async function body(request) {
28
+ let data = '';
29
+ for await (const chunk of request) {
30
+ data += chunk;
31
+ if (Buffer.byteLength(data) > 16 * 1024) throw new Error('request too large');
32
+ }
33
+ return data ? JSON.parse(data) : {};
34
+ }
35
+
36
  function runGit(args) {
37
  return new Promise((resolve, reject) => {
38
  execFile('git', args, { cwd: WORKSPACE, timeout: 5000, maxBuffer: 256 * 1024 }, (error, stdout, stderr) => {
 
66
  return json(response, 200, { workspace: WORKSPACE, status: '', unavailable: error.message });
67
  }
68
  }
69
+ if (request.method === 'GET' && request.url === '/api/models/status') {
70
+ return json(response, 200, { models: modelManager.status() });
71
+ }
72
+ if (request.method === 'POST' && request.url === '/api/models/start') {
73
+ return json(response, 200, await modelManager.start((await body(request)).id));
74
+ }
75
+ if (request.method === 'POST' && request.url === '/api/models/stop') {
76
+ const id = (await body(request)).id;
77
+ return json(response, 200, id ? await modelManager.stop(id) : (await modelManager.stopAll(), { status: 'stopped' }));
78
+ }
79
  return json(response, 404, { error: 'not found' });
80
  } catch (error) {
81
  return json(response, 500, { error: 'local daemon error' });
daemon/test-model-manager.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { ModelManager } from './model-manager.mjs';
6
+
7
+ const root = await mkdtemp(path.join(tmpdir(), 'aide-manager-'));
8
+ await mkdir(path.join(root, 'models'));
9
+ await writeFile(path.join(root, 'models', 'safe.gguf'), 'test');
10
+ await writeFile(path.join(root, 'models', 'manifest.json'), JSON.stringify({ models: [{ id: 'safe', name: 'Safe', status: 'pending', endpoint: 'http://127.0.0.1:9001/v1', artifact_uri: 'local://safe.gguf', context_tokens: 2048 }] }));
11
+ const manager = new ModelManager({ manifestPath: path.join(root, 'models', 'manifest.json'), modelDir: path.join(root, 'models'), binaryPath: '/missing' });
12
+ await manager.load();
13
+ assert.equal(manager.status()[0].status, 'pending');
14
+ await assert.rejects(manager.start('unknown'), /not allowlisted/);
15
+ await assert.rejects(manager.start('safe'), /llama-server binary/);
16
+ console.log('model manager test passed');
index.html CHANGED
@@ -79,7 +79,7 @@
79
  <div class="lane-grid" id="lane-grid"></div>
80
  <div class="runtime-row"><span class="dot green"></span><span>Local adapter</span><b id="runtime-name">OpenAI-compatible HTTP</b></div>
81
  <div id="collab-log" class="collab-log"><div class="empty-state">Cross-chat is idle.<br>Start a bounded review to create a research, build, and verify run.</div></div>
82
- <div class="agent-actions"><button id="review-button" class="primary">START BOUNDED REVIEW</button><button id="connection-button" class="secondary">TEST LOCAL RUNTIME</button></div>
83
  <div class="chat-row"><input id="input" placeholder="Ask the local lanes..." aria-label="Ask the local lanes"><button id="send-button">SEND</button></div>
84
  <div id="chat" class="chat" aria-live="polite"></div>
85
  </aside>
 
79
  <div class="lane-grid" id="lane-grid"></div>
80
  <div class="runtime-row"><span class="dot green"></span><span>Local adapter</span><b id="runtime-name">OpenAI-compatible HTTP</b></div>
81
  <div id="collab-log" class="collab-log"><div class="empty-state">Cross-chat is idle.<br>Start a bounded review to create a research, build, and verify run.</div></div>
82
+ <div class="agent-actions"><button id="review-button" class="primary">START BOUNDED REVIEW</button><button id="connection-button" class="secondary">START / TEST RUNTIME</button></div>
83
  <div class="chat-row"><input id="input" placeholder="Ask the local lanes..." aria-label="Ask the local lanes"><button id="send-button">SEND</button></div>
84
  <div id="chat" class="chat" aria-live="polite"></div>
85
  </aside>
package.json CHANGED
@@ -5,7 +5,7 @@
5
  "description": "Local-first sovereign development workbench",
6
  "type": "module",
7
  "scripts": {
8
- "test": "node tests/smoke.mjs && node harness/test-orchestrator.mjs",
9
  "check": "node --check app.js && node --check daemon/server.mjs && node --check harness/orchestrator.mjs && node --check harness/checks.mjs",
10
  "veritas": "node harness/run-veritas.mjs"
11
  },
 
5
  "description": "Local-first sovereign development workbench",
6
  "type": "module",
7
  "scripts": {
8
+ "test": "node tests/smoke.mjs && node harness/test-orchestrator.mjs && node daemon/test-model-manager.mjs",
9
  "check": "node --check app.js && node --check daemon/server.mjs && node --check harness/orchestrator.mjs && node --check harness/checks.mjs",
10
  "veritas": "node harness/run-veritas.mjs"
11
  },