/** * Tests for OpenWebUI provider implementation */ import { describe, it, expect, beforeEach } from 'bun:test'; import { OpenWebUIProvider, type OpenWebUIConfig } from '../src/providers/openwebui.js'; import { AIProviderError, AIErrorType, type CompletionParams } from '../src/types/index.js'; describe('OpenWebUIProvider', () => { let provider: OpenWebUIProvider; let mockConfig: OpenWebUIConfig; beforeEach(() => { mockConfig = { apiKey: 'test-bearer-token', baseUrl: 'http://localhost:3000', defaultModel: 'llama3.1' }; }); describe('Constructor', () => { it('should create provider with default configuration', () => { provider = new OpenWebUIProvider(mockConfig); expect(provider).toBeInstanceOf(OpenWebUIProvider); expect(provider.getInfo().name).toBe('OpenWebUI'); }); it('should use default values when not specified', () => { const minimalConfig: OpenWebUIConfig = { apiKey: 'test' }; provider = new OpenWebUIProvider(minimalConfig); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should handle custom configuration', () => { const config: OpenWebUIConfig = { apiKey: 'test', baseUrl: 'http://localhost:8080', defaultModel: 'mistral:7b', useOllamaProxy: true }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should handle trailing slash in baseUrl', () => { const config: OpenWebUIConfig = { apiKey: 'test', baseUrl: 'http://localhost:3000/', defaultModel: 'llama3.1' }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); }); describe('getInfo', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should return correct provider information', () => { const info = provider.getInfo(); expect(info.name).toBe('OpenWebUI'); expect(info.version).toBe('1.0.0'); expect(info.supportsStreaming).toBe(true); expect(info.maxContextLength).toBe(8192); expect(Array.isArray(info.models)).toBe(true); }); it('should include expected models', () => { const info = provider.getInfo(); expect(info.models).toContain('llama3.1'); expect(info.models).toContain('mistral'); expect(info.models).toContain('codellama'); expect(info.models).toContain('gemma2'); }); it('should have correct capabilities', () => { const info = provider.getInfo(); expect(info.capabilities).toEqual({ vision: false, functionCalling: false, systemMessages: true, localExecution: true, customModels: true, rag: true }); }); }); describe('Error Handling', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should handle connection refused errors', () => { const mockError = new Error('connect ECONNREFUSED') as any; mockError.code = 'ECONNREFUSED'; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.NETWORK); expect(aiError.message).toContain('Cannot connect to OpenWebUI'); }); it('should handle hostname resolution errors', () => { const mockError = new Error('getaddrinfo ENOTFOUND') as any; mockError.code = 'ENOTFOUND'; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.NETWORK); expect(aiError.message).toContain('Cannot resolve OpenWebUI hostname'); }); it('should handle 404 model not found errors', () => { const mockError = new Error('model not available') as any; mockError.status = 404; mockError.message = 'model not available'; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.MODEL_NOT_FOUND); }); it('should handle 404 endpoint not found errors', () => { const mockError = new Error('Not found') as any; mockError.status = 404; mockError.message = 'endpoint not found'; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.NETWORK); }); it('should handle timeout errors', () => { const mockError = new Error('Request timeout') as any; mockError.code = 'ETIMEDOUT'; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.TIMEOUT); }); it('should handle rate limit errors', () => { const mockError = new Error('Rate limit exceeded') as any; mockError.status = 429; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.RATE_LIMIT); }); it('should handle server errors', () => { const mockError = new Error('Internal server error') as any; mockError.status = 500; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.NETWORK); }); it('should handle authentication errors', () => { const mockError = new Error('Unauthorized') as any; mockError.status = 401; const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.AUTHENTICATION); expect(aiError.message).toContain('Settings > Account'); }); it('should handle unknown errors', () => { const mockError = new Error('Unknown error'); const aiError = (provider as any).handleOpenWebUIError(mockError); expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError.type).toBe(AIErrorType.UNKNOWN); }); }); describe('Message Conversion', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should convert messages correctly for chat API', () => { const messages = [ { role: 'system' as const, content: 'You are helpful' }, { role: 'user' as const, content: 'Hello' }, { role: 'assistant' as const, content: 'Hi there!' } ]; const converted = (provider as any).convertMessages(messages); expect(converted).toHaveLength(3); expect(converted[0]).toEqual({ role: 'system', content: 'You are helpful' }); expect(converted[1]).toEqual({ role: 'user', content: 'Hello' }); expect(converted[2]).toEqual({ role: 'assistant', content: 'Hi there!' }); }); it('should convert messages to prompt for Ollama API', () => { const messages = [ { role: 'system' as const, content: 'You are helpful' }, { role: 'user' as const, content: 'Hello' }, { role: 'assistant' as const, content: 'Hi there!' } ]; const prompt = (provider as any).convertMessagesToPrompt(messages); expect(prompt).toBe('System: You are helpful\n\nHuman: Hello\n\nAssistant: Hi there!'); }); }); describe('Response Formatting', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should format chat completion response correctly', () => { const mockResponse = { id: 'test-id', object: 'chat.completion', created: 1234567890, model: 'llama3.1', choices: [{ index: 0, message: { role: 'assistant', content: 'Hello there!' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }; const response = (provider as any).formatChatResponse(mockResponse); expect(response.content).toBe('Hello there!'); expect(response.model).toBe('llama3.1'); expect(response.id).toBe('test-id'); expect(response.usage.promptTokens).toBe(10); expect(response.usage.completionTokens).toBe(5); expect(response.usage.totalTokens).toBe(15); expect(response.metadata?.finishReason).toBe('stop'); expect(response.metadata?.created).toBe(1234567890); }); it('should format Ollama response correctly', () => { const mockResponse = { model: 'llama3.1', created_at: '2024-01-01T00:00:00Z', response: 'Hello there!', done: true, prompt_eval_count: 10, eval_count: 5, total_duration: 1000000, eval_duration: 500000 }; const response = (provider as any).formatOllamaResponse(mockResponse); expect(response.content).toBe('Hello there!'); expect(response.model).toBe('llama3.1'); expect(response.usage.promptTokens).toBe(10); expect(response.usage.completionTokens).toBe(5); expect(response.usage.totalTokens).toBe(15); expect(response.metadata?.created_at).toBe('2024-01-01T00:00:00Z'); expect(response.metadata?.total_duration).toBe(1000000); }); it('should handle response without content', () => { const mockResponse = { id: 'test-id', object: 'chat.completion', created: 1234567890, model: 'llama3.1', choices: [{ index: 0, message: { role: 'assistant' }, // Message without content finish_reason: 'stop' }], usage: { prompt_tokens: 5, completion_tokens: 0, total_tokens: 5 } }; expect(() => { (provider as any).formatChatResponse(mockResponse); }).toThrow(AIProviderError); }); it('should handle missing usage information', () => { const mockResponse = { id: 'test-id', object: 'chat.completion', created: 1234567890, model: 'llama3.1', choices: [{ index: 0, message: { role: 'assistant', content: 'Hello there!' }, finish_reason: 'stop' }] // No usage information }; const response = (provider as any).formatChatResponse(mockResponse); expect(response.content).toBe('Hello there!'); expect(response.usage.promptTokens).toBe(0); expect(response.usage.completionTokens).toBe(0); expect(response.usage.totalTokens).toBe(0); }); }); describe('Configuration Options', () => { it('should handle custom baseUrl', () => { const config: OpenWebUIConfig = { apiKey: 'test', baseUrl: 'https://my-openwebui.com' }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should handle custom default model', () => { const config: OpenWebUIConfig = { apiKey: 'test', defaultModel: 'mistral:7b' }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should handle Ollama proxy mode', () => { const config: OpenWebUIConfig = { apiKey: 'test', useOllamaProxy: true }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should handle SSL verification settings', () => { const config: OpenWebUIConfig = { apiKey: 'test', dangerouslyAllowInsecureConnections: false }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); }); describe('Request Generation', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should generate correct headers', () => { const headers = (provider as any).makeRequest('http://test.com', 'GET'); // Note: This would need to be mocked in a real test environment expect(typeof headers).toBe('object'); }); }); describe('API Mode Selection', () => { it('should use chat API by default', () => { const config: OpenWebUIConfig = { apiKey: 'test', useOllamaProxy: false }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); it('should use Ollama proxy when configured', () => { const config: OpenWebUIConfig = { apiKey: 'test', useOllamaProxy: true }; provider = new OpenWebUIProvider(config); expect(provider).toBeInstanceOf(OpenWebUIProvider); }); }); describe('Validation', () => { beforeEach(() => { provider = new OpenWebUIProvider(mockConfig); }); it('should validate provider is initialized before use', async () => { const params: CompletionParams = { messages: [{ role: 'user', content: 'Hello' }] }; await expect(provider.complete(params)).rejects.toThrow('Provider must be initialized before use'); }); }); });