optivise
Version:
Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support
240 lines • 8.4 kB
JavaScript
/**
* OpenAI Client Integration Service
* Provides embeddings and AI capabilities with automatic API key detection
*/
import OpenAI from 'openai';
import { APIKeyDetector } from './api-key-detector.js';
import { secretStore } from '../services/secret-store.js';
import { CircuitBreaker } from '../utils/circuit-breaker.js';
export class OpenAIClientService {
client = null;
keyDetector;
config = {};
breaker = new CircuitBreaker({ failureThreshold: 3, cooldownMs: 30000 });
constructor(config) {
this.keyDetector = new APIKeyDetector();
this.config = {
timeout: 30000,
maxRetries: 3,
...config
};
}
/**
* Initialize OpenAI client with automatic API key detection
*/
async initialize() {
try {
// Try provided API key first
if (this.config.apiKey) {
return this.initializeWithKey(this.config.apiKey);
}
// Try SecretStore (env/other providers)
const envKey = await secretStore.get('OPENAI_API_KEY');
if (envKey) {
return this.initializeWithKey(envKey);
}
// Auto-detect API keys from IDE configurations
const detection = await this.keyDetector.detectAPIKeys();
if (!detection.hasOpenAI) {
// Silently return false during initialization
return false;
}
// Use recommended key or first valid OpenAI key
const openAIKey = detection.recommended?.type === 'openai'
? detection.recommended
: detection.found.find(key => key.type === 'openai' && key.isValid);
if (!openAIKey) {
// Silently return false during initialization
return false;
}
// Get the actual key value
const apiKey = await this.getKeyValue(openAIKey);
if (!apiKey) {
// Silently return false during initialization
return false;
}
return this.initializeWithKey(apiKey);
}
catch (error) {
console.error('Failed to initialize OpenAI client:', error);
return false;
}
}
/**
* Initialize client with a specific API key
*/
initializeWithKey(apiKey) {
try {
this.client = new OpenAI({
apiKey,
organization: this.config.organization,
baseURL: this.config.baseURL,
timeout: this.config.timeout,
maxRetries: this.config.maxRetries
});
return true;
}
catch (error) {
console.error('Failed to initialize OpenAI client with key:', error);
return false;
}
}
/**
* Get API key value from detected source
*/
async getKeyValue(keySource) {
try {
if (keySource.environmentVar && process.env[keySource.environmentVar]) {
return process.env[keySource.environmentVar];
}
if (keySource.filePath) {
// For file-based keys, this would require reading the config file
// For now, we'll rely on environment variables
console.log(`API key found in ${keySource.filePath}, but file reading not implemented yet.`);
return null;
}
return null;
}
catch (error) {
console.error('Failed to get API key value:', error);
return null;
}
}
/**
* Check if OpenAI client is available
*/
isAvailable() {
return this.client !== null;
}
/**
* Generate embeddings for text
*/
async generateEmbedding(request) {
if (!this.client) {
console.warn('OpenAI client not initialized. Call initialize() first.');
return null;
}
if (!this.breaker.canAttempt()) {
console.warn('OpenAI circuit is open. Skipping request temporarily.');
return null;
}
try {
const model = request.model || 'text-embedding-ada-002';
const controller = new AbortController();
const timeoutMs = this.config.timeout || 30000;
const timer = setTimeout(() => controller.abort(), timeoutMs);
const response = await this.client.embeddings.create({
model,
input: request.text,
encoding_format: 'float'
}, { signal: controller.signal });
clearTimeout(timer);
if (response.data.length === 0) {
throw new Error('No embedding returned from OpenAI');
}
const result = {
embedding: response.data[0]?.embedding || [],
tokens: response.usage?.total_tokens || 0,
model: response.model
};
this.breaker.onSuccess();
return result;
}
catch (error) {
console.error('Failed to generate embedding:', error);
this.breaker.onFailure();
// Simple retry with jitter
if ((this.config.maxRetries || 0) > 0) {
const delay = 200 + Math.floor(Math.random() * 300);
await new Promise(r => setTimeout(r, delay));
this.config.maxRetries = (this.config.maxRetries || 0) - 1;
return this.generateEmbedding(request);
}
return null;
}
}
/**
* Generate embeddings for multiple texts (batch processing)
*/
async generateEmbeddings(texts, model) {
if (!this.client) {
console.warn('OpenAI client not initialized.');
return [];
}
const results = [];
// Process in batches to avoid rate limits
const batchSize = 100;
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
try {
const response = await this.client.embeddings.create({
model: model || 'text-embedding-ada-002',
input: batch,
encoding_format: 'float'
});
response.data.forEach((item, index) => {
results.push({
embedding: item.embedding,
tokens: Math.floor((response.usage?.total_tokens || 0) / response.data.length),
model: response.model
});
});
// Add delay between batches to avoid rate limiting
if (i + batchSize < texts.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
catch (error) {
console.error(`Failed to generate embeddings for batch ${i / batchSize + 1}:`, error);
// Continue with next batch
}
}
return results;
}
/**
* Test the OpenAI connection
*/
async testConnection() {
if (!this.client) {
return false;
}
try {
const response = await this.client.embeddings.create({
model: 'text-embedding-ada-002',
input: 'test',
encoding_format: 'float'
});
return response.data.length > 0;
}
catch (error) {
console.error('OpenAI connection test failed:', error);
return false;
}
}
/**
* Get client usage statistics
*/
getUsageStats() {
return {
clientInitialized: this.client !== null,
config: {
timeout: this.config.timeout,
maxRetries: this.config.maxRetries,
hasOrganization: !!this.config.organization,
hasCustomBaseURL: !!this.config.baseURL
}
};
}
/**
* Cleanup resources
*/
cleanup() {
this.client = null;
}
getCircuitState() {
return this.breaker.state();
}
}
// Singleton instance for global use
export const openAIClient = new OpenAIClientService();
//# sourceMappingURL=openai-client.js.map