/* 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 { OpenAIProvider } from '../src/OpenAIProvider'; import { OpenAIProviderConfig } from '../src/types/openai-types'; import OpenAI from 'openai'; // Mock the OpenAI SDK jest.mock('openai'); describe('OpenAIProvider', () => { let provider: OpenAIProvider; let mockClient: jest.Mocked; const config: OpenAIProviderConfig = { providerName: 'openai', apiKey: 'test-api-key', defaultModel: 'gpt-4-turbo', version: '1.0.0' }; beforeEach(() => { jest.clearAllMocks(); // Setup mock OpenAI client with properly typed Jest mocks const mockChatCompletionsCreate = jest.fn() as jest.Mock; const mockEmbeddingsCreate = jest.fn() as jest.Mock; const mockModerationsCreate = jest.fn() as jest.Mock; const mockModelsList = jest.fn() as jest.Mock; const mockModelsRetrieve = jest.fn() as jest.Mock; mockClient = { chat: { completions: { create: mockChatCompletionsCreate as any } }, embeddings: { create: mockEmbeddingsCreate as any }, moderations: { create: mockModerationsCreate as any }, models: { list: mockModelsList as any, retrieve: mockModelsRetrieve as any } } as unknown as jest.Mocked; (OpenAI as jest.MockedClass).mockImplementation(() => mockClient); provider = new OpenAIProvider(config); }); describe('Constructor', () => { it('should create provider with valid config', () => { expect(provider).toBeInstanceOf(OpenAIProvider); expect(provider.getProviderName()).toBe('openai'); expect(provider.getProviderVersion()).toBe('1.0.0'); }); it('should initialize OpenAI client with correct options', () => { const customConfig: OpenAIProviderConfig = { providerName: 'openai', apiKey: 'test-key', organizationId: 'org-123', baseURL: 'https://custom.openai.com', timeout: 30000, maxRetries: 5 }; new OpenAIProvider(customConfig); expect(OpenAI).toHaveBeenCalledWith({ apiKey: 'test-key', organization: 'org-123', baseURL: 'https://custom.openai.com', timeout: 30000, maxRetries: 5 }); }); it('should use default timeout and maxRetries if not provided', () => { expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ timeout: 60000, maxRetries: 3 })); }); }); describe('complete()', () => { it('should generate completion successfully', async () => { const mockResponse = { id: 'chatcmpl-123', model: 'gpt-4-turbo', created: Date.now(), system_fingerprint: 'fp-123', choices: [ { message: { role: 'assistant', content: 'This is a test response' }, finish_reason: 'stop' } ], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 } }; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.complete({ messages: [ { role: 'user', content: 'Hello' } ] }); expect(result.success).toBe(true); expect(result.content).toBe('This is a test response'); expect(result.finishReason).toBe('stop'); expect(result.usage).toEqual({ promptTokens: 10, completionTokens: 20, totalTokens: 30 }); expect(result.model).toBe('gpt-4-turbo'); }); it('should handle empty message content', async () => { const mockResponse = { id: 'chatcmpl-123', model: 'gpt-4-turbo', created: Date.now(), choices: [ { message: { role: 'assistant', content: null }, finish_reason: 'stop' } ] }; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.complete({ messages: [{ role: 'user', content: 'Hello' }] }); expect(result.success).toBe(true); expect(result.content).toBe(''); }); it('should pass all request parameters to OpenAI', async () => { (mockClient.chat.completions.create as jest.Mock).mockResolvedValue({ choices: [{ message: { content: 'test' }, finish_reason: 'stop' }] } as any); await provider.complete({ messages: [{ role: 'user', content: 'Test' }], model: 'gpt-3.5-turbo', maxTokens: 100, temperature: 0.8, topP: 0.9, frequencyPenalty: 0.5, presencePenalty: 0.6, stop: ['STOP'], user: 'user-123' }); expect(mockClient.chat.completions.create).toHaveBeenCalledWith({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Test' }], max_tokens: 100, temperature: 0.8, top_p: 0.9, frequency_penalty: 0.5, presence_penalty: 0.6, stop: ['STOP'], user: 'user-123' }); }); it('should use default model if not specified', async () => { (mockClient.chat.completions.create as jest.Mock).mockResolvedValue({ choices: [{ message: { content: 'test' }, finish_reason: 'stop' }] } as any); await provider.complete({ messages: [{ role: 'user', content: 'Test' }] }); expect(mockClient.chat.completions.create).toHaveBeenCalledWith( expect.objectContaining({ model: 'gpt-4-turbo' }) ); }); it('should return error for invalid request', async () => { const result = await provider.complete({ messages: [] }); expect(result.success).toBe(false); expect(result.error).toContain('messages'); }); it('should handle API errors', async () => { const apiError = new OpenAI.APIError( 400, { error: { message: 'Invalid request', type: 'invalid_request_error', param: null, code: null } }, 'Invalid request', {} ); (mockClient.chat.completions.create as jest.Mock).mockRejectedValue(apiError); const result = await provider.complete({ messages: [{ role: 'user', content: 'Test' }] }); expect(result.success).toBe(false); expect(result.error).toContain('OpenAI API Error'); }); }); describe('streamComplete()', () => { it('should stream completion chunks', async () => { const mockChunks = [ { choices: [{ delta: { content: 'Hello' }, finish_reason: null }] }, { choices: [{ delta: { content: ' world' }, finish_reason: null }] }, { choices: [{ delta: { content: '!' }, finish_reason: 'stop' }], usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 } } ]; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue({ async *[Symbol.asyncIterator]() { for (const chunk of mockChunks) { yield chunk; } } } as any); const chunks = []; for await (const chunk of provider.streamComplete({ messages: [{ role: 'user', content: 'Test' }] })) { chunks.push(chunk); } expect(chunks).toHaveLength(3); expect(chunks[0].delta).toBe('Hello'); expect(chunks[1].delta).toBe(' world'); expect(chunks[2].delta).toBe('!'); expect(chunks[2].finishReason).toBe('stop'); expect(chunks[2].usage).toEqual({ promptTokens: 5, completionTokens: 10, totalTokens: 15 }); }); it('should handle empty deltas', async () => { const mockChunks = [ { choices: [{ delta: {}, finish_reason: null }] }, { choices: [{ delta: { content: 'test' }, finish_reason: 'stop' }] } ]; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue({ async *[Symbol.asyncIterator]() { for (const chunk of mockChunks) { yield chunk; } } } as any); const chunks = []; for await (const chunk of provider.streamComplete({ messages: [{ role: 'user', content: 'Test' }] })) { chunks.push(chunk); } expect(chunks[0].delta).toBe(''); expect(chunks[1].delta).toBe('test'); }); it('should throw error for invalid request', async () => { await expect(async () => { for await (const _ of provider.streamComplete({ messages: [] })) { // Should throw before yielding } }).rejects.toThrow(); }); }); describe('generateEmbeddings()', () => { it('should generate embeddings successfully', async () => { const mockResponse = { model: 'text-embedding-3-small', data: [ { embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] } ], usage: { prompt_tokens: 10, total_tokens: 10 } }; (mockClient.embeddings.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.generateEmbeddings({ input: ['text 1', 'text 2'] }); expect(result.success).toBe(true); expect(result.embeddings).toHaveLength(2); expect(result.embeddings![0]).toEqual([0.1, 0.2, 0.3]); expect(result.embeddings![1]).toEqual([0.4, 0.5, 0.6]); expect(result.usage).toEqual({ promptTokens: 10, completionTokens: 0, totalTokens: 10 }); expect(result.model).toBe('text-embedding-3-small'); }); it('should use default embedding model if not specified', async () => { (mockClient.embeddings.create as jest.Mock).mockResolvedValue({ data: [{ embedding: [1, 2, 3] }], usage: { prompt_tokens: 5, total_tokens: 5 } } as any); await provider.generateEmbeddings({ input: 'test' }); expect(mockClient.embeddings.create).toHaveBeenCalledWith({ model: 'text-embedding-3-small', input: 'test', user: undefined }); }); it('should handle API errors', async () => { (mockClient.embeddings.create as jest.Mock).mockRejectedValue(new Error('API Error')); const result = await provider.generateEmbeddings({ input: 'test' }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('moderate()', () => { it('should check moderation successfully', async () => { const mockResponse = { results: [ { flagged: true, categories: { hate: true, 'hate/threatening': false, harassment: false, 'harassment/threatening': false, 'self-harm': false, 'self-harm/intent': false, 'self-harm/instructions': false, sexual: false, 'sexual/minors': false, violence: false, 'violence/graphic': false }, category_scores: { hate: 0.9, 'hate/threatening': 0.1, harassment: 0.05, 'harassment/threatening': 0.01, 'self-harm': 0.0, 'self-harm/intent': 0.0, 'self-harm/instructions': 0.0, sexual: 0.0, 'sexual/minors': 0.0, violence: 0.0, 'violence/graphic': 0.0 } } ] }; (mockClient.moderations.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.moderate('test content'); expect(result.success).toBe(true); expect(result.flagged).toBe(true); expect(result.categories).toEqual(mockResponse.results[0].categories); expect(result.categoryScores).toEqual(mockResponse.results[0].category_scores); }); it('should handle clean content', async () => { const mockResponse = { results: [ { flagged: false, categories: {}, category_scores: {} } ] }; (mockClient.moderations.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.moderate('clean content'); expect(result.success).toBe(true); expect(result.flagged).toBe(false); }); it('should handle API errors', async () => { (mockClient.moderations.create as jest.Mock).mockRejectedValue(new Error('API Error')); const result = await provider.moderate('test'); expect(result.success).toBe(false); expect(result.flagged).toBe(false); expect(result.error).toBeDefined(); }); }); describe('getAvailableModels()', () => { it('should fetch and filter models from API', async () => { const mockResponse = { data: [ { id: 'gpt-4', object: 'model' }, { id: 'gpt-3.5-turbo', object: 'model' }, { id: 'text-embedding-3-small', object: 'model' }, { id: 'babbage', object: 'model' }, // Should be filtered out { id: 'davinci', object: 'model' } // Should be filtered out ] }; (mockClient.models.list as jest.Mock).mockResolvedValue(mockResponse as any); const models = await provider.getAvailableModels(); expect(models).toHaveLength(3); expect(models.find(m => m.id === 'gpt-4')).toBeDefined(); expect(models.find(m => m.id === 'gpt-3.5-turbo')).toBeDefined(); expect(models.find(m => m.id === 'text-embedding-3-small')).toBeDefined(); expect(models.find(m => m.id === 'babbage')).toBeUndefined(); }); it('should return cached models if API fails', async () => { (mockClient.models.list as jest.Mock).mockRejectedValue(new Error('API Error')); const models = await provider.getAvailableModels(); expect(models.length).toBeGreaterThan(0); expect(models.find(m => m.id === 'gpt-4-turbo')).toBeDefined(); }); }); describe('checkHealth()', () => { it('should return healthy status when API is accessible', async () => { (mockClient.models.retrieve as jest.Mock).mockResolvedValue({ id: 'gpt-3.5-turbo' } as any); const health = await provider.checkHealth(); expect(health.status).toBe('healthy'); expect(health.latency).toBeGreaterThanOrEqual(0); expect(health.lastChecked).toBeInstanceOf(Date); }); it('should return unavailable status when API fails', async () => { (mockClient.models.retrieve as jest.Mock).mockRejectedValue(new Error('Connection failed')); const health = await provider.checkHealth(); expect(health.status).toBe('unavailable'); expect(health.latency).toBeGreaterThanOrEqual(0); expect(health.details?.error).toBeDefined(); }); }); describe('isAvailable()', () => { it('should return true when provider is healthy', async () => { (mockClient.models.retrieve as jest.Mock).mockResolvedValue({ id: 'gpt-3.5-turbo' } as any); const available = await provider.isAvailable(); expect(available).toBe(true); }); it('should return false when provider is unavailable', async () => { (mockClient.models.retrieve as jest.Mock).mockRejectedValue(new Error('API Error')); const available = await provider.isAvailable(); expect(available).toBe(false); }); }); describe('estimateCost()', () => { it('should estimate cost for GPT-4 Turbo correctly', () => { const cost = provider.estimateCost({ messages: [ { role: 'user', content: 'Hello world, how are you today?' } ], model: 'gpt-4-turbo', maxTokens: 1000 }); expect(cost.inputTokens).toBeGreaterThan(0); expect(cost.outputTokens).toBe(1000); expect(cost.totalTokens).toBe(cost.inputTokens + 1000); expect(cost.estimatedCostUSD).toBeGreaterThan(0); expect(cost.currency).toBe('USD'); }); it('should use default maxTokens if not specified', () => { const cost = provider.estimateCost({ messages: [{ role: 'user', content: 'Test' }], model: 'gpt-4-turbo' }); expect(cost.outputTokens).toBe(1000); }); it('should fall back to default pricing for unknown models', () => { const cost = provider.estimateCost({ messages: [{ role: 'user', content: 'Test' }], model: 'unknown-model', maxTokens: 500 }); expect(cost.estimatedCostUSD).toBeGreaterThan(0); }); }); });