import { ChatCompletionRequest, EmbeddingRequest, ImageEditRequest, CreateImageRequest, CreateSpeechRequest, CreateTranscriptionRequest, } from './OpenAITypes'; function concatenateStreamData(dataStream: string, initialBuffer = '') { let message = ''; let finalMessageReceived = false; let buffer = initialBuffer; // Initialize buffer with any initial leftover data // Split the single string into lines const lines = (buffer + dataStream).split('\n'); lines.forEach(line => { if (line.startsWith('data: ')) { // Append any buffered data to the line const completeData = line.substring(5); // Remove 'data: ' prefix console.log("Complete data:", completeData); try { const data = JSON.parse(completeData.trim()); // Try to parse the complete data buffer = ''; // Reset buffer since parsing was successful if (data.object === "chat.completion.chunk") { // Append delta content if available if (data.choices && data.choices.length > 0 && data.choices[0].delta && data.choices[0].delta.content) { message += data.choices[0].delta.content; } // Check if the finish reason indicates the end of the stream if (data.choices[0].finish_reason === 'stop') { finalMessageReceived = true; } } } catch (error) { console.error("Error parsing data:", error); // Parsing failed, likely due to incomplete data buffer = completeData; // Store incomplete data back to buffer for next try } } else if (line.startsWith('data: [DONE]')) { finalMessageReceived = true; } else { // Handle case where line might be empty or irrelevant // Potentially add non-data prefixed lines to buffer if expecting continuation if (!line.trim()) { buffer += '\n'; // Add newline back if buffering across empty lines } } }); if (finalMessageReceived) { console.log("Final message:", message); } else { console.log("Message so far:", message); } return { message, complete: finalMessageReceived, leftover: buffer }; } // OpenAIClient class class OpenAIClient { private projectId: string; private userId: string | null; private jwt: string | null; private baseURL: string; private headers: Record; private subscription: { unsubscribe: () => void } | null = null; constructor(projectId: string, userId?: string | null, private userAuth?: { type: string; object: any }, baseURL?: string) { this.projectId = projectId; this.userId = userId || null; this.userAuth = userAuth; this.jwt = null; this.baseURL = baseURL || process.env.BACKLESS_ENDPOINT || 'https://backless.ai/api/openai'; this.headers = { 'Content-Type': 'application/json', 'X-Project-Id': this.projectId, }; this.headers = this.initializeHeaders(); if (this.userAuth?.type === 'supabase') { this.initializeSupabaseAuth(); } } private initializeHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json', 'X-Project-Id': this.projectId, }; return headers; } private initializeSupabaseAuth(): void { const { data } = this.userAuth?.object.auth.onAuthStateChange( (event: string, session: any) => { console.log(event, session); this.handleAuthEvent(event, session); } ); this.subscription = data; } private handleAuthEvent(event: string, session: any): void { switch (event) { case 'INITIAL_SESSION': case 'SIGNED_IN': case 'TOKEN_REFRESHED': this.userId = session?.user?.id; this.updateJWT(session?.access_token); break; case 'SIGNED_OUT': this.clearJWT(); break; // Handle other events if needed } } private updateJWT(accessToken: string | undefined): void { if (accessToken) { this.jwt = accessToken; this.headers['Authorization'] = `Bearer ${this.jwt}`; } if (this.userId) { this.headers['X-User-Id'] = this.userId; } else { delete this.headers['X-User-Id']; } } public async ensureJWT(): Promise { if (this.jwt === null && this.userAuth?.type === 'supabase') { const currentSession = await this.userAuth.object.auth.getSession(); const user = currentSession?.session.user; if (currentSession?.data.session?.access_token) { this.userId = user?.id; this.updateJWT(currentSession.data.session.access_token); } } } private clearJWT(): void { this.jwt = null; delete this.headers['Authorization']; } destroy() { if (this.subscription) { this.subscription.unsubscribe(); } } // Function to get completions from OpenAI async getCompletion(body: ChatCompletionRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL + '/create_chat_completion', { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response.json(); } async getCompletionStream(body: ChatCompletionRequest, onData: (data: string) => void, config: { timeout?: number } = {}) { await this.ensureJWT(); body.stream = true; const timeout = config.timeout || 60000; // Default timeout 30 seconds const controller = new AbortController(); setTimeout(() => controller.abort(), timeout); try { const response = await fetch(this.baseURL + '/create_chat_completion', { method: 'POST', headers: this.headers, body: JSON.stringify(body), signal: controller.signal }); if (!response.ok) { const errorBody = await response.text(); console.error(`HTTP error! status: ${response.status}, Body: ${errorBody}`); throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { console.error("Failed to read response"); throw new Error("Failed to read response"); } const decoder = new TextDecoder('utf-8'); let leftover = ''; let message = ''; while (true) { const { done, value } = await reader.read(); if (done) { console.log("Stream complete"); break; } try { const newData = decoder.decode(value, { stream: true }); console.log("New data:", newData); const result = concatenateStreamData(newData, leftover); console.log("Result:", result); message = result.message; leftover = result.leftover; onData(message); } catch (callbackError) { console.error("Error in callback:", callbackError); } } } catch (error) { console.error("Error streaming data:", error); throw error; // Rethrow after logging or handle differently if required } finally { controller.abort(); // Ensure the fetch is aborted if still running } } // Function to get embeddings from OpenAI async getEmbedding(body: EmbeddingRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL, { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response.json(); } // Function to get an image from OpenAI async getImage(body: CreateImageRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL, { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response.json(); } // Function to edit an image from OpenAI async getImageEdit(body: ImageEditRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL, { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response.json(); } // Function to get speech synthesis from OpenAI async getSpeech(body: CreateSpeechRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL + '/create_speech', { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response; } // Function to transcribe speech from OpenAI async getTranscription(body: CreateTranscriptionRequest): Promise { await this.ensureJWT(); const response = await fetch(this.baseURL + '/create_transcription', { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`); } return response.json(); } } export default OpenAIClient;