import { OpenWebUIConfig, Session, Message, Role, OpenWebUIMessage } from '../types'; export class OpenWebUIClient { private config: OpenWebUIConfig; constructor(config: OpenWebUIConfig) { this.config = config; } private get headers() { return { 'Authorization': `Bearer ${this.config.apiKey}`, 'Content-Type': 'application/json' }; } // Check connection async testConnection(): Promise { try { const res = await fetch(`${this.config.baseUrl}/api/version`, { headers: this.headers }); return res.ok; } catch (e) { return false; } } // Get available models async getModels(): Promise { try { const res = await fetch(`${this.config.baseUrl}/api/models`, { headers: this.headers }); if (!res.ok) return []; const data = await res.json(); // OpenWebUI usually returns { data: [{id: 'name', ...}] } return data.data?.map((m: any) => m.id) || []; } catch (e) { return []; } } // Create a new chat session async createChat(title: string): Promise { const res = await fetch(`${this.config.baseUrl}/api/chats/new`, { method: 'POST', headers: this.headers, body: JSON.stringify({ title, content: null }) // OpenWebUI new chat format }); if (!res.ok) throw new Error('Failed to create chat'); return await res.json(); } // Get all chats async getChats(): Promise { const res = await fetch(`${this.config.baseUrl}/api/chats`, { headers: this.headers }); if (!res.ok) throw new Error('Failed to fetch chats'); const data = await res.json(); // Sort by updated_at descending return data.sort((a: Session, b: Session) => b.updated_at - a.updated_at); } // Get chat history async getChatHistory(chatId: string): Promise { const res = await fetch(`${this.config.baseUrl}/api/chats/${chatId}`, { headers: this.headers }); if (!res.ok) throw new Error('Failed to fetch chat history'); const data = await res.json(); // OpenWebUI returns a 'chat' object with 'messages' array usually, or the structure might vary. // Assuming standard OpenWebUI structure where chat.messages is list of messages // Adjusting based on common OpenWebUI API responses: const messages = data.chat?.messages || data.messages || []; return messages.map((m: any) => ({ id: m.id, role: m.role, content: m.content, timestamp: new Date(m.timestamp * 1000) })); } // Delete chat async deleteChat(chatId: string): Promise { await fetch(`${this.config.baseUrl}/api/chats/${chatId}`, { method: 'DELETE', headers: this.headers }); } // Delete specific message (Silent Deletion) async deleteMessage(chatId: string, messageId: string): Promise { await fetch(`${this.config.baseUrl}/api/chats/${chatId}/messages/${messageId}`, { method: 'DELETE', headers: this.headers }); } // Send message async sendMessage( chatId: string | null, content: string, history: Message[], model: string, offTheRecord: boolean = false ): Promise> { // Convert history to OpenWebUI format const contextMessages: OpenWebUIMessage[] = history.map(m => ({ role: m.role, content: m.content })); // Add current user message contextMessages.push({ role: Role.USER, content }); const payload: any = { model: model, messages: contextMessages, stream: true, }; // If persistent (not off the record) and we have a chat ID, we might need a different endpoint // OpenWebUI's /api/chat/completions is stateless. // To persist, usually the frontend creates the message structure and saves it, // OR we use the stateless endpoint and client manages state, then syncs. // BUT strictly following prompt: "Core API Endpoints... POST /api/chats/{chat_id}/messages" // If that endpoint exists and supports streaming, we use it. // If not, we use /api/chat/completions. // For this implementation, we will use the standard /api/chat/completions for generation // and manual message persistence if needed, to support both modes cleanly. // If NOT off-the-record, we should ideally save the user message to the backend first? // The prompt implies we should use /api/chats/{chat_id}/messages to SEND message. let endpoint = `${this.config.baseUrl}/api/chat/completions`; // Note: To properly support "Silent Deletion" of individual messages from the backend, // the messages must exist on the backend. // So for persistent chats, we must ensure they are saved. // However, for streaming response, /chat/completions is standard. const res = await fetch(endpoint, { method: 'POST', headers: this.headers, body: JSON.stringify(payload) }); if (!res.body) throw new Error('No response body'); return res.body.getReader(); } // Persist a message to a chat (used after generation or sending) async addMessageToChat(chatId: string, message: Message): Promise { await fetch(`${this.config.baseUrl}/api/chats/${chatId}/messages`, { method: 'POST', headers: this.headers, body: JSON.stringify({ id: message.id, role: message.role, content: message.content, timestamp: Math.floor(message.timestamp.getTime() / 1000) }) }); } }