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.
214 lines
6.4 KiB
TypeScript
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.');
|
|
}
|
|
}
|
|
}
|