@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
300 lines (299 loc) • 11.2 kB
JavaScript
/**
* Common utility functions for AI providers
* Consolidates duplicate logic across all provider implementations
*/
/**
* Serialize the complete AWS credential set as one secret value. Config and
* keychain backends encrypt/protect this whole value, avoiding secret material
* in metadata or VS Code settings.
*/
export function encodeBedrockCredential(bundle) {
const accessKeyId = bundle.accessKeyId?.trim();
const secretAccessKey = bundle.secretAccessKey?.trim();
if (!/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(accessKeyId)) {
throw new Error('AWS access key ID must be a 20-character AKIA or ASIA key');
}
if (!secretAccessKey) {
throw new Error('AWS secret access key is required');
}
return JSON.stringify({
accessKeyId,
secretAccessKey,
...(bundle.sessionToken?.trim() ? { sessionToken: bundle.sessionToken.trim() } : {}),
...(bundle.region?.trim() ? { region: bundle.region.trim() } : {}),
});
}
export function decodeBedrockCredential(value) {
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
}
catch {
throw new Error('Bedrock secure credentials must include both an AWS access key ID and secret access key');
}
}
if (!parsed || typeof parsed !== 'object') {
throw new Error('Invalid Bedrock credential bundle');
}
const candidate = parsed;
const encoded = encodeBedrockCredential({
accessKeyId: candidate.accessKeyId || '',
secretAccessKey: candidate.secretAccessKey || '',
sessionToken: candidate.sessionToken,
region: candidate.region,
});
return JSON.parse(encoded);
}
/**
* Standard model selection algorithm based on commit details
* @param {Object} commitDetails - Details about the commit (files, lines, breaking, complex)
* @param {Object} modelConfig - Provider-specific model configuration
* @returns {Object} Selected model and reason
*/
export function selectModelByComplexity(commitDetails, modelConfig) {
const { complexModel = 'default-complex', standardModel = 'default-standard', mediumModel = 'default-medium', smallModel = 'default-small', } = modelConfig;
// Breaking or complex changes need the most capable model
if (commitDetails.breaking || commitDetails.complex || commitDetails.files > 20) {
return {
model: complexModel,
reason: 'Complex or breaking change requiring advanced reasoning',
};
}
// Large commits need standard model
if (commitDetails.lines > 1000 || commitDetails.files > 10) {
return {
model: standardModel,
reason: 'Large change requiring standard capabilities',
};
}
// Medium commits
if (commitDetails.lines > 200 || commitDetails.files > 5) {
return {
model: mediumModel,
reason: 'Medium-sized change',
};
}
// Small commits can use the most efficient model
return {
model: smallModel,
reason: 'Small change, optimized for efficiency',
};
}
/**
* Standard connection test implementation
* @param {Function} generateCompletion - Provider's generateCompletion method
* @param {string} defaultModel - Default model to use for testing
* @returns {Promise<Object>} Test result with success status
*/
export async function standardConnectionTest(generateCompletion, defaultModel) {
try {
const response = await generateCompletion([{ role: 'user', content: 'Test connection' }], {
max_tokens: 5,
model: defaultModel,
temperature: 0.1,
});
return {
success: true,
response: response.content || response.text || 'Connection successful',
model: response.model || defaultModel,
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}
/**
* Generate standard error response for provider operations
* @param {string} providerName - Name of the provider
* @param {string} operation - Operation that failed
* @param {string} reason - Reason for failure
* @param {Array} alternatives - Alternative suggestions
* @returns {Object} Standardized error response
*/
export function createProviderErrorResponse(providerName, operation, reason, alternatives = []) {
return {
available: false,
provider: providerName,
operation,
error: reason,
alternatives: alternatives.length > 0 ? alternatives : [`Check ${providerName} configuration`],
timestamp: new Date().toISOString(),
};
}
/**
* Generate standard success response for provider operations
* @param {string} providerName - Name of the provider
* @param {Object} data - Success data
* @returns {Object} Standardized success response
*/
export function createProviderSuccessResponse(providerName, data = {}) {
return {
available: true,
provider: providerName,
...data,
timestamp: new Date().toISOString(),
};
}
/**
* Build common model capabilities structure
* @param {Object} baseCapabilities - Base capabilities all models have
* @param {Object} modelSpecificCapabilities - Capabilities specific to the model
* @returns {Object} Complete capabilities object
*/
export function buildCapabilities(baseCapabilities = {}, modelSpecificCapabilities = {}) {
const defaultCapabilities = {
vision: false,
tool_use: false,
json_mode: false,
reasoning: false,
large_context: false,
streaming: false,
temperature_control: true,
max_tokens_control: true,
};
return {
...defaultCapabilities,
...baseCapabilities,
...modelSpecificCapabilities,
};
}
/**
* Extract provider configuration with defaults
* @param {Object} config - Full configuration object
* @param {string} providerPrefix - Provider prefix (e.g., 'OPENAI', 'ANTHROPIC')
* @param {Object} defaults - Default values for missing config
* @returns {Object} Provider-specific configuration
*/
export function extractProviderConfig(config, providerPrefix, defaults = {}) {
const providerConfig = {};
// Extract all config keys that start with the provider prefix
Object.keys(config).forEach((key) => {
if (key.startsWith(`${providerPrefix}_`) || key === providerPrefix) {
providerConfig[key] = config[key];
}
});
// Add fallback AI_MODEL configs
if (config.AI_MODEL && !providerConfig[`${providerPrefix}_MODEL`]) {
providerConfig[`${providerPrefix}_MODEL`] = config.AI_MODEL;
}
// Apply defaults for missing values
Object.keys(defaults).forEach((key) => {
if (providerConfig[key] === undefined) {
providerConfig[key] = defaults[key];
}
});
return providerConfig;
}
/**
* Parse numeric configuration values with defaults
* @param {string|number} value - Value to parse
* @param {number} defaultValue - Default if parsing fails
* @returns {number} Parsed numeric value
*/
export function parseNumericConfig(value, defaultValue) {
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
}
/**
* Standard model validation logic
* @param {Function} testModelFn - Function to test if model is available
* @param {string} modelName - Model name to validate
* @param {Array} fallbackModels - Fallback models to suggest
* @returns {Promise<Object>} Validation result
*/
export async function validateModelWithFallbacks(testModelFn, modelName, fallbackModels = []) {
try {
const result = await testModelFn(modelName);
if (result.success || result.available) {
return createProviderSuccessResponse('validation', {
model: modelName,
capabilities: result.capabilities || {},
});
}
return createProviderErrorResponse('validation', 'model_test', result.error || 'Model not available', fallbackModels);
}
catch (error) {
return createProviderErrorResponse('validation', 'model_test', error.message, fallbackModels);
}
}
/**
* Common message formatting for different provider APIs
* @param {Array} messages - OpenAI-style messages
* @param {string} format - Target format ('openai', 'anthropic', 'google')
* @returns {Object} Formatted messages for the specific provider
*/
export function formatMessagesForProvider(messages, format) {
switch (format) {
case 'anthropic': {
const systemMessage = messages.find((m) => m.role === 'system');
const userMessages = messages.filter((m) => m.role !== 'system');
return {
system: systemMessage ? systemMessage.content : undefined,
messages: userMessages,
};
}
case 'google': {
const systemInstruction = messages.find((m) => m.role === 'system')?.content;
const history = messages
.filter((m) => m.role !== 'system')
.map((m) => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text: m.content }],
}));
return { systemInstruction, history };
}
default:
return { messages };
}
}
/**
* Build standard client options for providers
* @param {Object} config - Provider configuration
* @param {Object} defaults - Default client options
* @returns {Object} Client initialization options
*/
export function buildClientOptions(config, defaults = {}) {
const options = { ...defaults };
// Common timeout handling
if (config.timeout || config.TIMEOUT) {
options.timeout = parseNumericConfig(config.timeout || config.TIMEOUT, 60000);
}
// Common retry handling
if (config.maxRetries || config.MAX_RETRIES) {
options.maxRetries = parseNumericConfig(config.maxRetries || config.MAX_RETRIES, 2);
}
// Common base URL handling
if (config.baseURL || config.BASE_URL || config.API_URL) {
options.baseURL = config.baseURL || config.BASE_URL || config.API_URL;
}
return options;
}
/**
* Standard request parameters builder
* @param {Array} messages - Chat messages
* @param {Object} options - Request options
* @param {Object} defaults - Provider-specific defaults
* @returns {Object} Standard request parameters
*/
export function buildRequestParams(messages, options, defaults = {}) {
return {
messages,
model: options.model || defaults.model,
max_tokens: options.max_tokens || defaults.max_tokens || 1000,
temperature: options.temperature !== undefined ? options.temperature : defaults.temperature || 0.3,
stream: !!options.stream,
...(options.tools && { tools: options.tools }),
...(options.tool_choice && { tool_choice: options.tool_choice }),
...defaults.extraParams,
};
}