import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { MultiModalModel } from './abstract.classes.multimodal.js'; import type { ChatOptions, ChatResponse, ChatMessage } from './abstract.classes.multimodal.js'; import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; export interface IExoProviderOptions { exoBaseUrl?: string; apiKey?: string; } export class ExoProvider extends MultiModalModel { private options: IExoProviderOptions; public openAiApiClient: plugins.openai.default; constructor(optionsArg: IExoProviderOptions = {}) { super(); this.options = { exoBaseUrl: 'http://localhost:8080/v1', // Default Exo API endpoint ...optionsArg }; } public async start() { this.openAiApiClient = new plugins.openai.default({ apiKey: this.options.apiKey || 'not-needed', // Exo might not require an API key for local deployment baseURL: this.options.exoBaseUrl, }); } public async stop() {} public async chatStream(input: ReadableStream): Promise> { // Create a TextDecoder to handle incoming chunks const decoder = new TextDecoder(); let buffer = ''; let currentMessage: { role: string; content: string; } | null = null; // Create a TransformStream to process the input const transform = new TransformStream({ async transform(chunk, controller) { buffer += decoder.decode(chunk, { stream: true }); // Try to parse complete JSON messages from the buffer while (true) { const newlineIndex = buffer.indexOf('\n'); if (newlineIndex === -1) break; const line = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); if (line.trim()) { try { const message = JSON.parse(line); currentMessage = message; // Process the message based on its type if (message.type === 'message') { const response = await this.chat({ systemMessage: '', userMessage: message.content, messageHistory: [{ role: message.role as 'user' | 'assistant' | 'system', content: message.content }] }); controller.enqueue(JSON.stringify(response) + '\n'); } } catch (error) { console.error('Error processing message:', error); } } } }, flush(controller) { if (buffer) { try { const message = JSON.parse(buffer); currentMessage = message; } catch (error) { console.error('Error processing remaining buffer:', error); } } } }); return input.pipeThrough(transform); } public async chat(options: ChatOptions): Promise { const messages: ChatCompletionMessageParam[] = [ { role: 'system', content: options.systemMessage }, ...options.messageHistory, { role: 'user', content: options.userMessage } ]; try { const response = await this.openAiApiClient.chat.completions.create({ model: 'local-model', // Exo uses local models messages: messages, stream: false }); return { role: 'assistant', message: response.choices[0]?.message?.content || '' }; } catch (error) { console.error('Error in chat completion:', error); throw error; } } public async audio(optionsArg: { message: string }): Promise { throw new Error('Audio generation is not supported by Exo provider'); } public async vision(optionsArg: { image: Buffer; prompt: string }): Promise { throw new Error('Vision processing is not supported by Exo provider'); } public async document(optionsArg: { systemMessage: string; userMessage: string; pdfDocuments: Uint8Array[]; messageHistory: ChatMessage[]; }): Promise<{ message: any }> { throw new Error('Document processing is not supported by Exo provider'); } }