UNPKG

llm-checker

Version:

Intelligent CLI tool with AI-powered model selection that analyzes your hardware and recommends optimal LLM models for your system

363 lines (301 loc) 12.4 kB
const fetch = require('node-fetch'); class OllamaClient { constructor(baseURL = 'http://localhost:11434') { this.baseURL = baseURL; this.isAvailable = null; this.lastCheck = 0; this.cacheTimeout = 30000; } async checkOllamaAvailability() { if (this.isAvailable !== null && Date.now() - this.lastCheck < this.cacheTimeout) { return this.isAvailable; } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch(`${this.baseURL}/api/version`, { signal: controller.signal, headers: { 'Content-Type': 'application/json' } }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); this.isAvailable = { available: true, version: data.version || 'unknown' }; this.lastCheck = Date.now(); return this.isAvailable; } this.isAvailable = { available: false, error: 'Ollama not responding properly' }; this.lastCheck = Date.now(); return this.isAvailable; } catch (error) { this.isAvailable = { available: false, error: error.message.includes('ECONNREFUSED') ? 'Ollama not running (connection refused)' : error.message.includes('timeout') ? 'Ollama connection timeout' : error.message }; this.lastCheck = Date.now(); return this.isAvailable; } } async getLocalModels() { const availability = await this.checkOllamaAvailability(); if (!availability.available) { throw new Error(`Ollama not available: ${availability.error}`); } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); const response = await fetch(`${this.baseURL}/api/tags`, { signal: controller.signal, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`); } const data = await response.json(); if (!data.models) { return []; } const models = data.models.map(model => this.parseOllamaModel(model)); return models; } catch (error) { throw new Error(`Failed to fetch local models: ${error.message}`); } } async getRunningModels() { const availability = await this.checkOllamaAvailability(); if (!availability.available) { return []; } try { const response = await fetch(`${this.baseURL}/api/ps`, { timeout: 10000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); if (!response.ok) { return []; } const data = await response.json(); const runningModels = (data.models || []).map(model => ({ name: model.name, model: model.model, size: model.size, digest: model.digest, expires_at: model.expires_at, size_vram: model.size_vram, processor: model.processor || 'unknown' })); return runningModels; } catch (error) { return []; } } async testConnection() { try { // Test 1: Version check const versionResponse = await fetch(`${this.baseURL}/api/version`, { timeout: 5000 }); if (!versionResponse.ok) { return { success: false, error: `Version check failed: ${versionResponse.status}`, details: 'Ollama might not be running properly' }; } const versionData = await versionResponse.json(); // Test 2: Tags check const tagsResponse = await fetch(`${this.baseURL}/api/tags`, { timeout: 10000 }); if (!tagsResponse.ok) { return { success: false, error: `Tags check failed: ${tagsResponse.status}`, details: 'Could not access models API' }; } const tagsText = await tagsResponse.text(); let tagsData; try { tagsData = JSON.parse(tagsText); } catch (e) { return { success: false, error: 'Invalid JSON in tags response', details: tagsText.substring(0, 100) }; } return { success: true, version: versionData.version, modelsFound: tagsData.models ? tagsData.models.length : 0, models: tagsData.models || [] }; } catch (error) { return { success: false, error: error.message, details: error.code || 'Unknown error' }; } } parseOllamaModel(ollamaModel) { const sizeBytes = ollamaModel.size || 0; const sizeGB = Math.round(sizeBytes / (1024 ** 3) * 10) / 10; const [modelFamily, version] = ollamaModel.name.split(':'); const details = ollamaModel.details || {}; let estimatedParams = 'Unknown'; if (details.parameter_size) { estimatedParams = details.parameter_size; } else if (sizeGB > 0) { if (sizeGB < 2) estimatedParams = '1B'; else if (sizeGB < 4) estimatedParams = '3B'; else if (sizeGB < 6) estimatedParams = '7B'; else if (sizeGB < 15) estimatedParams = '8B'; else if (sizeGB < 25) estimatedParams = '13B'; else if (sizeGB < 45) estimatedParams = '34B'; else estimatedParams = '70B+'; } return { name: ollamaModel.name, displayName: `${modelFamily} ${version || 'latest'}`, family: details.family || modelFamily.toLowerCase(), size: estimatedParams, fileSizeGB: sizeGB, quantization: details.quantization_level || 'Unknown', format: details.format || 'GGUF', digest: ollamaModel.digest, modified: ollamaModel.modified_at, source: 'ollama_local', details: { parameter_size: details.parameter_size, quantization_level: details.quantization_level, families: details.families || [details.family || modelFamily], parent_model: details.parent_model || '' } }; } async pullModel(modelName, onProgress = null) { const availability = await this.checkOllamaAvailability(); if (!availability.available) { throw new Error(`Ollama not available: ${availability.error}`); } try { const response = await fetch(`${this.baseURL}/api/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName, stream: true }) }); if (!response.ok) { throw new Error(`Failed to pull model: HTTP ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { try { const data = JSON.parse(line); if (onProgress && (data.status || data.completed !== undefined)) { onProgress({ status: data.status, completed: data.completed, total: data.total, percent: data.total ? Math.round((data.completed / data.total) * 100) : 0 }); } if (data.status === 'success') { return { success: true, model: modelName }; } } catch (e) { // Skip malformed JSON lines } } } return { success: true, model: modelName }; } catch (error) { throw new Error(`Failed to pull model: ${error.message}`); } } async deleteModel(modelName) { const availability = await this.checkOllamaAvailability(); if (!availability.available) { throw new Error(`Ollama not available: ${availability.error}`); } try { const response = await fetch(`${this.baseURL}/api/delete`, { method: 'DELETE', timeout: 10000, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName }) }); if (!response.ok) { throw new Error(`Failed to delete model: HTTP ${response.status}`); } return { success: true, model: modelName }; } catch (error) { throw new Error(`Failed to delete model: ${error.message}`); } } async testModelPerformance(modelName, testPrompt = "Hello, how are you?") { const availability = await this.checkOllamaAvailability(); if (!availability.available) { throw new Error(`Ollama not available: ${availability.error}`); } const startTime = Date.now(); try { const response = await fetch(`${this.baseURL}/api/generate`, { method: 'POST', timeout: 30000, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName, prompt: testPrompt, stream: false, options: { num_predict: 50 // Limitar respuesta para test rápido } }) }); if (!response.ok) { throw new Error(`Test failed: HTTP ${response.status}`); } const data = await response.json(); const endTime = Date.now(); const totalTime = endTime - startTime; const tokensGenerated = data.eval_count || 50; const tokensPerSecond = Math.round((tokensGenerated / (totalTime / 1000)) * 10) / 10; return { success: true, responseTime: totalTime, tokensPerSecond, tokensGenerated, loadTime: data.load_duration ? Math.round(data.load_duration / 1000000) : null, evalTime: data.eval_duration ? Math.round(data.eval_duration / 1000000) : null, response: data.response }; } catch (error) { return { success: false, error: error.message, responseTime: Date.now() - startTime }; } } } module.exports = OllamaClient;