@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
329 lines (296 loc) • 9.32 kB
JavaScript
import Anthropic from '@anthropic-ai/sdk';
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';
export class AnthropicProvider extends BaseProvider {
constructor(config) {
super(config);
this.anthropic = null;
if (this.isAvailable()) {
this.initializeClient();
}
}
initializeClient() {
const clientOptions = buildClientOptions(this.getProviderConfig(), {
timeout: 60000,
maxRetries: 2,
defaultHeaders: {
'X-Client-Name': 'ai-changelog-generator',
'X-Client-Version': '1.0.0'
}
});
this.anthropic = new Anthropic({
apiKey: clientOptions.apiKey,
baseURL: clientOptions.baseURL,
timeout: clientOptions.timeout,
maxRetries: clientOptions.maxRetries,
defaultHeaders: clientOptions.defaultHeaders
});
}
getName() {
return 'anthropic';
}
isAvailable() {
return !!this.config.ANTHROPIC_API_KEY;
}
getRequiredEnvVars() {
return ['ANTHROPIC_API_KEY'];
}
getDefaultModel() {
return 'claude-sonnet-4-20250514';
}
async generateCompletion(messages, options = {}) {
return ProviderResponseHandler.executeWithErrorHandling(
this,
'generate_completion',
async () => {
const modelConfig = this.getProviderModelConfig();
// Anthropic API uses a slightly different message format
const systemMessage = messages.find(m => m.role === 'system');
const userMessages = messages.filter(m => m.role !== 'system');
const params = {
model: options.model || modelConfig.standardModel || this.getDefaultModel(),
system: systemMessage ? systemMessage.content : undefined,
messages: userMessages,
max_tokens: options.max_tokens || 4096,
temperature: options.temperature || 0.3,
};
// Add tool calling if provided
if (options.tools) {
params.tools = options.tools;
params.tool_choice = options.tool_choice || 'auto';
}
// Add streaming if requested
if (options.stream) {
params.stream = true;
const stream = await this.anthropic.messages.create(params);
return { stream, model: params.model };
}
const response = await this.anthropic.messages.create(params);
// Check if there are tool calls in the response
const toolCalls = response.content.some(c => c.type === 'tool_use')
? response.content.filter(c => c.type === 'tool_use').map(c => c.tool_use)
: null;
// Get the text content
const textContent = response.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
return {
text: textContent,
content: textContent,
model: response.model,
usage: response.usage,
tokens: response.usage.input_tokens + response.usage.output_tokens,
tool_calls: toolCalls,
stop_reason: response.stop_reason,
stop_sequence: response.stop_sequence
};
},
{ model: options.model }
);
}
async generateText(messages, model = null) {
const response = await this.generateCompletion(messages, { model });
return response;
}
async getAvailableModels() {
// Anthropic doesn't provide a models endpoint, return known models
return [
{
name: 'claude-sonnet-4-20250514',
id: 'claude-sonnet-4-20250514',
description: 'Claude Sonnet 4 - Latest balanced model (2025)',
contextWindow: 200000,
capabilities: {
reasoning: true,
function_calling: true,
json_mode: true,
multimodal: true,
largeContext: true,
toolUse: true
}
},
{
name: 'claude-opus-4-20250514',
id: 'claude-opus-4-20250514',
description: 'Claude Opus 4 - Most capable model for complex tasks (2025)',
contextWindow: 200000,
capabilities: {
reasoning: true,
function_calling: true,
json_mode: true,
multimodal: true,
largeContext: true,
toolUse: true,
advancedReasoning: true
}
},
{
name: 'claude-3.7-sonnet-20250219',
id: 'claude-3.7-sonnet-20250219',
description: 'Claude 3.7 Sonnet - Enhanced reasoning capabilities',
contextWindow: 200000,
capabilities: {
reasoning: true,
function_calling: true,
json_mode: true,
multimodal: true,
largeContext: true,
toolUse: true
}
},
{
name: 'claude-3-5-sonnet-20241022',
id: 'claude-3-5-sonnet-20241022',
description: 'Claude 3.5 Sonnet - Previous generation',
contextWindow: 200000,
capabilities: {
reasoning: true,
function_calling: true,
json_mode: true,
multimodal: true
}
},
{
name: 'claude-3-5-haiku-20241022',
id: 'claude-3-5-haiku-20241022',
description: 'Claude 3.5 Haiku - Fast and efficient',
contextWindow: 200000,
capabilities: {
reasoning: true,
function_calling: true,
json_mode: true,
multimodal: true
}
}
];
}
async validateModelAvailability(modelName) {
try {
const models = await this.getAvailableModels();
const model = models.find(m => m.name === modelName);
if (model) {
return {
available: true,
model: modelName,
capabilities: model.capabilities,
contextWindow: model.contextWindow
};
} else {
const availableModels = models.map(m => m.name);
return {
available: false,
error: `Model '${modelName}' not available`,
alternatives: availableModels.slice(0, 5)
};
}
} catch (error) {
return {
available: false,
error: error.message,
alternatives: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-3.7-sonnet-20250219']
};
}
}
async testConnection() {
try {
const response = await this.generateCompletion([
{ role: 'user', content: 'Hello' }
], { max_tokens: 5 });
return {
success: true,
model: response.model,
message: 'Connection successful'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async testModel(modelName) {
try {
const response = await this.anthropic.messages.create({
model: modelName,
messages: [{ role: 'user', content: 'Test' }],
max_tokens: 1
});
return {
success: true,
model: response.model,
sdk_version: '0.56.0'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
getCapabilities(modelName) {
return {
completion: true,
streaming: true,
function_calling: true,
json_mode: true,
reasoning: true,
multimodal: true,
large_context: true
};
}
getModelRecommendation(commitDetails) {
const { files = 0, lines = 0, breaking = false, complex = false } = commitDetails;
// Use the most capable model for complex or breaking changes
if (breaking || complex || files > 20 || lines > 1000) {
return {
model: 'claude-3-5-sonnet-20241022',
reason: 'Complex or breaking change requiring advanced reasoning'
};
}
// Use standard model for medium changes
if (files > 5 || lines > 200) {
return {
model: 'claude-3-sonnet-20240229',
reason: 'Medium-sized change requiring good analysis'
};
}
// Use efficient model for small changes
return {
model: 'claude-3-haiku-20240307',
reason: 'Small change, optimized for efficiency'
};
}
async selectOptimalModel(commitInfo) {
const recommendation = this.getModelRecommendation(commitInfo);
const validation = await this.validateModelAvailability(recommendation.model);
if (validation.available) {
return {
model: recommendation.model,
reason: recommendation.reason,
capabilities: validation.capabilities
};
} else {
return {
model: this.getDefaultModel(),
reason: 'Fallback to default model',
capabilities: this.getCapabilities(this.getDefaultModel())
};
}
}
getProviderModelConfig() {
return {
smallModel: 'claude-3-5-haiku-20241022',
mediumModel: 'claude-3.7-sonnet-20250219',
standardModel: 'claude-sonnet-4-20250514',
complexModel: 'claude-opus-4-20250514',
default: 'claude-sonnet-4-20250514',
temperature: 0.3,
maxTokens: 4096
};
}
}
// Apply mixins to add standard provider functionality
export default applyMixins ? applyMixins(AnthropicProvider, 'anthropic') : AnthropicProvider;