capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
230 lines • 7.87 kB
JavaScript
import chalk from 'chalk';
export class LocalModelsService {
models = [];
lastRefresh = null;
refreshInterval = 60000;
servers = [];
constructor() {
this.servers = [
{ type: 'ollama', baseUrl: 'http://localhost:11434', available: false, models: [] },
{ type: 'openai', baseUrl: 'http://localhost:1234', available: false, models: [] },
{ type: 'openai', baseUrl: 'http://localhost:8080', available: false, models: [] },
];
this.discoverModels().catch(() => { });
}
getAvailableModels() {
if (this.shouldRefresh()) {
this.discoverModels().catch(() => { });
}
return this.models;
}
getModelsByProvider(provider) {
const server = this.servers.find(s => (provider === 'ollama' && s.type === 'ollama') ||
(provider === 'lmstudio' && s.baseUrl.includes('1234')) ||
(provider === 'local' && s.available));
if (!server)
return [];
return server.models.map(name => ({
name,
supports_tools: true,
}));
}
async isAnyServerAvailable() {
for (const server of this.servers) {
if (await this.checkServer(server)) {
return true;
}
}
return false;
}
async getAvailableServerUrl() {
for (const server of this.servers) {
if (await this.checkServer(server)) {
return server.baseUrl;
}
}
return null;
}
async discoverModels() {
const allModels = [];
for (const server of this.servers) {
try {
const models = await this.discoverFromServer(server);
if (models.length > 0) {
server.available = true;
server.models = models;
allModels.push(...models);
}
else {
server.available = false;
server.models = [];
}
}
catch {
server.available = false;
server.models = [];
}
}
this.models = [...new Set(allModels)];
this.lastRefresh = new Date();
}
async discoverFromServer(server) {
if (server.type === 'ollama') {
return this.discoverOllamaModels(server.baseUrl);
}
else {
return this.discoverOpenAIModels(server.baseUrl);
}
}
async discoverOllamaModels(baseUrl) {
try {
const response = await fetch(`${baseUrl}/api/tags`, {
method: 'GET',
signal: AbortSignal.timeout(2000),
});
if (!response.ok)
return [];
const data = await response.json();
const models = data.models || [];
return models.map((model) => {
return model.name || model.model;
});
}
catch {
return [];
}
}
async discoverOpenAIModels(baseUrl) {
try {
const response = await fetch(`${baseUrl}/v1/models`, {
method: 'GET',
signal: AbortSignal.timeout(2000),
});
if (!response.ok)
return [];
const data = await response.json();
const models = data.data || [];
return models.map((model) => model.id);
}
catch {
return [];
}
}
async checkServer(server) {
try {
const endpoints = server.type === 'ollama'
? [`${server.baseUrl}/api/version`]
: [`${server.baseUrl}/v1/models`];
for (const endpoint of endpoints) {
const response = await fetch(endpoint, {
method: 'GET',
signal: AbortSignal.timeout(1000),
});
if (response.ok) {
return true;
}
}
return false;
}
catch {
return false;
}
}
shouldRefresh() {
if (!this.lastRefresh)
return true;
const now = new Date();
const diff = now.getTime() - this.lastRefresh.getTime();
return diff > this.refreshInterval;
}
async getModelInfo(modelName) {
const ollamaServer = this.servers.find(s => s.type === 'ollama' && s.available);
if (!ollamaServer)
return null;
try {
const response = await fetch(`${ollamaServer.baseUrl}/api/show`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: modelName }),
signal: AbortSignal.timeout(2000),
});
if (!response.ok)
return null;
const data = await response.json();
return {
name: modelName,
size: data.details?.parameter_size,
modified: data.modified_at,
context_length: data.details?.context_length || 4096,
supports_tools: true,
};
}
catch {
return null;
}
}
async pullModel(modelName, onProgress) {
const ollamaServer = this.servers.find(s => s.type === 'ollama');
if (!ollamaServer) {
console.error(chalk.red('Ollama server not found'));
return false;
}
try {
const response = await fetch(`${ollamaServer.baseUrl}/api/pull`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: modelName, stream: true }),
});
if (!response.ok || !response.body) {
return false;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let done = false;
while (!done) {
const { value, done: isDone } = await reader.read();
done = isDone;
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim())
continue;
try {
const data = JSON.parse(line);
if (data.total && data.completed && onProgress) {
const progress = (data.completed / data.total) * 100;
onProgress(progress);
}
if (data.status === 'success') {
await this.discoverModels();
return true;
}
}
catch (e) {
}
}
}
return true;
}
catch (error) {
console.error(chalk.red('Failed to pull model:'), error);
return false;
}
}
getServerStatus() {
return this.servers.map(server => ({
name: server.type === 'ollama' ? 'Ollama' :
server.baseUrl.includes('1234') ? 'LM Studio' :
server.baseUrl.includes('8080') ? 'llama.cpp' : 'Local Server',
url: server.baseUrl,
available: server.available,
models: server.models.length,
}));
}
}
export const localModelsService = new LocalModelsService();
//# sourceMappingURL=local-models.js.map