feat(docs): update README with OpenWebUI support details

This commit is contained in:
2025-05-28 12:31:11 +02:00
parent 5da37f388f
commit 664a775724
8 changed files with 1705 additions and 717 deletions

416
tests/openwebui.test.ts Normal file
View File

@ -0,0 +1,416 @@
/**
* 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');
});
});
});