import { RealtimeXSDK, ProvidersResponse } from '@realtimex/sdk'; import os from 'os'; import path from 'path'; import { createLogger } from '../utils/logger.js'; const logger = createLogger('SDKService'); export interface ProviderResult { provider: string; model: string; isDefaultFallback?: boolean; } /** * Centralized SDK Service * Manages RealTimeX SDK initialization and provides singleton access */ export class SDKService { private static instance: RealtimeXSDK | null = null; private static initAttempted = false; // Default provider/model configuration // realtimexai routes through RealTimeX Desktop to user's configured providers static readonly DEFAULT_LLM_PROVIDER = 'realtimexai'; static readonly DEFAULT_LLM_MODEL = 'gpt-4o-mini'; static readonly DEFAULT_EMBED_PROVIDER = 'realtimexai'; static readonly DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; /** * Initialize SDK with required permissions * Safe to call multiple times - returns existing instance */ static initialize(): RealtimeXSDK { if (!this.instance && !this.initAttempted) { this.initAttempted = true; try { this.instance = new RealtimeXSDK({ realtimex: { // @ts-ignore - Dev Mode feature apiKey: 'SXKX93J-QSWMB04-K9E0GRE-J5DA8J0' }, permissions: [ 'api.agents', // List agents 'api.workspaces', // List workspaces 'api.threads', // List threads 'webhook.trigger', // Trigger agents 'activities.read', // Read activities 'activities.write', // Write activities 'llm.chat', // Chat completion 'llm.embed', // Generate embeddings 'llm.providers', // List LLM providers (chat, embed) 'vectors.read', // Query vectors 'vectors.write', // Store vectors ], }); logger.info('RealTimeX SDK initialized successfully'); // Verify connection with Desktop App // @ts-ignore - Dev Mode feature this.instance.ping() .then((status: any) => logger.debug('Desktop App Connection:', { status })) .catch((err: any) => logger.warn('Desktop App Connection failed', { error: err.message })); } catch (error: any) { logger.error('Failed to initialize SDK', error); this.instance = null; } } return this.instance!; } /** * Get SDK instance (initializes if needed) */ static getSDK(): RealtimeXSDK | null { if (!this.instance && !this.initAttempted) { this.initialize(); } return this.instance; } /** * Check if SDK is available and working */ static async isAvailable(): Promise { try { const sdk = this.getSDK(); if (!sdk) return false; // Try to ping first (faster) try { // @ts-ignore - Dev Mode feature await sdk.ping(); return true; } catch (e) { // Fallback to providers check if ping not available/fails await sdk.llm.chatProviders(); return true; } } catch (error: any) { logger.warn('SDK not available', { error: error.message }); return false; } } /** * Helper to wrap a promise with a timeout */ static async withTimeout(promise: Promise, timeoutMs: number, errorMessage: string): Promise { let timeoutHandle: any; const timeoutPromise = new Promise((_, reject) => { timeoutHandle = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); }); try { const result = await Promise.race([promise, timeoutPromise]); return result as T; } finally { clearTimeout(timeoutHandle); } } // Cache for default providers (avoid repeated SDK calls) private static defaultChatProvider: ProviderResult | null = null; private static defaultEmbedProvider: ProviderResult | null = null; /** * Get default chat provider/model from SDK dynamically * Caches result to avoid repeated SDK calls */ static async getDefaultChatProvider(): Promise { // Return cached if available if (this.defaultChatProvider) { return this.defaultChatProvider; } const sdk = this.getSDK(); if (!sdk) { throw new Error('RealTimeX SDK not available. Cannot determine default LLM provider.'); } try { const { providers } = await this.withTimeout( sdk.llm.chatProviders(), 30000, 'Chat providers fetch timed out' ); if (!providers || providers.length === 0) { throw new Error('No LLM providers available. Please configure a provider in RealTimeX Desktop.'); } // 1. Try to find the preferred default provider (realtimexai) const preferredProvider = providers.find(p => p.provider === this.DEFAULT_LLM_PROVIDER); if (preferredProvider && preferredProvider.models && preferredProvider.models.length > 0) { // Try to find the preferred default model (gpt-4o-mini) within it const preferredModel = preferredProvider.models.find(m => m.id === this.DEFAULT_LLM_MODEL) || preferredProvider.models[0]; this.defaultChatProvider = { provider: preferredProvider.provider, model: preferredModel.id, isDefaultFallback: false }; logger.info(`Using preferred chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`); return this.defaultChatProvider; } // 2. Fallback to the first provider with available models for (const p of providers) { if (p.models && p.models.length > 0) { this.defaultChatProvider = { provider: p.provider, model: p.models[0].id, isDefaultFallback: true }; logger.info(`Defaulting to first available chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`); return this.defaultChatProvider; } } throw new Error('No LLM models available. Please configure a model in RealTimeX Desktop.'); } catch (error: any) { logger.warn('Failed to get default chat provider from SDK. Using hardcoded fallback.', error); return { provider: this.DEFAULT_LLM_PROVIDER, model: this.DEFAULT_LLM_MODEL, isDefaultFallback: true }; } } /** * Get default embedding provider/model from SDK dynamically * Caches result to avoid repeated SDK calls */ static async getDefaultEmbedProvider(): Promise { // Return cached if available if (this.defaultEmbedProvider) { return this.defaultEmbedProvider; } const sdk = this.getSDK(); if (!sdk) { throw new Error('RealTimeX SDK not available. Cannot determine default embedding provider.'); } try { const { providers } = await this.withTimeout( sdk.llm.embedProviders(), 30000, 'Embed providers fetch timed out' ); if (!providers || providers.length === 0) { throw new Error('No embedding providers available. Please configure a provider in RealTimeX Desktop.'); } // 1. Try to find the preferred default provider (realtimexai) const preferredProvider = providers.find(p => p.provider === this.DEFAULT_EMBED_PROVIDER); if (preferredProvider && preferredProvider.models && preferredProvider.models.length > 0) { // Try to find the preferred default model (text-embedding-3-small) within it const preferredModel = preferredProvider.models.find(m => m.id === this.DEFAULT_EMBED_MODEL) || preferredProvider.models[0]; this.defaultEmbedProvider = { provider: preferredProvider.provider, model: preferredModel.id, isDefaultFallback: false }; logger.info(`Using preferred embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`); return this.defaultEmbedProvider; } // 2. Fallback to the first provider with available models (excluding native/local) for (const p of providers) { // Skip native/local providers as they require local models if (p.provider === 'native' || p.provider === 'local') { logger.debug(`Skipping native/local provider: ${p.provider}`); continue; } if (p.models && p.models.length > 0) { this.defaultEmbedProvider = { provider: p.provider, model: p.models[0].id, isDefaultFallback: true }; logger.info(`Defaulting to first available embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`); return this.defaultEmbedProvider; } } throw new Error('No embedding models available. Please configure a model in RealTimeX Desktop.'); } catch (error: any) { logger.warn('Failed to get default embed provider from SDK. Using hardcoded fallback.', error); return { provider: this.DEFAULT_EMBED_PROVIDER, model: this.DEFAULT_EMBED_MODEL, isDefaultFallback: true }; } } /** * Resolve LLM provider/model - use settings if available, otherwise use defaults */ static async resolveChatProvider(settings: { llm_provider?: string; llm_model?: string }): Promise { // If both provider and model are set in settings, use them if (settings.llm_provider && settings.llm_model) { return { provider: settings.llm_provider, model: settings.llm_model, isDefaultFallback: false }; } // If only model is set, use default provider with custom model if (!settings.llm_provider && settings.llm_model) { const defaultProvider = await this.getDefaultChatProvider(); return { provider: defaultProvider.provider, model: settings.llm_model, isDefaultFallback: false }; } // If only provider is set, use custom provider with its default model if (settings.llm_provider && !settings.llm_model) { const sdk = this.getSDK(); if (sdk) { try { const { providers } = await this.withTimeout( sdk.llm.chatProviders(), 30000, 'Chat providers fetch timed out' ); if (providers) { const provider = providers.find(p => p.provider === settings.llm_provider); if (provider && provider.models && provider.models.length > 0) { return { provider: settings.llm_provider, model: provider.models[0].id, isDefaultFallback: false }; } } } catch (error: any) { logger.warn(`Failed to fetch models for provider ${settings.llm_provider}`, { error: error?.message || String(error) }); } } // Fallback to using the provider with hardcoded default model return { provider: settings.llm_provider, model: this.DEFAULT_LLM_MODEL, isDefaultFallback: true }; } // Try to get from SDK discovery first return await this.getDefaultChatProvider(); } /** * Resolve embedding provider/model - use settings if available, otherwise use defaults */ static async resolveEmbedProvider(settings: { embedding_provider?: string; embedding_model?: string }): Promise { // If both provider and model are set in settings, use them if (settings.embedding_provider && settings.embedding_model) { return { provider: settings.embedding_provider, model: settings.embedding_model, isDefaultFallback: false }; } // Try to get from SDK discovery first return await this.getDefaultEmbedProvider(); } /** * Clear provider cache (useful when providers change) */ static clearProviderCache(): void { this.defaultChatProvider = null; this.defaultEmbedProvider = null; } }