/** * OpenWebUI Provider Implementation * * Integrates with OpenWebUI instances supporting both its native * chat-completions API (OpenAI-compatible) and the Ollama proxy. The * actual HTTP plumbing lives in two strategy classes; this file picks * one at construction based on `useOllamaProxy`. * * @author Jan-Marlon Leibl * @see https://docs.openwebui.com/ */ import type { AIProviderConfig, CompletionParams, CompletionResponse, CompletionChunk, ProviderInfo } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; import { DEFAULT_MODELS, DEFAULT_OPENWEBUI_BASE_URL } from '../constants.js'; import { OpenWebUIHttpClient } from './openwebui-http.js'; import { OpenWebUIChatStrategy, OpenWebUIOllamaStrategy, type OpenWebUIStrategy } from './openwebui-strategies.js'; export interface OpenWebUIConfig extends AIProviderConfig { /** Default model to use for requests. @default 'llama3.1:latest' */ defaultModel?: string; /** Base URL for the OpenWebUI instance. @default 'http://localhost:3000' */ baseUrl?: string; /** * Use the Ollama proxy endpoint (`/ollama/api/generate`) instead of the * OpenAI-compatible chat completions endpoint. @default false */ useOllamaProxy?: boolean; /** * Allow connections to instances with self-signed certs or plain HTTP. * @default true */ dangerouslyAllowInsecureConnections?: boolean; } export class OpenWebUIProvider extends BaseAIProvider { private readonly defaultModel: string; private readonly baseUrl: string; private readonly strategy: OpenWebUIStrategy; constructor(config: OpenWebUIConfig) { super(config); this.defaultModel = config.defaultModel || DEFAULT_MODELS.openwebui; this.baseUrl = this.normalizeBaseUrl(config.baseUrl || DEFAULT_OPENWEBUI_BASE_URL); this.validateBaseUrl(); this.warnOnInsecureMix(config); const http = new OpenWebUIHttpClient({ baseUrl: this.baseUrl, apiKey: this.config.apiKey, timeout: this.config.timeout, dangerouslyAllowInsecureConnections: config.dangerouslyAllowInsecureConnections ?? true }); this.strategy = config.useOllamaProxy ? new OpenWebUIOllamaStrategy(http) : new OpenWebUIChatStrategy(http); } public getInfo(): ProviderInfo { return { name: 'OpenWebUI', version: '2.0.0', models: [ 'llama3.1:latest', 'llama3.1:8b', 'llama3.1:70b', 'mistral:latest', 'mistral:7b', 'codellama:latest', 'codellama:7b', 'phi3:latest', 'phi3:mini', 'qwen2:latest', 'gemma:latest' ], maxContextLength: 32768, supportsStreaming: true, capabilities: { vision: false, functionCalling: false, jsonMode: false, systemMessages: true, reasoning: true, codeGeneration: true, localDeployment: true, multiModel: true } }; } protected async doInitialize(): Promise { try { await this.validateConnection(); } catch (error) { throw new AIProviderError( `Failed to initialize OpenWebUI provider: ${(error as Error).message}`, AIErrorType.NETWORK, undefined, error as Error ); } } protected async doComplete(params: CompletionParams): Promise> { return this.strategy.complete(params, this.defaultModel); } protected async *doStream(params: CompletionParams): AsyncIterable { yield* this.strategy.stream(params, this.defaultModel); } protected override async validateConnection(): Promise { try { await this.strategy.validateConnection(); } catch (error: any) { if (error.message?.includes('ECONNREFUSED')) { throw new AIProviderError( `Cannot connect to OpenWebUI at ${this.baseUrl}. Please verify the instance is running and accessible.`, AIErrorType.NETWORK, undefined, error ); } if (error.message?.includes('certificate')) { throw new AIProviderError( `SSL certificate error connecting to ${this.baseUrl}. Consider setting dangerouslyAllowInsecureConnections to true for local instances.`, AIErrorType.NETWORK, undefined, error ); } throw new AIProviderError( `Failed to validate OpenWebUI connection: ${error.message}`, AIErrorType.NETWORK, undefined, error ); } } protected override providerErrorMessages(): Partial> { return { 401: 'Authentication failed with OpenWebUI. Please check your API key or instance configuration.', 403: 'Authentication failed with OpenWebUI. Please check your API key or instance configuration.', 404: 'OpenWebUI endpoint not found. Please verify the base URL and API path.', 429: 'Rate limit exceeded on OpenWebUI instance.', 500: 'OpenWebUI instance error. The server may be overloaded or misconfigured.', 502: 'OpenWebUI instance error. The server may be overloaded or misconfigured.', 503: 'OpenWebUI instance error. The server may be overloaded or misconfigured.' }; } protected override mapProviderError(error: any): AIProviderError | null { if (!error) return null; const message: string = error.message || ''; const status = error.status || error.statusCode; if (status === 404 && message.includes('model')) { return new AIProviderError( "Model not found in OpenWebUI. Please check the model name and ensure it's available.", AIErrorType.MODEL_NOT_FOUND, status, error ); } return null; } private normalizeBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/$/, ''); } private validateBaseUrl(): void { try { new URL(this.baseUrl); } catch { throw new AIProviderError( `Invalid base URL: ${this.baseUrl}. Must be a valid URL.`, AIErrorType.INVALID_REQUEST ); } } private warnOnInsecureMix(config: OpenWebUIConfig): void { const allowInsecure = config.dangerouslyAllowInsecureConnections ?? true; if (this.baseUrl.includes('https://') && allowInsecure) { console.warn('Using HTTPS with insecure connections allowed. Consider setting dangerouslyAllowInsecureConnections to false.'); } } }