Files
simple-ai-provider/src/providers/openwebui.ts
Jan-Marlon Leibl 3d985a95a8 refactor: split OpenWebUI into strategy classes by backend
Replace Conditional with Polymorphism from the Refactoring Guru catalog.

The OpenWebUIProvider previously contained two parallel implementations
(completeWithChat / completeWithOllama, streamWithChat / streamWithOllama,
plus branching in validateConnection) selected by a `useOllamaProxy`
boolean. The 987-line file is split into:

  - openwebui-types.ts       wire-format response types
  - openwebui-http.ts        shared HTTP client (auth, timeout)
  - openwebui-strategies.ts  OpenWebUIStrategy interface plus
                             OpenWebUIChatStrategy and
                             OpenWebUIOllamaStrategy implementations
  - openwebui.ts             thin provider that picks one strategy at
                             construction and delegates

OpenWebUIProvider and OpenWebUIConfig (including useOllamaProxy) stay
in their original locations, so consumer imports are unchanged.

Side effects:
- Error mapping now goes through the base mapProviderError / providerErrorMessages
  hooks added in R2, removing handleOpenWebUIError and its duplicated status switch.
- providerInfo.version bumped to 2.0.0 to reflect the rewrite.
2026-05-21 13:51:08 +02:00

214 lines
6.4 KiB
TypeScript

/**
* 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<void> {
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<CompletionResponse<string>> {
return this.strategy.complete(params, this.defaultModel);
}
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
yield* this.strategy.stream(params, this.defaultModel);
}
protected override async validateConnection(): Promise<void> {
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<Record<number, string>> {
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.');
}
}
}