/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ import { AIProvider, CompletionRequest, CompletionResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse, ModerationResponse, ModelInfo, HealthStatus, CostEstimate } from '@bernierllc/ai-provider-core'; import OpenAI from 'openai'; import { OpenAIProviderConfig, OpenAIFunction } from './types/openai-types'; import { OPENAI_MODELS } from './models/model-registry'; import { handleOpenAIError } from './utils/error-handling'; /** * OpenAI Provider Implementation * Implements the unified AI provider interface for OpenAI's API */ export class OpenAIProvider extends AIProvider { private client: OpenAI; private models: Map = new Map(); constructor(config: OpenAIProviderConfig) { super(config); this.client = new OpenAI({ apiKey: config.apiKey, organization: config.organizationId, baseURL: config.baseURL, timeout: config.timeout || 60000, maxRetries: config.maxRetries || 3 }); this.initializeModels(); } // ============================================ // CORE OPERATIONS (REQUIRED) // ============================================ /** * Generate text completion using OpenAI API */ async complete(request: CompletionRequest): Promise { // Validate request const validation = this.validateRequest(request); if (!validation.isValid) { return { success: false, error: validation.errors.join(', ') }; } try { const completion = await this.client.chat.completions.create({ model: request.model || this.config.defaultModel || 'gpt-4-turbo', messages: request.messages.map(msg => ({ role: msg.role as 'system' | 'user' | 'assistant', content: msg.content, name: msg.name })), max_tokens: request.maxTokens, temperature: request.temperature, top_p: request.topP, frequency_penalty: request.frequencyPenalty, presence_penalty: request.presencePenalty, stop: request.stop, user: request.user }); const choice = completion.choices[0]; // Map OpenAI's finish_reason to base interface types const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call' ? 'function_call' as const : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined; return { success: true, content: choice.message.content || '', finishReason, usage: completion.usage ? { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens } : undefined, model: completion.model, metadata: { id: completion.id, created: completion.created, systemFingerprint: completion.system_fingerprint } }; } catch (error) { const aiError = handleOpenAIError(error); return { success: false, error: aiError.message }; } } /** * Generate streaming text completion */ async *streamComplete( request: CompletionRequest ): AsyncGenerator { const validation = this.validateRequest(request); if (!validation.isValid) { throw new Error(validation.errors.join(', ')); } try { const stream = await this.client.chat.completions.create({ model: request.model || this.config.defaultModel || 'gpt-4-turbo', messages: request.messages.map(msg => ({ role: msg.role as 'system' | 'user' | 'assistant', content: msg.content })), max_tokens: request.maxTokens, temperature: request.temperature, stream: true }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content || ''; const rawFinishReason = chunk.choices[0]?.finish_reason; // Map finish_reason for streaming (StreamChunk doesn't include function_call) const finishReason = (rawFinishReason === 'stop' || rawFinishReason === 'length' || rawFinishReason === 'content_filter') ? rawFinishReason : undefined; yield { delta, finishReason, usage: chunk.usage ? { promptTokens: chunk.usage.prompt_tokens, completionTokens: chunk.usage.completion_tokens, totalTokens: chunk.usage.total_tokens } : undefined }; } } catch (error) { throw handleOpenAIError(error); } } /** * Generate embeddings using OpenAI embeddings API */ async generateEmbeddings( request: EmbeddingRequest ): Promise { try { const response = await this.client.embeddings.create({ model: request.model || 'text-embedding-3-small', input: request.input, user: request.user }); return { success: true, embeddings: response.data.map(item => item.embedding), usage: { promptTokens: response.usage.prompt_tokens, completionTokens: 0, totalTokens: response.usage.total_tokens }, model: response.model }; } catch (error) { const aiError = handleOpenAIError(error); return { success: false, error: aiError.message }; } } /** * Check content moderation using OpenAI moderation API */ async moderate(content: string): Promise { try { const response = await this.client.moderations.create({ input: content }); const result = response.results[0]; return { success: true, flagged: result.flagged, categories: result.categories as unknown as Record, categoryScores: result.category_scores as unknown as Record }; } catch (error) { const aiError = handleOpenAIError(error); return { success: false, flagged: false, error: aiError.message }; } } /** * Get available OpenAI models */ async getAvailableModels(): Promise { try { const response = await this.client.models.list(); return response.data .filter(model => model.id.startsWith('gpt-') || model.id.startsWith('text-embedding') ) .map(model => this.mapOpenAIModel(model)); } catch { // Return cached models if API fails return Array.from(this.models.values()); } } /** * Check OpenAI API health */ async checkHealth(): Promise { const startTime = Date.now(); try { // Simple test request to verify API connectivity await this.client.models.retrieve('gpt-3.5-turbo'); return { status: 'healthy', latency: Date.now() - startTime, lastChecked: new Date() }; } catch (error) { return { status: 'unavailable', latency: Date.now() - startTime, lastChecked: new Date(), details: { error: error instanceof Error ? error.message : 'Unknown error' } }; } } // ============================================ // OPENAI-SPECIFIC FEATURES // ============================================ /** * Chat completion with function calling */ async completionWithFunctions( request: CompletionRequest & { functions: OpenAIFunction[] } ): Promise { try { const completion = await this.client.chat.completions.create({ model: request.model || 'gpt-4-turbo', messages: request.messages.map(msg => ({ role: msg.role as 'system' | 'user' | 'assistant', content: msg.content })), functions: request.functions, function_call: 'auto' }); const choice = completion.choices[0]; // Map finish_reason to base types const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call' ? 'function_call' as const : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined; return { success: true, content: choice.message.content || '', finishReason, usage: completion.usage ? { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens } : undefined, model: completion.model, metadata: { functionCall: choice.message.function_call } }; } catch (error) { const aiError = handleOpenAIError(error); return { success: false, error: aiError.message }; } } /** * Vision capabilities (GPT-4 Vision) */ async analyzeImage( imageUrl: string, prompt: string, model: string = 'gpt-4-vision-preview' ): Promise { try { const completion = await this.client.chat.completions.create({ model, messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', image_url: { url: imageUrl } } ] } ], max_tokens: 1000 }); const choice = completion.choices[0]; // Map finish_reason to base types const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call' ? 'function_call' as const : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined; return { success: true, content: choice.message.content || '', finishReason, usage: completion.usage ? { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens } : undefined, model: completion.model }; } catch (error) { const aiError = handleOpenAIError(error); return { success: false, error: aiError.message }; } } // ============================================ // COST ESTIMATION (OVERRIDE) // ============================================ /** * Estimate cost using OpenAI pricing */ estimateCost(request: CompletionRequest): CostEstimate { const model = request.model || this.config.defaultModel || 'gpt-4-turbo'; const pricing = this.getModelPricing(model); const inputTokens = this.estimateTokens( request.messages.map(m => m.content).join(' ') ); const outputTokens = request.maxTokens || 1000; const inputCost = (inputTokens / 1000) * pricing.inputPrice; const outputCost = (outputTokens / 1000) * pricing.outputPrice; return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, estimatedCostUSD: inputCost + outputCost, currency: 'USD' }; } // ============================================ // PRIVATE METHODS // ============================================ private initializeModels() { OPENAI_MODELS.forEach(model => { this.models.set(model.id, model); }); } private mapOpenAIModel(model: OpenAI.Model): ModelInfo { const cached = this.models.get(model.id); if (cached) { return cached; } return { id: model.id, name: model.id, contextWindow: 8192, // Default maxOutputTokens: 4096, // Default capabilities: ['chat'] }; } private getModelPricing(model: string): { inputPrice: number; outputPrice: number } { const modelInfo = this.models.get(model); if (modelInfo?.pricing) { return { inputPrice: modelInfo.pricing.inputPricePerToken * 1000, outputPrice: modelInfo.pricing.outputPricePerToken * 1000 }; } // Default pricing (gpt-3.5-turbo) return { inputPrice: 0.0005, outputPrice: 0.0015 }; } }