UNPKG

ai-github-changelog-generator-cli-mcp

Version:

AI-powered changelog generator with MCP server support - works with OpenAI GPT-4.1 and Azure OpenAI with reasoning models

573 lines (484 loc) 20.4 kB
#!/usr/bin/env node // Load environment variables from .env.local require('dotenv').config({ path: '.env.local' }); const { OpenAI } = require('openai'); /** * AI Provider for Changelog Generation * Supports both OpenAI and Azure OpenAI with intelligent fallback * Updated for GPT-4.1 series and latest Azure OpenAI v1 API */ class AIProvider { constructor() { this.provider = process.env.AI_PROVIDER || 'auto'; this.openaiKey = process.env.OPENAI_API_KEY; // Model configuration with latest models from OpenAI API docs (June 2025) this.modelConfig = { default: process.env.AI_MODEL || 'gpt-4.1', simple: process.env.AI_MODEL_SIMPLE || 'gpt-4.1-mini', complex: process.env.AI_MODEL_COMPLEX || 'gpt-4.1', reasoning: process.env.AI_MODEL_REASONING || 'o4-mini', // Latest compact reasoning model (Azure-only) advanced_reasoning: process.env.AI_MODEL_ADVANCED_REASONING || 'o4', // Latest advanced reasoning model (Azure-only) // Legacy reasoning models (still supported) reasoning_legacy: process.env.AI_MODEL_REASONING_LEGACY || 'o3-mini', // Previous generation compact reasoning advanced_reasoning_legacy: process.env.AI_MODEL_ADVANCED_REASONING_LEGACY || 'o3', // Previous generation advanced reasoning nano: process.env.AI_MODEL_NANO || 'gpt-4.1-nano' }; this.azureConfig = { endpoint: process.env.AZURE_OPENAI_ENDPOINT, key: process.env.AZURE_OPENAI_KEY, deploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME || this.modelConfig.default, // Current preview API for latest features apiVersion: process.env.AZURE_OPENAI_API_VERSION || '2025-04-01-preview', // Latest GA API version gaApiVersion: process.env.AZURE_OPENAI_GA_API_VERSION || '2024-10-21', // Use v1 API by default (recommended per Microsoft docs) useV1API: process.env.AZURE_OPENAI_USE_V1_API !== 'false' }; this.activeProvider = this.determineActiveProvider(); this.isAvailable = this.activeProvider !== 'none'; // Initialize OpenAI clients based on provider this.initializeClients(); if (this.isAvailable) { console.log(`🤖 AI Provider: ${this.activeProvider.toUpperCase()} (${this.getProviderInfo()})`); } else { console.log('⚠️ No AI provider configured. Using rule-based analysis...'); } } initializeClients() { try { // Initialize Azure OpenAI client if (this.isAzureConfigured()) { if (this.azureConfig.useV1API) { console.log('🚀 Using Azure OpenAI v1 API (recommended)'); // v1 API uses standard OpenAI client with Azure base URL // Format: https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/ this.azureClient = new OpenAI({ apiKey: this.azureConfig.key, baseURL: `${this.azureConfig.endpoint}/openai/v1/`, defaultQuery: { 'api-version': 'preview' } // Use 'preview' for latest features }); } else { console.log('📦 Using Azure OpenAI traditional API'); // Traditional API for compatibility this.azureClient = new OpenAI({ apiKey: this.azureConfig.key, baseURL: `${this.azureConfig.endpoint}/openai/deployments/${this.azureConfig.deploymentName}/`, defaultQuery: { 'api-version': this.azureConfig.apiVersion } }); } } // Initialize standard OpenAI client if (this.isOpenAIConfigured()) { this.openaiClient = new OpenAI({ apiKey: this.openaiKey }); } } catch (error) { console.error('Failed to initialize AI clients:', error); throw error; } } shouldUseV1API() { // Use v1 API by default as recommended by Microsoft (May 2025+) return this.azureConfig.useV1API; } determineActiveProvider() { // Auto mode: try Azure first, then OpenAI if (this.provider === 'auto') { if (this.isAzureConfigured()) return 'azure'; if (this.isOpenAIConfigured()) return 'openai'; return 'none'; } // Explicit provider selection if (this.provider === 'azure' && this.isAzureConfigured()) return 'azure'; if (this.provider === 'openai' && this.isOpenAIConfigured()) return 'openai'; // Fallback if (this.isAzureConfigured()) return 'azure'; if (this.isOpenAIConfigured()) return 'openai'; return 'none'; } isAzureConfigured() { return !!(this.azureConfig.endpoint && this.azureConfig.key); } isOpenAIConfigured() { return !!this.openaiKey; } getProviderInfo() { if (this.activeProvider === 'azure') { const resourceName = this.azureConfig.endpoint?.match(/https:\/\/([^.]+)\.openai\.azure\.com/)?.[1] || 'unknown'; return `${resourceName} / ${this.azureConfig.deploymentName}`; } if (this.activeProvider === 'openai') { return `api.openai.com`; } return 'none'; } async generateCompletion(messages, options = {}) { if (!this.isAvailable) { throw new Error('No AI provider available'); } const defaultOptions = { temperature: 0.3, max_tokens: 1000, model: 'gpt-4' }; const settings = { ...defaultOptions, ...options }; try { if (this.activeProvider === 'azure') { return await this.callAzureOpenAI(messages, settings); } else { return await this.callOpenAI(messages, settings); } } catch (error) { console.error(`❌ ${this.activeProvider.toUpperCase()} API error:`, error.message); // Try fallback provider if auto mode if (this.provider === 'auto') { const fallbackProvider = this.activeProvider === 'azure' ? 'openai' : 'azure'; if (fallbackProvider === 'openai' && this.isOpenAIConfigured()) { console.log('🔄 Falling back to OpenAI...'); return await this.callOpenAI(messages, settings); } if (fallbackProvider === 'azure' && this.isAzureConfigured()) { console.log('🔄 Falling back to Azure OpenAI...'); return await this.callAzureOpenAI(messages, settings); } } throw error; } } async callAzureOpenAI(messages, settings) { if (!this.azureClient) { throw new Error('Azure OpenAI client not initialized'); } // Build request body optimized for GPT-4.1 series const requestBody = { model: this.azureConfig.deploymentName, messages, temperature: settings.temperature }; // Use max_completion_tokens for newer API versions and o-series models const useMaxCompletionTokens = this.shouldUseV1API() || this.azureConfig.apiVersion >= '2024-08-01-preview' || this.azureConfig.apiVersion.includes('2025-'); if (useMaxCompletionTokens) { requestBody.max_completion_tokens = settings.max_tokens; } else { requestBody.max_tokens = settings.max_tokens; } // Handle model-specific settings based on your document const modelName = this.azureConfig.deploymentName.toLowerCase(); if (modelName.includes('o3') || modelName.includes('o4')) { // Reasoning models (o-series) don't support temperature delete requestBody.temperature; console.log(`🧠 Using reasoning model ${this.azureConfig.deploymentName} - temperature disabled`); // o3 and o4-mini include reasoning summaries and enhanced safety features console.log(`🛡️ Model ${this.azureConfig.deploymentName} includes reasoning summaries and enhanced safety`); } // Enable prompt caching for GPT-4.1 series (75% cost reduction on repeated prefixes) if (modelName.includes('gpt-4.1') && settings.enableCaching !== false) { console.log(`💰 Enabling prompt caching for GPT-4.1 (75% cost reduction on repeated content)`); // Prompt caching is automatically handled by the API for GPT-4.1 series } // Set appropriate context length for GPT-4.1 series (1M tokens) if (modelName.includes('gpt-4.1')) { console.log(`🚀 Using GPT-4.1 with 1M token context window`); } const apiType = this.shouldUseV1API() ? 'v1 API' : 'Traditional API'; console.log(`🌐 Using ${apiType} Azure OpenAI API`); console.log(`🎯 Model: ${this.azureConfig.deploymentName}`); try { const completion = await this.azureClient.chat.completions.create(requestBody); return { content: completion.choices[0]?.message?.content, usage: completion.usage, model: completion.model || this.azureConfig.deploymentName }; } catch (error) { console.error('Azure OpenAI API error details:', { message: error.message, code: error.code, type: error.type, status: error.status }); // Better error handling for common issues if (error.status === 404) { throw new Error(`Model ${this.azureConfig.deploymentName} not found in deployment. Please check your deployment name.`); } if (error.status === 429) { throw new Error(`Rate limit exceeded. Please wait and try again.`); } if (error.status === 401) { throw new Error(`Authentication failed. Please check your API key.`); } throw error; } } async callOpenAI(messages, settings) { if (!this.openaiClient) { throw new Error('OpenAI client not initialized'); } const requestBody = { model: settings.model, messages, temperature: settings.temperature }; // OpenAI doesn't have o-series models (those are Azure-only per your document) const modelName = settings.model.toLowerCase(); // Use max_completion_tokens for GPT-4.1 series (per your document) if (modelName.includes('gpt-4.1')) { requestBody.max_completion_tokens = settings.max_tokens; // Enable prompt caching for GPT-4.1 if (modelName.includes('gpt-4.1')) { console.log(`💰 Enabling prompt caching for GPT-4.1 (75% cost reduction on repeated content)`); } } else { requestBody.max_tokens = settings.max_tokens; } try { const completion = await this.openaiClient.chat.completions.create(requestBody); return { content: completion.choices[0]?.message?.content, usage: completion.usage, model: completion.model }; } catch (error) { console.error('OpenAI API error details:', { message: error.message, code: error.code, type: error.type, status: error.status }); throw error; } } /** * Select the optimal model based on commit complexity * Updated for GPT-4.1 series with improved thresholds */ selectModelForCommit(commitInfo) { const filesChanged = commitInfo.files?.length || 0; const linesChanged = (commitInfo.additions || 0) + (commitInfo.deletions || 0); const hasBreakingChanges = commitInfo.message?.includes('BREAKING') || commitInfo.message?.includes('!:'); const hasComplexLogic = commitInfo.message?.toLowerCase().includes('refactor') || commitInfo.message?.toLowerCase().includes('algorithm') || commitInfo.message?.toLowerCase().includes('architecture'); // Very large or complex changes requiring advanced reasoning // Note: o-series models are Azure-only if (filesChanged > 100 || linesChanged > 5000 || hasBreakingChanges || hasComplexLogic) { if (this.activeProvider === 'azure') { // Use Azure reasoning models for complex analysis if (filesChanged > 200 || linesChanged > 10000) { return this.modelConfig.advanced_reasoning; // o4 for maximum reasoning } return this.modelConfig.reasoning; // o4-mini for advanced reasoning } else { // OpenAI fallback to GPT-4.1 for complex analysis return this.modelConfig.complex; // gpt-4.1 for complex analysis } } // Complex commit criteria - use flagship GPT-4.1 model if (filesChanged > 20 || linesChanged > 1000) { return this.modelConfig.complex; // gpt-4.1 for complex analysis with 1M token context } // Very simple commits - use ultra-efficient nano model if (filesChanged < 3 && linesChanged < 50) { return this.modelConfig.nano; // gpt-4.1-nano for minimal changes } // Simple commit criteria - use efficient mini model if (filesChanged < 10 && linesChanged < 200) { return this.modelConfig.simple; // gpt-4.1-mini for quick analysis } // Default for most commits - balanced GPT-4.1 performance return this.modelConfig.default; // gpt-4.1 for balanced analysis } /** * Check if model supports specific capabilities * Based on actual model documentation, not hallucinated features */ getModelCapabilities(modelName) { const model = modelName.toLowerCase(); return { // Reasoning models (Azure-only: o3 series, o4 series) reasoning: model.includes('o3') || model.includes('o4'), // Vision capabilities (not needed for changelog generation) vision: false, // Context windows largeContext: model.includes('gpt-4.1'), // 1M tokens mediumContext: model.includes('o3') || model.includes('o4'), // 200K tokens for reasoning models standardContext: model.includes('gpt-4o'), // 128K tokens // Prompt caching (GPT-4.1 series only, 75% discount) promptCaching: model.includes('gpt-4.1'), // Text-only capabilities (relevant for changelog generation) textGeneration: true, // Function calling (available on modern models) tools: model.includes('o3') || model.includes('o4') || model.includes('gpt-4'), // Azure-specific reasoning features parallelToolCalling: model.includes('o3') || model.includes('o4'), reasoningSummary: model.includes('o3') || model.includes('o4'), // Latest generation features (o4 series only) latestReasoning: model.includes('o4'), // Cost efficiency indicators ultraEfficient: model.includes('nano'), costEfficient: model.includes('mini'), // Coding optimization (GPT-4.1 series specialization) codingOptimized: model.includes('gpt-4.1') }; } /** * Validate model availability for the current provider * with actual deployment checking for Azure */ async validateModelAvailability(modelName) { if (this.activeProvider === 'azure') { try { // Check if model exists in Azure deployment by attempting a minimal completion const testResponse = await this.azureClient.chat.completions.create({ model: modelName, messages: [{ role: 'user', content: 'test' }], max_completion_tokens: 1, temperature: 0 }); const capabilities = this.getModelCapabilities(modelName); // Log capabilities for user awareness if (capabilities.reasoning) { console.log(`🧠 Model ${modelName} supports advanced reasoning`); } if (capabilities.largeContext) { console.log(`📚 Model ${modelName} supports 1M token context window`); } if (capabilities.promptCaching) { console.log(`💰 Model ${modelName} supports prompt caching (75% cost reduction)`); } return { available: true, model: testResponse.model, capabilities }; } catch (error) { // Only log model availability errors once per session to avoid spam if (!this.modelErrorsLogged) { this.modelErrorsLogged = new Set(); } if (!this.modelErrorsLogged.has(modelName)) { // Clean up error message for better UX const cleanError = error.message.includes('400') ? `Model not available in deployment` : 'Connection failed'; console.warn(`⚠️ Model ${modelName} not available in Azure deployment: ${cleanError}`); this.modelErrorsLogged.add(modelName); } return { available: false, error: error.message, capabilities: null }; } } if (this.activeProvider === 'openai') { // For OpenAI, exclude Azure-only models (o-series, audio, image gen) const supportedModels = [ 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'gpt-4', 'gpt-3.5-turbo' ]; const available = supportedModels.includes(modelName); const capabilities = available ? this.getModelCapabilities(modelName) : null; return { available, capabilities }; } return { available: false, error: 'No provider configured' }; } /** * Get available models in Azure deployment */ async getAvailableAzureModels() { if (!this.isAzureConfigured()) { return { success: false, error: 'Azure not configured' }; } try { // Try common model names for changelog generation const commonModels = [ // GPT-4.1 series (available on both platforms) 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', // GPT-4o series (available on both platforms) 'gpt-4o', 'gpt-4o-mini', // o-series reasoning models (Azure-only) 'o4', 'o4-mini', // Latest generation 'o3', 'o3-mini', // Previous generation // Legacy models for fallback 'gpt-4', 'gpt-35-turbo' ]; const availableModels = []; for (const model of commonModels) { const validation = await this.validateModelAvailability(model); if (validation.available) { availableModels.push({ name: model, capabilities: validation.capabilities }); } } return { success: true, models: availableModels }; } catch (error) { return { success: false, error: error.message }; } } /** * Select optimal model with availability fallback */ async selectOptimalModel(commitInfo = null) { const preferredModel = commitInfo ? this.selectModelForCommit(commitInfo) : this.modelConfig.default; // Validate preferred model availability const validation = await this.validateModelAvailability(preferredModel); if (validation.available) { return { model: preferredModel, capabilities: validation.capabilities }; } // Fallback logic based on provider if (this.activeProvider === 'azure') { // Only show fallback message once to avoid spam if (!this.fallbackMessageShown) { console.warn(`⚠️ Preferred model ${preferredModel} not available, finding fallback...`); this.fallbackMessageShown = true; } // Try fallback models in order of preference const fallbackModels = [ this.modelConfig.default, this.modelConfig.simple, 'gpt-4o', 'gpt-4', 'gpt-35-turbo' ]; for (const fallback of fallbackModels) { if (fallback !== preferredModel) { const fallbackValidation = await this.validateModelAvailability(fallback); if (fallbackValidation.available) { if (!this.fallbackSuccessShown) { console.log(`✅ Using fallback model: ${fallback}`); this.fallbackSuccessShown = true; } return { model: fallback, capabilities: fallbackValidation.capabilities }; } } } throw new Error('No available models found in Azure deployment'); } // For OpenAI, use the preferred model (assume available) return { model: preferredModel, capabilities: this.getModelCapabilities(preferredModel) }; } // Test the AI provider connection async testConnection() { if (!this.isAvailable) { return { success: false, error: 'No provider available' }; } try { const response = await this.generateCompletion([{ role: 'user', content: "Test connection - please respond with 'OK'" }], { max_tokens: 10 }); if (response && response.content && response.content.length > 0) { return { success: true, response: response.content, model: response.model }; } else { return { success: false, error: 'Empty response' }; } } catch (error) { console.error('AI Provider test failed:', error.message); return { success: false, error: error.message }; } } } module.exports = AIProvider;