visaire
Version:
A fully autonomous agentic CLI tool for interacting with large language models (Claude, Gemini, GPT) that can execute multi-step tasks automatically
339 lines (291 loc) • 9.11 kB
JavaScript
const axios = require('axios');
const Utils = require('./utils');
/**
* LLM Provider implementations for Claude, Gemini, and GPT
*/
class Providers {
constructor(config = {}) {
this.timeout = config.timeout || 30000;
this.maxRetries = config.maxRetries || 3;
}
/**
* Make API call to Claude (Anthropic)
*/
async callClaude(apiKey, prompt, options = {}) {
const url = 'https://api.anthropic.com/v1/messages';
const headers = {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
};
const model = this.getModelForProvider(options.model, 'claude');
const data = {
model: model,
max_tokens: options.maxTokens || 4000,
messages: [
{
role: 'user',
content: prompt
}
]
};
try {
const response = await this.makeRequest(url, {
method: 'POST',
headers,
data,
timeout: this.timeout
});
// Extract content from Claude's response format
if (response.data && response.data.content && response.data.content[0]) {
return response.data.content[0].text;
} else {
throw new Error('Unexpected response format from Claude API');
}
} catch (error) {
throw this.handleProviderError(error, 'Claude');
}
}
/**
* Make API call to GPT (OpenAI)
*/
async callGPT(apiKey, prompt, options = {}) {
const url = 'https://api.openai.com/v1/chat/completions';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
};
const model = this.getModelForProvider(options.model, 'gpt');
const data = {
model: model,
messages: [
{
role: 'user',
content: prompt
}
],
max_tokens: options.maxTokens || 4000,
temperature: options.temperature || 0.7
};
try {
const response = await this.makeRequest(url, {
method: 'POST',
headers,
data,
timeout: this.timeout
});
// Extract content from OpenAI's response format
if (response.data && response.data.choices && response.data.choices[0]) {
return response.data.choices[0].message.content;
} else {
throw new Error('Unexpected response format from OpenAI API');
}
} catch (error) {
throw this.handleProviderError(error, 'GPT');
}
}
/**
* Make API call to Gemini (Google)
*/
async callGemini(apiKey, prompt, options = {}) {
const model = this.getModelForProvider(options.model, 'gemini');
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const headers = {
'Content-Type': 'application/json'
};
const data = {
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
temperature: options.temperature || 0.7,
topK: options.topK || 40,
topP: options.topP || 0.95,
maxOutputTokens: options.maxTokens || 4000
}
};
try {
const response = await this.makeRequest(url, {
method: 'POST',
headers,
data,
timeout: this.timeout
});
// Extract content from Gemini's response format
if (response.data && response.data.candidates && response.data.candidates[0]) {
const candidate = response.data.candidates[0];
if (candidate.content && candidate.content.parts && candidate.content.parts[0]) {
return candidate.content.parts[0].text;
}
}
throw new Error('Unexpected response format from Gemini API');
} catch (error) {
throw this.handleProviderError(error, 'Gemini');
}
}
/**
* Generic method to call any provider
*/
async call(provider, apiKey, prompt, options = {}) {
// Validate inputs
if (!provider || !apiKey || !prompt) {
throw new Error('Provider, API key, and prompt are required');
}
if (!Utils.validateApiKey(apiKey, provider)) {
throw new Error(`Invalid API key format for ${provider}`);
}
const trimmedPrompt = prompt.trim();
if (trimmedPrompt.length === 0) {
throw new Error('Prompt cannot be empty');
}
// Call the appropriate provider method
switch (provider.toLowerCase()) {
case 'claude':
return await this.callClaude(apiKey, trimmedPrompt, options);
case 'gpt':
return await this.callGPT(apiKey, trimmedPrompt, options);
case 'gemini':
return await this.callGemini(apiKey, trimmedPrompt, options);
default:
throw new Error(`Unsupported provider: ${provider}. Supported providers: claude, gpt, gemini`);
}
}
/**
* Make HTTP request with retry logic
*/
async makeRequest(url, config, retryCount = 0) {
try {
return await axios(url, config);
} catch (error) {
// Retry on network errors or 5xx server errors
const shouldRetry = retryCount < this.maxRetries && (
!error.response ||
error.response.status >= 500 ||
error.code === 'ENOTFOUND' ||
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT'
);
if (shouldRetry) {
const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff
Utils.logWarning(`Request failed, retrying in ${delay}ms... (${retryCount + 1}/${this.maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.makeRequest(url, config, retryCount + 1);
}
throw error;
}
}
/**
* Handle and format provider-specific errors
*/
handleProviderError(error, provider) {
const formattedMessage = Utils.formatError(error, provider);
// Add provider-specific context
let contextualMessage = formattedMessage;
if (error.response) {
const status = error.response.status;
switch (provider) {
case 'Claude':
if (status === 401) {
contextualMessage += '\nEnsure your Anthropic API key is valid and has the correct permissions.';
}
break;
case 'GPT':
if (status === 401) {
contextualMessage += '\nEnsure your OpenAI API key is valid and has sufficient credits.';
}
break;
case 'Gemini':
if (status === 400) {
contextualMessage += '\nCheck that your Google API key is enabled for the Gemini API.';
}
break;
}
}
const providerError = new Error(contextualMessage);
providerError.provider = provider;
providerError.originalError = error;
return providerError;
}
/**
* Get available models for each provider
*/
getAvailableModels(provider) {
const models = {
claude: [
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307',
'claude-3-opus-20240229'
],
gpt: [
'gpt-3.5-turbo',
'gpt-4',
'gpt-4-turbo-preview',
'gpt-4-32k'
],
gemini: [
'gemini-1.5-pro',
'gemini-1.5-flash',
'gemini-1.5-flash-8b',
'gemini-1.0-pro-vision-latest'
]
};
return models[provider] || [];
}
/**
* Get default model for each provider
*/
getDefaultModel(provider) {
const defaults = {
claude: 'claude-3-sonnet-20240229',
gpt: 'gpt-3.5-turbo',
gemini: 'gemini-1.5-flash'
};
return defaults[provider] || null;
}
/**
* Validate if a model is compatible with a provider
*/
validateModelForProvider(model, provider) {
if (!model || !provider) {
return false;
}
const availableModels = this.getAvailableModels(provider);
return availableModels.includes(model);
}
/**
* Get the appropriate model for a provider, with validation
*/
getModelForProvider(requestedModel, provider) {
// If no model requested, use provider default
if (!requestedModel) {
return this.getDefaultModel(provider);
}
// Validate requested model is compatible with provider
if (this.validateModelForProvider(requestedModel, provider)) {
return requestedModel;
}
// If invalid model requested, log warning and use default
Utils.logWarning(`Model '${requestedModel}' is not compatible with provider '${provider}'`);
Utils.logInfo(`Using default model '${this.getDefaultModel(provider)}' instead`);
return this.getDefaultModel(provider);
}
/**
* Test API key validity for a provider
*/
async testApiKey(provider, apiKey) {
try {
const testPrompt = 'Hello, please respond with "API key is working"';
const response = await this.call(provider, apiKey, testPrompt);
return response.toLowerCase().includes('api key is working') || response.length > 0;
} catch (error) {
return false;
}
}
}
module.exports = Providers;