Skip to main content
Glama
llm-utils.js10.3 kB
/** * LLM Utilities and Best Practices Framework * Provides tools for proper LLM interaction patterns including * response validation, token management, error handling, and prompt optimization */ /** * Estimate token count for text content * Uses a simple approximation: 1 token ≈ 4 characters * More accurate tokenization would require model-specific tokenizers */ function estimateTokenCount(text) { if (typeof text !== 'string') { return 0; } // Rough approximation: 1 token per 4 characters // This varies by model and language, but provides a baseline return Math.ceil(text.length / 4); } /** * Token limit configurations for different model types */ const TOKEN_LIMITS = { 'gpt-3.5-turbo': { input: 4096, output: 4096 }, 'gpt-4': { input: 8192, output: 8192 }, 'gpt-4-32k': { input: 32768, output: 32768 }, 'claude-3-sonnet': { input: 200000, output: 4096 }, 'claude-3-opus': { input: 200000, output: 4096 }, 'claude-3-haiku': { input: 200000, output: 4096 }, default: { input: 4096, output: 1024 } }; /** * Validate and potentially truncate content to fit within token limits */ function validateTokenLimits(content, modelType = 'default', reserveOutputTokens = 1024) { const limits = TOKEN_LIMITS[modelType] || TOKEN_LIMITS.default; const maxInputTokens = limits.input - reserveOutputTokens; const estimatedTokens = estimateTokenCount(content); if (estimatedTokens <= maxInputTokens) { return { content, withinLimits: true, estimatedTokens, maxInputTokens }; } // Calculate truncation point (leave some buffer) const targetChars = Math.floor(maxInputTokens * 4 * 0.9); // 90% of limit for safety const truncatedContent = content.substring(0, targetChars) + '...\n[Content truncated to fit token limits]'; return { content: truncatedContent, withinLimits: false, estimatedTokens: estimateTokenCount(truncatedContent), originalTokens: estimatedTokens, maxInputTokens, truncated: true }; } /** * Response validation schemas for different prompt types */ const RESPONSE_SCHEMAS = { playlist_description: { type: 'object', required: ['description'], properties: { description: { type: 'string', minLength: 50, maxLength: 500 } } }, content_recommendation: { type: 'object', required: ['recommendations'], properties: { recommendations: { type: 'array', minItems: 1, maxItems: 10, items: { type: 'object', required: ['title', 'reason'], properties: { title: { type: 'string', minLength: 1 }, year: { type: 'number', minimum: 1900, maximum: 2100 }, reason: { type: 'string', minLength: 20 }, appeal: { type: 'string' }, features: { type: 'array', items: { type: 'string' } } } } } } }, smart_playlist_rules: { type: 'object', required: ['criteria'], properties: { criteria: { type: 'object', properties: { filters: { type: 'array', items: { type: 'object' } }, sorting: { type: 'object' }, advanced_criteria: { type: 'array' }, tips: { type: 'array', items: { type: 'string' } } } } } }, media_analysis: { type: 'object', required: ['insights'], properties: { insights: { type: 'object', properties: { patterns: { type: 'array' }, recommendations: { type: 'array' }, statistics: { type: 'object' }, trends: { type: 'array' } } } } } }; /** * Validate LLM response against expected schema */ function validateResponse(response, promptType) { const schema = RESPONSE_SCHEMAS[promptType]; if (!schema) { return { valid: true, warnings: [`No validation schema defined for prompt type: ${promptType}`] }; } const errors = []; const warnings = []; try { // Basic structure validation if (typeof response !== 'object' || response === null) { errors.push('Response must be a valid object'); return { valid: false, errors, warnings }; } // Check required fields if (schema.required) { for (const field of schema.required) { if (!(field in response)) { errors.push(`Missing required field: ${field}`); } } } // Validate specific fields based on prompt type switch (promptType) { case 'playlist_description': if (response.description && response.description.length < 50) { warnings.push('Description is quite short, consider expanding'); } break; case 'content_recommendation': if (response.recommendations && response.recommendations.length === 0) { errors.push('At least one recommendation is required'); } break; case 'smart_playlist_rules': if (response.criteria && !response.criteria.filters) { warnings.push('No specific filters provided in criteria'); } break; } } catch (error) { errors.push(`Validation error: ${error.message}`); } return { valid: errors.length === 0, errors, warnings }; } /** * Retry logic with exponential backoff for external API calls */ async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Don't retry on certain error types if (error.code === 'INVALID_REQUEST' || error.status === 400) { throw error; } // If this is the last attempt, throw the error if (attempt === maxRetries - 1) { throw error; } // Calculate delay with exponential backoff and jitter const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000; await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } /** * Model parameter configurations for different use cases */ const MODEL_PARAMETERS = { creative: { temperature: 0.8, top_p: 0.9, frequency_penalty: 0.1, presence_penalty: 0.1 }, analytical: { temperature: 0.3, top_p: 0.8, frequency_penalty: 0.0, presence_penalty: 0.0 }, balanced: { temperature: 0.6, top_p: 0.85, frequency_penalty: 0.05, presence_penalty: 0.05 }, conservative: { temperature: 0.1, top_p: 0.7, frequency_penalty: 0.0, presence_penalty: 0.0 } }; /** * Get recommended model parameters for a prompt type */ function getModelParameters(promptType, style = 'balanced') { const baseParams = MODEL_PARAMETERS[style] || MODEL_PARAMETERS.balanced; // Adjust parameters based on prompt type switch (promptType) { case 'playlist_description': return { ...baseParams, temperature: 0.8 }; // More creative case 'content_recommendation': return { ...baseParams, temperature: 0.6 }; // Balanced case 'smart_playlist_rules': return { ...baseParams, temperature: 0.3 }; // More analytical case 'media_analysis': return { ...baseParams, temperature: 0.2 }; // Very analytical default: return baseParams; } } /** * Enhanced prompt builder with context inclusion and validation */ function buildEnhancedPrompt(promptType, args, context = {}) { const errors = []; // Validate required arguments based on prompt type switch (promptType) { case 'playlist_description': if (!args.playlist_name) { errors.push('playlist_name is required'); } break; case 'content_recommendation': if (!args.liked_content) { errors.push('liked_content is required'); } break; case 'smart_playlist_rules': if (!args.intent) { errors.push('intent is required'); } break; case 'media_analysis': if (!args.content_data) { errors.push('content_data is required'); } break; } if (errors.length > 0) { throw new Error(`Prompt validation failed: ${errors.join(', ')}`); } // Include relevant context const contextInfo = []; if (context.userLibrarySize) { contextInfo.push(`User has ${context.userLibrarySize} items in their library`); } if (context.preferredGenres) { contextInfo.push(`Preferred genres: ${context.preferredGenres.join(', ')}`); } if (context.recentActivity) { contextInfo.push(`Recent activity: ${context.recentActivity}`); } const contextString = contextInfo.length > 0 ? `\n\nContext: ${contextInfo.join('. ')}\n` : ''; return { promptType, args, context: contextString, modelParameters: getModelParameters(promptType), tokenValidation: validateTokenLimits(JSON.stringify(args) + contextString) }; } /** * Error handling patterns for LLM interactions */ function handleLLMError(error, promptType) { const baseMessage = `Error in ${promptType} prompt`; if (error.code === 'rate_limit_exceeded') { return { error: 'Rate limit exceeded', message: `${baseMessage}: Too many requests. Please try again later.`, retryable: true, retryAfter: error.retry_after || 60 }; } if (error.code === 'insufficient_quota') { return { error: 'Quota exceeded', message: `${baseMessage}: API quota exceeded. Check your billing.`, retryable: false }; } if (error.code === 'model_overloaded') { return { error: 'Model overloaded', message: `${baseMessage}: Model is overloaded. Try again shortly.`, retryable: true, retryAfter: 30 }; } if (error.code === 'invalid_request_error') { return { error: 'Invalid request', message: `${baseMessage}: Request format is invalid. Check prompt structure.`, retryable: false }; } return { error: 'Unknown error', message: `${baseMessage}: ${error.message}`, retryable: true }; } module.exports = { estimateTokenCount, validateTokenLimits, validateResponse, retryWithBackoff, getModelParameters, buildEnhancedPrompt, handleLLMError, TOKEN_LIMITS, MODEL_PARAMETERS, RESPONSE_SCHEMAS };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vyb1ng/plex-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server