UNPKG

glassbox-ai

Version:

Enterprise-grade AI testing framework with reliability, observability, and comprehensive validation

293 lines (260 loc) 7.2 kB
import axios from 'axios'; import { exponentialBackoff, classifyError, handleErrorWithGracefulDegradation, logger, RETRY_CONFIG, ERROR_TYPES } from '../error-handler.js'; const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434/api/generate'; const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'mistral:7b'; // Simple token counter (splits on whitespace) function countTokens(text) { if (!text) return 0; return text.trim().split(/\s+/).length; } /** * Enhanced Ollama API client with comprehensive error handling */ class OllamaClient { constructor() { this.baseURL = OLLAMA_URL; this.defaultModel = OLLAMA_MODEL; this.timeout = RETRY_CONFIG.TIMEOUT_MS; } /** * Make a request to Ollama API with enhanced error handling */ async makeRequest(prompt, options = {}) { const { model = this.defaultModel, maxTokens = 1000, temperature = 0.7, retryConfig = {} } = options; const requestConfig = { method: 'POST', url: this.baseURL, headers: { 'Content-Type': 'application/json', 'User-Agent': 'Glassbox-CLI/1.0' }, timeout: this.timeout, data: { model, prompt, stream: false, options: { num_predict: maxTokens, temperature } } }; try { // Use exponential backoff for retries const response = await exponentialBackoff(async () => { const res = await axios(requestConfig); // Validate response structure if (!res.data || res.data.response === undefined) { throw new Error('Invalid response structure from Ollama API'); } return res; }, retryConfig.maxRetries || RETRY_CONFIG.DEFAULT_MAX_RETRIES); const responseText = response.data.response; const tokenCount = countTokens(responseText); // Log successful request logger.info('Ollama API request successful', { model, promptLength: prompt.length, responseLength: responseText.length, tokenCount }); return { response: responseText, tokenCount, model, usage: { prompt_tokens: countTokens(prompt), completion_tokens: tokenCount, total_tokens: countTokens(prompt) + tokenCount } }; } catch (error) { // Handle error with graceful degradation const errorResult = handleErrorWithGracefulDegradation(error, { provider: 'ollama', model, promptLength: prompt.length, operation: 'generate_response' }); // Log detailed error information logger.error('Ollama API request failed', { error: errorResult.error.type, severity: errorResult.error.severity, retryable: errorResult.error.retryable, context: errorResult.error.context }); // Throw the classified error for upstream handling throw errorResult.error; } } /** * Check if the service is healthy */ async checkHealth() { try { const response = await axios.get('http://localhost:11434/api/tags', { timeout: 5000 }); return { healthy: response.status === 200, status: response.status, available: response.data?.models?.length > 0 }; } catch (error) { const errorResult = handleErrorWithGracefulDegradation(error, { provider: 'ollama', operation: 'health_check' }); return { healthy: false, error: errorResult.error.type, message: errorResult.error.message }; } } /** * Get available models */ async getAvailableModels() { try { const response = await axios.get('http://localhost:11434/api/tags', { timeout: 5000 }); return { models: response.data?.models?.map(m => m.name) || [], count: response.data?.models?.length || 0 }; } catch (error) { const errorResult = handleErrorWithGracefulDegradation(error, { provider: 'ollama', operation: 'get_models' }); return { models: [], count: 0, error: errorResult.error.type, message: errorResult.error.message }; } } /** * Check if a specific model is available */ async isModelAvailable(modelName) { try { const models = await this.getAvailableModels(); return { available: models.models.includes(modelName), models: models.models }; } catch (error) { return { available: false, error: error.message }; } } /** * Pull a model if not available */ async pullModel(modelName) { try { logger.info(`Pulling model: ${modelName}`); const response = await axios.post('http://localhost:11434/api/pull', { name: modelName }, { timeout: 300000, // 5 minutes for model download headers: { 'Content-Type': 'application/json' } }); return { success: true, model: modelName, status: response.data?.status || 'completed' }; } catch (error) { const errorResult = handleErrorWithGracefulDegradation(error, { provider: 'ollama', operation: 'pull_model', model: modelName }); return { success: false, model: modelName, error: errorResult.error.type, message: errorResult.error.message }; } } } // Create singleton instance const ollamaClient = new OllamaClient(); /** * Legacy function for backward compatibility * @param {string} prompt * @returns {Promise<{response: string, tokenCount: number}>} */ export async function generateResponse(prompt) { try { const result = await ollamaClient.makeRequest(prompt); return { response: result.response, tokenCount: result.tokenCount }; } catch (error) { // Re-throw the error for backward compatibility throw error; } } /** * Enhanced response generation with options * @param {string} prompt * @param {object} options * @returns {Promise<{response: string, tokenCount: number, model: string, usage: object}>} */ export async function generateResponseWithOptions(prompt, options = {}) { return await ollamaClient.makeRequest(prompt, options); } /** * Check Ollama service health * @returns {Promise<object>} */ export async function checkOllamaHealth() { return await ollamaClient.checkHealth(); } /** * Get available Ollama models * @returns {Promise<object>} */ export async function getOllamaModels() { return await ollamaClient.getAvailableModels(); } /** * Check if model is available * @param {string} modelName * @returns {Promise<object>} */ export async function isOllamaModelAvailable(modelName) { return await ollamaClient.isModelAvailable(modelName); } /** * Pull Ollama model * @param {string} modelName * @returns {Promise<object>} */ export async function pullOllamaModel(modelName) { return await ollamaClient.pullModel(modelName); }