@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
660 lines (581 loc) • 21.8 kB
JavaScript
/**
* Helper functions that can be mixed into provider classes
* Reduces code duplication by providing common implementations
*/
import {
selectModelByComplexity,
standardConnectionTest,
createProviderErrorResponse,
createProviderSuccessResponse,
validateModelWithFallbacks,
extractProviderConfig,
buildClientOptions
} from './provider-utils.js';
import {
MODEL_CONFIGS,
getProviderModelConfig,
getModelCapabilities,
getSuggestedModels,
normalizeModelName,
analyzeCommitComplexity,
getBestModelForCapabilities,
getAllHubProviders
} from './model-config.js';
// Provider utility functions
function isHubProvider(providerName) {
const config = MODEL_CONFIGS[providerName];
return config && config.isHub === true;
}
function selectHubModel(providerName, complexity = 'standard', availableModels = []) {
const modelConfig = getProviderModelConfig(providerName);
if (!modelConfig) return null;
const modelKey = {
simple: 'smallModel',
standard: 'standardModel',
medium: 'mediumModel',
complex: 'complexModel'
}[complexity] || 'standardModel';
const preferredModel = modelConfig[modelKey];
// If we have available models list, check if preferred model exists
if (Array.isArray(availableModels) && availableModels.length > 0) {
if (availableModels.includes(preferredModel)) {
return preferredModel;
}
// Try fallbacks
for (const fallback of modelConfig.fallbacks || []) {
if (availableModels.includes(fallback)) {
return fallback;
}
}
// Return first available model if nothing else matches
return availableModels[0];
}
return preferredModel;
}
/**
* Mixin that provides enhanced model recommendation logic with hub support
* @param {string} providerName - Name of the provider
* @returns {Object} Mixin methods
*/
export function ModelRecommendationMixin(providerName) {
return {
getModelRecommendation(commitDetails) {
// Get available models for hub providers
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
const modelConfig = getProviderModelConfig(providerName, this.config, availableModels);
// Use enhanced complexity analysis
const complexityAnalysis = analyzeCommitComplexity(commitDetails, providerName);
// For hub providers, use specialized selection logic
if (isHubProvider(providerName)) {
const selectedModel = selectHubModel(providerName, complexityAnalysis.complexity, availableModels);
return {
model: selectedModel,
complexity: complexityAnalysis.complexity,
reasoning: complexityAnalysis.reasoning,
isHubProvider: true,
availableModels: availableModels.length
};
}
// Standard provider logic
return selectModelByComplexity(commitDetails, modelConfig);
},
async selectOptimalModel(commitDetails) {
// Enhanced async version with hub awareness
if (isHubProvider(providerName)) {
// Try to refresh available models for hub providers
if (this.refreshAvailableModels) {
try {
await this.refreshAvailableModels();
} catch (error) {
// Continue with cached models if refresh fails
console.warn(`Failed to refresh models for ${providerName}:`, error.message);
}
}
}
return this.getModelRecommendation(commitDetails);
},
async selectModelForCapabilities(requiredCapabilities = []) {
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
return getBestModelForCapabilities(providerName, requiredCapabilities, this.config, availableModels);
}
};
}
/**
* Mixin that provides standard connection testing
* @param {string} providerName - Name of the provider
* @returns {Object} Mixin methods
*/
export function ConnectionTestMixin(providerName) {
return {
async testConnection() {
if (!this.isAvailable()) {
return createProviderErrorResponse(
providerName,
'connection_test',
`${providerName} provider is not configured`,
[`Configure ${providerName.toUpperCase()}_API_KEY`]
);
}
const modelConfig = getProviderModelConfig(providerName, this.config);
const defaultModel = normalizeModelName(providerName, modelConfig.standardModel);
const result = await standardConnectionTest(
this.generateCompletion.bind(this),
defaultModel
);
if (result.success) {
return createProviderSuccessResponse(providerName, {
response: result.response,
model: result.model,
provider_info: this.getProviderInfo ? this.getProviderInfo() : {}
});
} else {
return createProviderErrorResponse(
providerName,
'connection_test',
result.error,
modelConfig.fallbacks
);
}
}
};
}
/**
* Mixin that provides standard model validation
* @param {string} providerName - Name of the provider
* @returns {Object} Mixin methods
*/
export function ModelValidationMixin(providerName) {
return {
async validateModelAvailability(modelName) {
if (!this.isAvailable()) {
return createProviderErrorResponse(
providerName,
'model_validation',
`${providerName} provider is not configured`,
[`Configure ${providerName.toUpperCase()}_API_KEY`]
);
}
const normalizedModel = normalizeModelName(providerName, modelName);
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
const fallbacks = getSuggestedModels(providerName, normalizedModel, availableModels);
// For hub providers, check if model is in available list first
if (isHubProvider(providerName) && Array.isArray(availableModels) && availableModels.length > 0) {
if (!availableModels.includes(normalizedModel)) {
return createProviderErrorResponse(
providerName,
'model_validation',
`Model '${normalizedModel}' not found in available deployments`,
fallbacks
);
}
}
// Use provider-specific model testing if available
if (this.testModel) {
return validateModelWithFallbacks(
this.testModel.bind(this),
normalizedModel,
fallbacks
);
}
// Fallback to basic connection test with the model
try {
const result = await standardConnectionTest(
this.generateCompletion.bind(this),
normalizedModel
);
if (result.success) {
return createProviderSuccessResponse(providerName, {
model: normalizedModel,
capabilities: this.getCapabilities(normalizedModel),
isHubProvider: isHubProvider(providerName),
availableModels: availableModels.length
});
} else {
return createProviderErrorResponse(
providerName,
'model_validation',
result.error,
fallbacks
);
}
} catch (error) {
return createProviderErrorResponse(
providerName,
'model_validation',
error.message,
fallbacks
);
}
}
};
}
/**
* Mixin that provides standard capabilities lookup
* @param {string} providerName - Name of the provider
* @returns {Object} Mixin methods
*/
export function CapabilitiesMixin(providerName) {
return {
getCapabilities(modelName) {
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
const modelConfig = getProviderModelConfig(providerName, this.config, availableModels);
const model = modelName ||
this.config.AI_MODEL ||
this.config[`${providerName.toUpperCase()}_MODEL`] ||
modelConfig.standardModel;
const normalizedModel = normalizeModelName(providerName, model);
const capabilities = getModelCapabilities(normalizedModel);
// Add provider-specific metadata
if (isHubProvider(providerName)) {
capabilities.isHubProvider = true;
capabilities.availableInHub = !Array.isArray(availableModels) || availableModels.length === 0 || availableModels.includes(normalizedModel);
}
return capabilities;
},
/**
* Enhanced capability testing - tests actual provider functionality
* @param {Object} options - Test options
* @returns {Promise<Object>} Detailed capability test results
*/
async testCapabilities(options = {}) {
const results = {
available: false,
connection: false,
modelAccess: false,
capabilities: {},
errors: [],
performance: {},
tested_at: new Date().toISOString()
};
try {
// Test 1: Basic availability
results.available = this.isAvailable();
if (!results.available) {
results.errors.push('Provider not available - check configuration');
return results;
}
// Test 2: Connection test
if (this.testConnection) {
const startTime = Date.now();
try {
await this.testConnection();
results.connection = true;
results.performance.connectionTime = Date.now() - startTime;
} catch (error) {
results.errors.push(`Connection test failed: ${error.message}`);
}
}
// Test 3: Model access test
if (options.testModel !== false) {
try {
const testStartTime = Date.now();
const testResponse = await ProviderResponseHandler.executeWithErrorHandling(
this,
'test_model_access',
async () => {
return await this.generateCompletion([
{ role: 'user', content: 'Test message - respond with "OK"' }
], { max_tokens: 10 });
}
);
if (testResponse && !testResponse.error) {
results.modelAccess = true;
results.performance.modelResponseTime = Date.now() - testStartTime;
results.performance.tokensGenerated = testResponse.tokens || 0;
} else {
results.errors.push(`Model test failed: ${testResponse.error || 'Unknown error'}`);
}
} catch (error) {
results.errors.push(`Model access test failed: ${error.message}`);
}
}
// Test 4: Get detailed capabilities
try {
results.capabilities = this.getCapabilities(options.model);
} catch (error) {
results.errors.push(`Capabilities detection failed: ${error.message}`);
}
} catch (error) {
results.errors.push(`Capability testing failed: ${error.message}`);
}
return results;
},
/**
* Quick health check - lightweight version of testCapabilities
* @returns {Promise<Object>} Basic health status
*/
async quickHealthCheck() {
const health = {
status: 'unknown',
available: false,
configured: false,
timestamp: new Date().toISOString()
};
try {
health.available = this.isAvailable();
health.configured = this.getName && this.config;
if (health.available && health.configured) {
health.status = 'healthy';
} else if (health.configured) {
health.status = 'configured_but_unavailable';
} else {
health.status = 'not_configured';
}
} catch (error) {
health.status = 'error';
health.error = error.message;
}
return health;
},
getSimilarModels(modelName, providedAvailableModels = []) {
// Use provider's available models if not provided
const availableModels = providedAvailableModels.length > 0 ?
providedAvailableModels :
(this.getAvailableModels ? this.getAvailableModels() : []);
// Enhanced similarity matching for hub providers
if (isHubProvider(providerName) && Array.isArray(availableModels) && availableModels.length > 0) {
const modelFamily = modelName.split('-')[0] || modelName.split('.')[0]; // e.g., 'gpt', 'claude', 'gemini', 'anthropic'
const familyModels = availableModels
.filter(m => m.includes(modelFamily) && m !== modelName)
.slice(0, 3);
// If no family matches, get models from the same capability tier
if (familyModels.length === 0) {
const modelCapabilities = getModelCapabilities(modelName);
const similarCapabilityModels = availableModels
.filter(m => {
const caps = getModelCapabilities(m);
return caps.reasoning === modelCapabilities.reasoning &&
caps.large_context === modelCapabilities.large_context;
})
.slice(0, 3);
return similarCapabilityModels;
}
return familyModels;
}
// Standard provider logic with enhanced suggestions
return getSuggestedModels(providerName, modelName, availableModels);
}
};
}
/**
* Mixin that provides standard configuration handling
* @param {string} providerName - Name of the provider
* @param {Object} defaults - Default configuration values
* @returns {Object} Mixin methods
*/
export function ConfigurationMixin(providerName, defaults = {}) {
return {
getProviderConfig() {
return extractProviderConfig(
this.config,
providerName.toUpperCase(),
defaults
);
},
getProviderModelConfig() {
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
return getProviderModelConfig(providerName, this.config, availableModels);
},
buildClientOptions(extraDefaults = {}) {
const providerConfig = this.getProviderConfig();
return buildClientOptions(providerConfig, { ...defaults, ...extraDefaults });
},
getProviderInfo() {
const providerConfig = this.getProviderConfig();
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
const modelConfig = getProviderModelConfig(providerName, this.config, availableModels);
const info = {
name: providerName,
configured: this.isAvailable(),
config_keys: Object.keys(providerConfig).filter(k => providerConfig[k]),
default_model: modelConfig.standardModel || 'unknown',
isHub: isHubProvider(providerName)
};
// Add hub-specific information
if (isHubProvider(providerName)) {
info.hubInfo = {
availableModels: availableModels.length,
supportedProviders: modelConfig.hubInfo?.supportedProviders || [],
defaultProvider: modelConfig.hubInfo?.defaultProvider,
canDetectDeployments: !!(this.getAvailableModels || this.refreshAvailableModels)
};
if (availableModels.length > 0) {
info.hubInfo.sampleModels = availableModels.slice(0, 3);
}
}
return info;
}
};
}
/**
* Unified Provider Response Handler
* Centralizes common patterns across all providers for consistency and maintainability
*/
export class ProviderResponseHandler {
/**
* Execute provider operation with standardized error handling and availability checking
* @param {Object} provider - Provider instance
* @param {string} operation - Operation name (e.g., 'generate_completion')
* @param {Function} operationFn - Function to execute the operation
* @param {Object} context - Additional context for error handling
* @returns {Promise<Object>} Standardized response
*/
static async executeWithErrorHandling(provider, operation, operationFn, context = {}) {
// Check availability first
if (!provider.isAvailable()) {
return this.createUnavailableResponse(provider.getName(), operation);
}
try {
return await operationFn();
} catch (error) {
return provider.handleProviderError(error, operation, context);
}
}
/**
* Create standardized unavailable response
* @param {string} providerName - Name of the provider
* @param {string} operation - Operation that was attempted
* @returns {Object} Error response
*/
static createUnavailableResponse(providerName, operation) {
return createProviderErrorResponse(
providerName,
operation,
`${providerName} provider is not configured`,
[`Configure ${providerName.toUpperCase()}_API_KEY and other required settings`]
);
}
/**
* Execute multiple provider operations in sequence with unified error handling
* @param {Object} provider - Provider instance
* @param {Array} operations - Array of {name, fn, context} operations
* @returns {Promise<Array>} Array of results
*/
static async executeMultiple(provider, operations) {
const results = [];
for (const op of operations) {
const result = await this.executeWithErrorHandling(provider, op.name, op.fn, op.context);
results.push(result);
// Stop on first error if any operation fails
if (result.error) {
break;
}
}
return results;
}
}
/**
* Mixin that provides standard error handling for providers
* @param {string} providerName - Name of the provider
* @returns {Object} Mixin methods
*/
export function ErrorHandlingMixin(providerName) {
return {
handleProviderError(error, operation, context = {}) {
// Common error patterns and their standardized responses
if (error.message.includes('API key') || error.message.includes('401')) {
return createProviderErrorResponse(
providerName,
operation,
'Invalid or missing API key',
[`Check ${providerName.toUpperCase()}_API_KEY configuration`]
);
}
if (error.message.includes('model') && error.message.includes('not found')) {
const availableModels = this.getAvailableModels ? this.getAvailableModels() : [];
const modelConfig = getProviderModelConfig(providerName, this.config, availableModels);
const fallbacks = getSuggestedModels(providerName, context.model, availableModels);
let errorMessage = `Model not available: ${context.model}`;
if (isHubProvider(providerName) && Array.isArray(availableModels) && availableModels.length > 0) {
errorMessage += ` (Available models: ${availableModels.length})`;
}
return createProviderErrorResponse(
providerName,
operation,
errorMessage,
fallbacks
);
}
if (error.message.includes('rate limit') || error.message.includes('429')) {
return createProviderErrorResponse(
providerName,
operation,
'Rate limit exceeded',
['Wait before retrying', 'Consider upgrading API plan']
);
}
if (error.message.includes('timeout')) {
return createProviderErrorResponse(
providerName,
operation,
'Request timeout',
['Increase timeout setting', 'Try again later']
);
}
// Generic error response
return createProviderErrorResponse(
providerName,
operation,
error.message,
['Check provider configuration', 'Verify network connectivity']
);
}
};
}
/**
* Apply multiple mixins to a provider class
* @param {Function} ProviderClass - The provider class to enhance
* @param {string} providerName - Name of the provider
* @param {Array<Function>} mixins - Array of mixin functions to apply
* @returns {Function} Enhanced provider class
*/
export function applyMixins(ProviderClass, providerName, mixins = []) {
const defaultMixins = [
ConfigurationMixin,
ModelRecommendationMixin,
ConnectionTestMixin,
ModelValidationMixin,
CapabilitiesMixin,
ErrorHandlingMixin
];
const allMixins = [...defaultMixins, ...mixins];
// Apply each mixin to the prototype
allMixins.forEach(mixinFn => {
const methods = mixinFn(providerName);
Object.assign(ProviderClass.prototype, methods);
});
return ProviderClass;
}
/**
* Create a standardized provider class with all common functionality
* @param {string} providerName - Name of the provider
* @param {Object} options - Provider-specific options
* @returns {Function} Base provider class with mixins applied
*/
export function createEnhancedProvider(providerName, options = {}) {
const { mixins = [] } = options;
class EnhancedProvider {
constructor(config) {
this.config = config;
this.providerName = providerName;
// Initialize provider-specific client if available method exists
if (this.initializeClient) {
if (this.isAvailable()) {
this.initializeClient();
}
}
}
getName() {
return providerName;
}
// These methods should be implemented by the specific provider
isAvailable() {
throw new Error('isAvailable() must be implemented by the provider');
}
async generateCompletion() {
throw new Error('generateCompletion() must be implemented by the provider');
}
}
// Apply all mixins
return applyMixins(EnhancedProvider, providerName, mixins);
}