UNPKG

@entro314labs/ai-changelog-generator

Version:

AI-powered changelog generator with MCP server support - works with most providers, online and local models

297 lines (261 loc) 10.8 kB
import { AzureOpenAI } from 'openai'; import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity'; import { BaseProvider } from '../core/base-provider.js'; import { ProviderError } from '../../../shared/utils/utils.js'; import { applyMixins, ProviderResponseHandler } from '../utils/base-provider-helpers.js'; import { buildClientOptions } from '../utils/provider-utils.js'; class AzureOpenAIProvider extends BaseProvider { constructor(config) { super(config); this.azureClient = null; this._cachedDeployments = null; this._deploymentsCacheTime = 0; // Only initialize if properly configured if (this.isAvailable()) { try { this.initializeClient(); } catch (error) { // Don't throw during construction, just log console.warn(`Azure provider initialization warning: ${error.message}`); } } } initializeClient() { // Check if using API key or Azure AD authentication if (this.config.AZURE_USE_AD_AUTH === 'true') { try { const credential = new DefaultAzureCredential(); const azureADTokenProvider = getBearerTokenProvider( credential, 'https://cognitiveservices.azure.com/.default' ); this.azureClient = new AzureOpenAI({ azureADTokenProvider, apiVersion: this.config.AZURE_OPENAI_API_VERSION || '2025-04-01-preview', endpoint: this.config.AZURE_OPENAI_ENDPOINT, timeout: 30000, // Reduced timeout for individual requests maxRetries: 2, // Add retry logic defaultQuery: { 'api-version': '2025-04-01-preview' } }); } catch (error) { console.error('Failed to initialize Azure AD authentication:', error.message); // Fallback to API key if AD auth fails this.initializeWithApiKey(); } } else { this.initializeWithApiKey(); } } initializeWithApiKey() { this.azureClient = new AzureOpenAI({ apiKey: this.config.AZURE_OPENAI_KEY, apiVersion: this.config.AZURE_OPENAI_API_VERSION || '2025-04-01-preview', endpoint: this.config.AZURE_OPENAI_ENDPOINT, timeout: 30000, // Reduced timeout for individual requests maxRetries: 2, // Add retry logic defaultQuery: { 'api-version': '2025-04-01-preview' } }); } getName() { return 'azure'; } isAvailable() { const { AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY, AZURE_USE_AD_AUTH } = this.config; // Must have endpoint, and either API key or AD auth enabled const hasEndpoint = AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_ENDPOINT.trim() !== ''; const hasAuth = (AZURE_OPENAI_KEY && AZURE_OPENAI_KEY.trim() !== '') || AZURE_USE_AD_AUTH === 'true'; return hasEndpoint && hasAuth; } async generateCompletion(messages, options = {}) { return ProviderResponseHandler.executeWithErrorHandling( this, 'generate_completion', async () => { const modelConfig = this.getProviderModelConfig(); // In Azure, the model is the deployment name. const deploymentName = options.model || this.config.AZURE_OPENAI_DEPLOYMENT_NAME || modelConfig.standardModel; if (!deploymentName) { throw new Error('Azure deployment name is not configured. Please set AZURE_OPENAI_DEPLOYMENT_NAME or pass a model (deployment name) in the options'); } const params = { model: deploymentName, messages, max_tokens: options.max_tokens || 2000, // Reduced for faster responses temperature: options.temperature || 0.3, user: options.user || this.config.AZURE_USER_ID, }; // Add tool calling if provided if (options.tools) { params.tools = options.tools; params.tool_choice = options.tool_choice || 'auto'; } // Add data sources for Azure-specific features like "On Your Data" if (options.dataSources) { params.data_sources = options.dataSources; } // Add streaming if requested if (options.stream) { params.stream = true; const stream = await this.azureClient.chat.completions.create(params); return { stream, model: deploymentName }; } // Add additional timeout wrapper for better control const completion = await Promise.race([ this.azureClient.chat.completions.create(params), new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout after 25 seconds')), 25000) ) ]); if (!completion.choices?.length || !completion.choices[0]?.message?.content) { const finishReason = completion.choices?.[0]?.finish_reason; const errorMsg = finishReason === 'length' ? `Response truncated due to token limit (max_tokens: ${params.max_tokens}). Consider increasing max_tokens or reducing prompt size.` : 'Empty response from Azure API'; throw new Error(errorMsg); } const content = completion.choices[0].message.content; const finishReason = completion.choices[0].finish_reason; // Warn if response was truncated but still return the partial content if (finishReason === 'length') { console.warn(`⚠️ Azure response truncated due to token limit (${params.max_tokens}). Response may be incomplete.`); } // Extract Azure-specific content filter results if present let contentFilters = null; if (completion.choices[0].content_filter_results) { contentFilters = completion.choices[0].content_filter_results; } return { content: content, model: completion.model, tokens: completion.usage.total_tokens, usage: { prompt_tokens: completion.usage.prompt_tokens, completion_tokens: completion.usage.completion_tokens, total_tokens: completion.usage.total_tokens }, finish_reason: finishReason, tool_calls: completion.choices[0].message.tool_calls, content_filters: contentFilters }; }, { model: options.model } ); } // Azure-specific helper methods getDeploymentName() { return this.config.AZURE_OPENAI_DEPLOYMENT_NAME || this.getProviderModelConfig().standardModel; } getModelContextWindow(modelName) { const contextWindows = { // Latest 2025 models (Azure exclusive) 'o4': 500000, 'o4-mini': 200000, 'o3': 300000, 'o3-mini': 150000, // Standard 2025 models 'gpt-4o': 128000, 'gpt-4o-mini': 128000, 'o1': 200000, 'o1-mini': 128000, 'gpt-4.1': 200000, 'gpt-4.1-mini': 200000, 'gpt-4.1-nano': 200000, // Legacy models 'gpt-4': 8192, 'gpt-4-32k': 32768, 'gpt-4-turbo': 128000, 'gpt-35-turbo': 4096, 'gpt-35-turbo-16k': 16384 }; return contextWindows[modelName] || 128000; } getModelCapabilities(modelName) { return { reasoning: modelName.includes('o1') || modelName.includes('o3') || modelName.includes('o4') || modelName.includes('gpt-4'), function_calling: !modelName.includes('o1') && !modelName.includes('o3') && !modelName.includes('o4'), // o-series models don't support function calling json_mode: true, multimodal: modelName.includes('gpt-4o') || modelName.includes('gpt-4.1'), largeContext: modelName.includes('4.1') || modelName.includes('o1') || modelName.includes('o3') || modelName.includes('o4') || modelName.includes('4o'), promptCaching: modelName.includes('4.1'), advancedReasoning: modelName.includes('o3') || modelName.includes('o4'), azureExclusive: modelName.includes('o3') || modelName.includes('o4') }; } // Azure-specific method for testing deployment availability async testDeployment(deploymentName) { try { const response = await this.azureClient.chat.completions.create({ model: deploymentName, messages: [{ role: 'user', content: 'Test' }], max_tokens: 1 }); return { success: true, deployment: deploymentName, model: response.model }; } catch (error) { return { success: false, error: error.message, deployment: deploymentName }; } } // Get available deployments by testing common model names async getAvailableModels() { if (!this.isAvailable()) return []; // Cache the result to avoid repeated API calls if (this._cachedDeployments && Date.now() - this._deploymentsCacheTime < 300000) { // 5 min cache return this._cachedDeployments; } // Get base config directly to avoid recursion const baseConfig = { commonDeployments: ['o4', 'o3', 'gpt-4.1', 'gpt-4o', 'gpt-35-turbo', 'o1'], fallbacks: ['gpt-4.1', 'gpt-4o', 'o1', 'gpt-35-turbo'] }; const potentialDeployments = [ // User configured deployment this.config.AZURE_OPENAI_DEPLOYMENT_NAME, // Common deployment names ...baseConfig.commonDeployments, // Fallback models ...baseConfig.fallbacks ].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i); // Remove duplicates const availableDeployments = []; // Test each potential deployment in parallel (but limit concurrency) const testPromises = potentialDeployments.slice(0, 8).map(async (deployment) => { const result = await this.testDeployment(deployment); if (result.success) { availableDeployments.push(deployment); } return result; }); try { await Promise.allSettled(testPromises); // Cache the result this._cachedDeployments = availableDeployments; this._deploymentsCacheTime = Date.now(); if (availableDeployments.length === 0) { console.warn('⚠️ No Azure deployments found. Using configured deployment name as fallback.'); // Fallback to configured deployment even if untested, preferring latest models const fallback = this.config.AZURE_OPENAI_DEPLOYMENT_NAME || 'o4' || 'gpt-4.1'; return [fallback]; } return availableDeployments; } catch (error) { console.warn('⚠️ Failed to detect Azure deployments:', error.message); // Return common deployments as fallback return baseConfig.commonDeployments; } } // Force refresh of available deployments async refreshAvailableModels() { this._cachedDeployments = null; this._deploymentsCacheTime = 0; return await this.getAvailableModels(); } } // Apply mixins to add standard provider functionality export default applyMixins(AzureOpenAIProvider, 'azure');