/* 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, OpenAIFunction } from '../src/types/openai-types'; import OpenAI from 'openai'; jest.mock('openai'); describe('OpenAI-Specific Features', () => { let provider: OpenAIProvider; let mockClient: jest.Mocked; const config: OpenAIProviderConfig = { providerName: 'openai', apiKey: 'test-api-key', defaultModel: 'gpt-4-turbo' }; beforeEach(() => { jest.clearAllMocks(); // Setup mock OpenAI client with properly typed Jest mocks const mockChatCompletionsCreate = jest.fn() as jest.Mock; mockClient = { chat: { completions: { create: mockChatCompletionsCreate as any } } } as unknown as jest.Mocked; (OpenAI as jest.MockedClass).mockImplementation(() => mockClient); provider = new OpenAIProvider(config); }); describe('completionWithFunctions()', () => { it('should call OpenAI with function definitions', async () => { const functions: OpenAIFunction[] = [ { name: 'get_weather', description: 'Get current weather', parameters: { type: 'object', properties: { location: { type: 'string', description: 'City name' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'] } }, required: ['location'] } } ]; const mockResponse = { id: 'chatcmpl-123', model: 'gpt-4-turbo', created: Date.now(), choices: [ { message: { role: 'assistant', content: null, function_call: { name: 'get_weather', arguments: '{"location":"San Francisco","unit":"celsius"}' } }, finish_reason: 'function_call' } ], usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 } }; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.completionWithFunctions({ messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }], functions }); expect(result.success).toBe(true); expect(result.metadata?.functionCall).toEqual({ name: 'get_weather', arguments: '{"location":"San Francisco","unit":"celsius"}' }); expect(result.finishReason).toBe('function_call'); expect(mockClient.chat.completions.create).toHaveBeenCalledWith({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }], functions, function_call: 'auto' }); }); it('should handle regular text response when function not needed', async () => { const functions: OpenAIFunction[] = [ { name: 'get_weather', description: 'Get current weather', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'] } } ]; const mockResponse = { choices: [ { message: { role: 'assistant', content: 'I need more information to check the weather.', function_call: undefined }, finish_reason: 'stop' } ], usage: { prompt_tokens: 30, completion_tokens: 10, total_tokens: 40 } }; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.completionWithFunctions({ messages: [{ role: 'user', content: 'Hello' }], functions }); expect(result.success).toBe(true); expect(result.content).toBe('I need more information to check the weather.'); expect(result.finishReason).toBe('stop'); expect(result.metadata?.functionCall).toBeUndefined(); }); it('should handle multiple function definitions', async () => { const functions: OpenAIFunction[] = [ { name: 'get_weather', description: 'Get weather', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'] } }, { name: 'get_time', description: 'Get current time', parameters: { type: 'object', properties: { timezone: { type: 'string' } } } } ]; mockClient.chat.completions.create.mockResolvedValue({ choices: [ { message: { role: 'assistant', content: null, function_call: { name: 'get_time', arguments: '{"timezone":"PST"}' } }, finish_reason: 'function_call' } ] } as any); const result = await provider.completionWithFunctions({ messages: [{ role: 'user', content: 'What time is it in PST?' }], functions }); expect(result.success).toBe(true); expect(result.metadata?.functionCall).toBeDefined(); expect((result.metadata?.functionCall as any)?.name).toBe('get_time'); }); it('should handle API errors', async () => { const functions: OpenAIFunction[] = [ { name: 'test_function', description: 'Test', parameters: { type: 'object', properties: {} } } ]; (mockClient.chat.completions.create as jest.Mock).mockRejectedValue(new Error('API Error')); const result = await provider.completionWithFunctions({ messages: [{ role: 'user', content: 'Test' }], functions }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('analyzeImage()', () => { it('should analyze image with GPT-4 Vision', async () => { const mockResponse = { model: 'gpt-4-vision-preview', choices: [ { message: { role: 'assistant', content: 'The image shows a beautiful sunset over the ocean with orange and purple hues.' }, finish_reason: 'stop' } ], usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 } }; (mockClient.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any); const result = await provider.analyzeImage( 'https://example.com/sunset.jpg', 'Describe this image in detail' ); expect(result.success).toBe(true); expect(result.content).toContain('sunset'); expect(result.usage?.totalTokens).toBe(150); expect(result.model).toBe('gpt-4-vision-preview'); expect(mockClient.chat.completions.create).toHaveBeenCalledWith({ model: 'gpt-4-vision-preview', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe this image in detail' }, { type: 'image_url', image_url: { url: 'https://example.com/sunset.jpg' } } ] } ], max_tokens: 1000 }); }); it('should support custom model', async () => { mockClient.chat.completions.create.mockResolvedValue({ model: 'gpt-4-turbo', choices: [{ message: { content: 'Analysis result' }, finish_reason: 'stop' }] } as any); await provider.analyzeImage( 'https://example.com/image.jpg', 'Analyze this', 'gpt-4-turbo' ); expect(mockClient.chat.completions.create).toHaveBeenCalledWith( expect.objectContaining({ model: 'gpt-4-turbo' }) ); }); it('should handle empty or null content', async () => { mockClient.chat.completions.create.mockResolvedValue({ choices: [{ message: { content: null }, finish_reason: 'stop' }] } as any); const result = await provider.analyzeImage( 'https://example.com/image.jpg', 'Describe this' ); expect(result.success).toBe(true); expect(result.content).toBe(''); }); it('should handle API errors', async () => { const apiError = new OpenAI.APIError( 400, { error: { message: 'Invalid image URL' } }, 'Invalid image URL', {} ); (mockClient.chat.completions.create as jest.Mock).mockRejectedValue(apiError); const result = await provider.analyzeImage( 'invalid-url', 'Describe this' ); expect(result.success).toBe(false); expect(result.error).toContain('OpenAI API Error'); }); it('should handle rate limiting', async () => { const rateLimitError = new OpenAI.APIError( 429, { error: { message: 'Rate limit exceeded' } }, 'Rate limit exceeded', {} ); (mockClient.chat.completions.create as jest.Mock).mockRejectedValue(rateLimitError); const result = await provider.analyzeImage( 'https://example.com/image.jpg', 'Describe' ); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); });