@realtimex/realtimex-alchemy 1.0.50 → 1.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.52] - 2026-01-27
9
+
10
+ ### Changed
11
+ - **Configuration**: Refactored LLM provider configuration to drop legacy fields and utilize new SDK defaults, simplifying setup.
12
+
13
+ ### Fixed
14
+ - **Embeddings**: Updated `AlchemistService` to allow embedding generation without an explicit `embedding_model` setting, enabling dynamic model resolution via the SDK.
15
+
16
+ ## [1.0.51] - 2026-01-27
17
+
18
+ ### Added
19
+ - **Vector Storage**: Migrated embedding storage from local SDK to Supabase `pgvector`. This enables cloud-native similarity search and persistent memory across devices.
20
+ - **Database**: Added `alchemy_vectors` table and HNSW indexes for high-performance semantic retrieval.
21
+
22
+ ### Improved
23
+ - **Performance**: Implemented server-side similarity search via `match_vectors` RPC function, reducing latency for RAG and deduplication.
24
+
8
25
  ## [1.0.50] - 2026-01-26
9
26
 
10
27
  ### Improved
package/dist/api/index.js CHANGED
@@ -276,11 +276,8 @@ app.post('/api/browser-paths/validate', async (req, res) => {
276
276
  app.post('/api/test/analyze', async (req, res) => {
277
277
  const { text } = req.body;
278
278
  try {
279
- // Fetch settings from database
280
- let settings = {
281
- llm_provider: 'realtimexai',
282
- llm_model: 'gpt-4o'
283
- };
279
+ // Fetch settings from database (empty object = use dynamic defaults from SDK)
280
+ let settings = {};
284
281
  if (SupabaseService.isConfigured()) {
285
282
  const supabase = SupabaseService.getServiceRoleClient();
286
283
  const { data: userData } = await supabase.rpc('get_any_user_id');
@@ -306,9 +303,10 @@ app.post('/api/test/analyze', async (req, res) => {
306
303
  app.post('/api/llm/test', async (req, res) => {
307
304
  const { llmProvider, llmModel } = req.body;
308
305
  try {
306
+ // Pass provided values (if any) - if not provided, resolveChatProvider will get defaults from SDK
309
307
  const settings = {
310
- llm_provider: llmProvider || 'realtimexai',
311
- llm_model: llmModel || 'gpt-4o'
308
+ llm_provider: llmProvider,
309
+ llm_model: llmModel
312
310
  };
313
311
  const result = await alchemist.testConnection(settings);
314
312
  res.json(result);
@@ -89,10 +89,9 @@ export class AlchemistService {
89
89
  skipped: 0,
90
90
  errors: 0
91
91
  };
92
- console.log('[AlchemistService] LLM Config:', {
93
- provider: settings.llm_provider || 'realtimexai',
94
- model: settings.llm_model || 'gpt-4o'
95
- });
92
+ // Resolve LLM provider dynamically
93
+ const llmConfig = await SDKService.resolveChatProvider(settings);
94
+ console.log('[AlchemistService] LLM Config:', llmConfig);
96
95
  for (const entry of allowedEntries) {
97
96
  // Emit: Reading
98
97
  await this.processingEvents.log({
@@ -189,7 +188,8 @@ export class AlchemistService {
189
188
  }, supabase);
190
189
  stats.signals++;
191
190
  // 5. Generate Embedding & Check for Duplicates (non-blocking)
192
- if (settings.embedding_model && await embeddingService.isAvailable()) {
191
+ // Note: embedding_model is resolved dynamically via SDK if not in settings
192
+ if (await embeddingService.isAvailable()) {
193
193
  await this.processEmbedding(insertedSignal, settings, userId, supabase).catch((err) => {
194
194
  console.error('[AlchemistService] Embedding processing failed:', err);
195
195
  });
@@ -261,6 +261,8 @@ export class AlchemistService {
261
261
  if (!sdk) {
262
262
  throw new Error('RealTimeX SDK not available');
263
263
  }
264
+ // Resolve LLM provider dynamically from SDK
265
+ const { provider, model } = await SDKService.resolveChatProvider(settings);
264
266
  const prompt = `
265
267
  Act as "The Alchemist", a high-level intelligence analyst.
266
268
  Analyze the following article value based on the content and the User's Interests.
@@ -309,8 +311,8 @@ export class AlchemistService {
309
311
  { role: 'system', content: 'You are a precise analyzer. Return ONLY valid JSON, no other text.' },
310
312
  { role: 'user', content: prompt }
311
313
  ], {
312
- provider: settings.llm_provider || 'realtimexai',
313
- model: settings.llm_model || 'gpt-4o'
314
+ provider,
315
+ model
314
316
  });
315
317
  // SDK returns response.response?.content based on documentation
316
318
  const raw = response.response?.content || '{}';
@@ -329,16 +331,18 @@ export class AlchemistService {
329
331
  message: 'RealTimeX SDK not available. Please run via RealTimeX Desktop.'
330
332
  };
331
333
  }
334
+ // Resolve LLM provider dynamically from SDK
335
+ const { provider, model } = await SDKService.resolveChatProvider(settings);
332
336
  const response = await sdk.llm.chat([
333
337
  { role: 'user', content: 'Say "OK"' }
334
338
  ], {
335
- provider: settings.llm_provider || 'realtimexai',
336
- model: settings.llm_model || 'gpt-4o'
339
+ provider,
340
+ model
337
341
  });
338
342
  return {
339
343
  success: true,
340
344
  message: `Connection successful!`,
341
- model: settings.llm_model || 'gpt-4o'
345
+ model
342
346
  };
343
347
  }
344
348
  catch (error) {
@@ -392,6 +396,8 @@ export class AlchemistService {
392
396
  async processEmbedding(signal, settings, userId, supabase) {
393
397
  try {
394
398
  console.log('[AlchemistService] Generating embedding for signal:', signal.id);
399
+ // Resolve embedding provider dynamically
400
+ const { model: embeddingModel } = await SDKService.resolveEmbedProvider(settings);
395
401
  // Generate embedding
396
402
  const text = `${signal.title} ${signal.summary}`;
397
403
  const embedding = await embeddingService.generateEmbedding(text, settings);
@@ -410,20 +416,21 @@ export class AlchemistService {
410
416
  .eq('id', signal.id);
411
417
  return;
412
418
  }
413
- // Store embedding in RealTimeX vector storage
419
+ // Store embedding in Supabase pgvector storage
414
420
  await embeddingService.storeSignalEmbedding(signal.id, embedding, {
415
421
  title: signal.title,
416
422
  summary: signal.summary,
417
423
  url: signal.url,
418
424
  category: signal.category,
419
- userId
420
- });
425
+ userId,
426
+ model: embeddingModel
427
+ }, supabase);
421
428
  // Update signal metadata
422
429
  await supabase
423
430
  .from('signals')
424
431
  .update({
425
432
  has_embedding: true,
426
- embedding_model: settings.embedding_model
433
+ embedding_model: embeddingModel
427
434
  })
428
435
  .eq('id', signal.id);
429
436
  console.log('[AlchemistService] Embedding processed successfully for signal:', signal.id);
@@ -64,7 +64,7 @@ export class ChatService {
64
64
  let sources = [];
65
65
  // 3. Retrieve Context (if embedding checks out)
66
66
  if (queryEmbedding) {
67
- const similar = await embeddingService.findSimilarSignals(queryEmbedding, userId, 0.55, // Lowered threshold for better recall
67
+ const similar = await embeddingService.findSimilarSignals(queryEmbedding, userId, supabase, 0.55, // Lowered threshold for better recall
68
68
  10 // Increased Top K
69
69
  );
70
70
  console.log(`[ChatService] RAG Retrieval: Found ${similar.length} signals for query: "${content}"`);
@@ -106,9 +106,11 @@ Be concise, helpful, and professional.
106
106
  { role: 'user', content: finalPrompt } // Current turn with RAG context
107
107
  ];
108
108
  console.log('[ChatService] Final Prompt being sent to LLM:', JSON.stringify(messages, null, 2));
109
+ // Resolve LLM provider dynamically from SDK
110
+ const { provider, model } = await SDKService.resolveChatProvider(settings);
109
111
  const response = await sdk.llm.chat(messages, {
110
- provider: settings.llm_provider || 'realtimexai',
111
- model: settings.llm_model || 'gpt-4o'
112
+ provider,
113
+ model
112
114
  });
113
115
  console.log('[ChatService] LLM Response:', JSON.stringify(response, null, 2));
114
116
  const aiContent = response.response?.content || "I'm sorry, I couldn't generate a response. The LLM returned empty content.";
@@ -161,12 +163,14 @@ Be concise, helpful, and professional.
161
163
  const sdk = SDKService.getSDK();
162
164
  if (!sdk)
163
165
  return;
166
+ // Resolve LLM provider dynamically from SDK
167
+ const { provider, model } = await SDKService.resolveChatProvider(settings);
164
168
  const response = await sdk.llm.chat([
165
169
  { role: 'system', content: 'Generate a very short title (3-5 words) for this chat conversation. Return ONLY the title.' },
166
170
  { role: 'user', content: `User: ${userMsg}\nAI: ${aiMsg}` }
167
171
  ], {
168
- provider: settings.llm_provider || 'realtimexai',
169
- model: 'gpt-4o-mini'
172
+ provider,
173
+ model
170
174
  });
171
175
  const newTitle = response.response?.content?.replace(/['"]/g, '').trim();
172
176
  if (newTitle) {
@@ -18,7 +18,7 @@ export class DeduplicationService {
18
18
  try {
19
19
  // 1. Semantic Check (if embedding exists)
20
20
  if (embedding && embedding.length > 0) {
21
- const similar = await embeddingService.findSimilarSignals(embedding, userId, this.SIMILARITY_THRESHOLD, 5 // Check top 5 matches
21
+ const similar = await embeddingService.findSimilarSignals(embedding, userId, supabase, this.SIMILARITY_THRESHOLD, 5 // Check top 5 matches
22
22
  );
23
23
  if (similar.length > 0) {
24
24
  const bestMatch = similar[0];
@@ -154,6 +154,8 @@ export class DeduplicationService {
154
154
  // Fallback: use longer summary
155
155
  return existing.length >= newSummary.length ? existing : newSummary;
156
156
  }
157
+ // Resolve LLM provider dynamically from SDK
158
+ const { provider, model } = await SDKService.resolveChatProvider(settings);
157
159
  // Use LLM to intelligently merge summaries
158
160
  const response = await sdk.llm.chat([
159
161
  {
@@ -165,8 +167,8 @@ export class DeduplicationService {
165
167
  content: `Summary 1: ${existing}\nSummary 2: ${newSummary}\n\nMerged summary:`
166
168
  }
167
169
  ], {
168
- provider: settings.llm_provider || 'realtimexai',
169
- model: settings.llm_model || 'gpt-4o-mini'
170
+ provider,
171
+ model
170
172
  });
171
173
  const merged = response.response?.content?.trim();
172
174
  return merged || existing;
@@ -1,11 +1,11 @@
1
1
  import { SDKService } from './SDKService.js';
2
2
  /**
3
- * Embedding Service using RealTimeX SDK
4
- * Provides simplified interface for embedding generation and vector operations
3
+ * Embedding Service
4
+ * Uses RealTimeX SDK for embedding generation
5
+ * Uses Supabase pgvector for vector storage and similarity search
5
6
  * Gracefully degrades if SDK is not available
6
7
  */
7
8
  export class EmbeddingService {
8
- WORKSPACE_ID = 'alchemy-signals';
9
9
  SIMILARITY_THRESHOLD = 0.85;
10
10
  /**
11
11
  * Generate embedding for a single text
@@ -20,8 +20,8 @@ export class EmbeddingService {
20
20
  console.warn('[EmbeddingService] RealTimeX SDK not available');
21
21
  return null;
22
22
  }
23
- const provider = this.getProvider(settings);
24
- const model = settings.embedding_model || 'text-embedding-3-small';
23
+ // Resolve embedding provider dynamically from SDK
24
+ const { provider, model } = await SDKService.resolveEmbedProvider(settings);
25
25
  const response = await sdk.llm.embed(text, {
26
26
  provider,
27
27
  model
@@ -46,8 +46,8 @@ export class EmbeddingService {
46
46
  console.warn('[EmbeddingService] RealTimeX SDK not available');
47
47
  return null;
48
48
  }
49
- const provider = this.getProvider(settings);
50
- const model = settings.embedding_model || 'text-embedding-3-small';
49
+ // Resolve embedding provider dynamically from SDK
50
+ const { provider, model } = await SDKService.resolveEmbedProvider(settings);
51
51
  const response = await sdk.llm.embed(texts, {
52
52
  provider,
53
53
  model
@@ -60,22 +60,40 @@ export class EmbeddingService {
60
60
  }
61
61
  }
62
62
  /**
63
- * Store signal embedding in RealTimeX vector storage
63
+ * Store signal embedding in Supabase pgvector storage
64
64
  * @param signalId - Unique signal ID
65
65
  * @param embedding - Embedding vector
66
66
  * @param metadata - Signal metadata
67
+ * @param supabase - Supabase client
67
68
  */
68
- async storeSignalEmbedding(signalId, embedding, metadata) {
69
+ async storeSignalEmbedding(signalId, embedding, metadata, supabase) {
69
70
  try {
70
- const sdk = SDKService.getSDK();
71
- if (!sdk) {
72
- throw new Error('SDK not available');
71
+ // Format embedding as pgvector string
72
+ const embeddingStr = `[${embedding.join(',')}]`;
73
+ // Use model from metadata if provided, otherwise try to get default
74
+ let modelName = metadata.model || 'unknown';
75
+ if (!metadata.model) {
76
+ try {
77
+ const { model } = await SDKService.resolveEmbedProvider({});
78
+ modelName = model;
79
+ }
80
+ catch (e) {
81
+ // Keep 'unknown' if we can't resolve
82
+ }
83
+ }
84
+ const { error } = await supabase
85
+ .from('alchemy_vectors')
86
+ .upsert({
87
+ signal_id: signalId,
88
+ user_id: metadata.userId,
89
+ embedding: embeddingStr,
90
+ model: modelName
91
+ }, {
92
+ onConflict: 'signal_id,model'
93
+ });
94
+ if (error) {
95
+ throw error;
73
96
  }
74
- await sdk.llm.vectors.upsert([{
75
- id: signalId,
76
- vector: embedding,
77
- metadata
78
- }], { workspaceId: this.WORKSPACE_ID });
79
97
  console.log('[EmbeddingService] Stored embedding for signal:', signalId);
80
98
  }
81
99
  catch (error) {
@@ -84,32 +102,39 @@ export class EmbeddingService {
84
102
  }
85
103
  }
86
104
  /**
87
- * Find similar signals using semantic search
105
+ * Find similar signals using semantic search via Supabase pgvector
88
106
  * @param queryEmbedding - Query embedding vector
89
107
  * @param userId - User ID for filtering
108
+ * @param supabase - Supabase client
90
109
  * @param threshold - Similarity threshold (0-1)
91
110
  * @param limit - Max results
92
111
  * @returns Array of similar signals
93
112
  */
94
- async findSimilarSignals(queryEmbedding, userId, threshold = this.SIMILARITY_THRESHOLD, limit = 10) {
113
+ async findSimilarSignals(queryEmbedding, userId, supabase, threshold = this.SIMILARITY_THRESHOLD, limit = 10) {
95
114
  try {
96
- const sdk = SDKService.getSDK();
97
- if (!sdk) {
115
+ // Format embedding as pgvector string
116
+ const embeddingStr = `[${queryEmbedding.join(',')}]`;
117
+ const { data, error } = await supabase.rpc('match_vectors', {
118
+ query_embedding: embeddingStr,
119
+ match_threshold: threshold,
120
+ match_count: limit,
121
+ target_user_id: userId
122
+ });
123
+ if (error) {
124
+ console.error('[EmbeddingService] Similarity search RPC error:', error.message);
98
125
  return [];
99
126
  }
100
- const response = await sdk.llm.vectors.query(queryEmbedding, {
101
- topK: limit,
102
- workspaceId: this.WORKSPACE_ID
103
- });
104
- // Filter by similarity threshold and user
105
- const results = response.results || [];
106
- return results
107
- .filter((r) => r.metadata?.userId === userId)
108
- .filter((r) => r.score >= threshold)
109
- .map((r) => ({
110
- id: r.id,
111
- score: r.score,
112
- metadata: r.metadata || {}
127
+ // Map results to expected format
128
+ return (data || []).map((r) => ({
129
+ id: r.signal_id,
130
+ score: r.similarity,
131
+ metadata: {
132
+ title: r.title,
133
+ summary: r.summary,
134
+ url: r.url,
135
+ category: r.category,
136
+ userId
137
+ }
113
138
  }));
114
139
  }
115
140
  catch (error) {
@@ -120,39 +145,24 @@ export class EmbeddingService {
120
145
  /**
121
146
  * Delete all embeddings for a user
122
147
  * @param userId - User ID
148
+ * @param supabase - Supabase client
123
149
  */
124
- async deleteUserEmbeddings(userId) {
150
+ async deleteUserEmbeddings(userId, supabase) {
125
151
  try {
126
- // Note: Current SDK only supports deleteAll
127
- // In future, we may need user-specific workspaces
128
- console.warn('[EmbeddingService] User-specific deletion not yet supported');
152
+ const { error } = await supabase
153
+ .from('alchemy_vectors')
154
+ .delete()
155
+ .eq('user_id', userId);
156
+ if (error) {
157
+ throw error;
158
+ }
159
+ console.log('[EmbeddingService] Deleted all embeddings for user:', userId);
129
160
  }
130
161
  catch (error) {
131
162
  console.error('[EmbeddingService] Deletion failed:', error.message);
132
163
  throw error;
133
164
  }
134
165
  }
135
- /**
136
- * Determine provider from settings
137
- * @param settings - Alchemy settings
138
- * @returns Provider name
139
- */
140
- getProvider(settings) {
141
- // Use embedding_provider if set
142
- if (settings.embedding_provider) {
143
- return settings.embedding_provider;
144
- }
145
- // Fallback: detect from base URL (legacy)
146
- if (settings.embedding_base_url) {
147
- const url = settings.embedding_base_url.toLowerCase();
148
- if (url.includes('openai'))
149
- return 'openai';
150
- if (url.includes('google') || url.includes('gemini'))
151
- return 'gemini';
152
- }
153
- // Default to realtimexai
154
- return 'realtimexai';
155
- }
156
166
  /**
157
167
  * Check if embedding service is available
158
168
  * @returns True if SDK is configured and available
@@ -107,6 +107,130 @@ export class SDKService {
107
107
  this.instance = null;
108
108
  this.initAttempted = false;
109
109
  }
110
+ // Cache for default providers (avoid repeated SDK calls)
111
+ static defaultChatProvider = null;
112
+ static defaultEmbedProvider = null;
113
+ /**
114
+ * Get default chat provider/model from SDK dynamically
115
+ * Caches result to avoid repeated SDK calls
116
+ */
117
+ static async getDefaultChatProvider() {
118
+ // Return cached if available
119
+ if (this.defaultChatProvider) {
120
+ return this.defaultChatProvider;
121
+ }
122
+ const sdk = this.getSDK();
123
+ if (!sdk) {
124
+ throw new Error('RealTimeX SDK not available. Cannot determine default LLM provider.');
125
+ }
126
+ try {
127
+ const { providers } = await this.withTimeout(sdk.llm.chatProviders(), 30000, 'Chat providers fetch timed out');
128
+ if (!providers || providers.length === 0) {
129
+ throw new Error('No LLM providers available. Please configure a provider in RealTimeX Desktop.');
130
+ }
131
+ // Find first provider with available models
132
+ for (const p of providers) {
133
+ if (p.models && p.models.length > 0) {
134
+ this.defaultChatProvider = {
135
+ provider: p.provider,
136
+ model: p.models[0].id
137
+ };
138
+ console.log(`[SDKService] Default chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
139
+ return this.defaultChatProvider;
140
+ }
141
+ }
142
+ throw new Error('No LLM models available. Please configure a model in RealTimeX Desktop.');
143
+ }
144
+ catch (error) {
145
+ console.error('[SDKService] Failed to get default chat provider:', error.message);
146
+ throw error;
147
+ }
148
+ }
149
+ /**
150
+ * Get default embedding provider/model from SDK dynamically
151
+ * Caches result to avoid repeated SDK calls
152
+ */
153
+ static async getDefaultEmbedProvider() {
154
+ // Return cached if available
155
+ if (this.defaultEmbedProvider) {
156
+ return this.defaultEmbedProvider;
157
+ }
158
+ const sdk = this.getSDK();
159
+ if (!sdk) {
160
+ throw new Error('RealTimeX SDK not available. Cannot determine default embedding provider.');
161
+ }
162
+ try {
163
+ const { providers } = await this.withTimeout(sdk.llm.embedProviders(), 30000, 'Embed providers fetch timed out');
164
+ if (!providers || providers.length === 0) {
165
+ throw new Error('No embedding providers available. Please configure a provider in RealTimeX Desktop.');
166
+ }
167
+ // Find first provider with available models
168
+ for (const p of providers) {
169
+ if (p.models && p.models.length > 0) {
170
+ this.defaultEmbedProvider = {
171
+ provider: p.provider,
172
+ model: p.models[0].id
173
+ };
174
+ console.log(`[SDKService] Default embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
175
+ return this.defaultEmbedProvider;
176
+ }
177
+ }
178
+ throw new Error('No embedding models available. Please configure a model in RealTimeX Desktop.');
179
+ }
180
+ catch (error) {
181
+ console.error('[SDKService] Failed to get default embed provider:', error.message);
182
+ throw error;
183
+ }
184
+ }
185
+ // Default provider/model configuration
186
+ // realtimexai routes through RealTimeX Desktop to user's configured providers
187
+ static DEFAULT_LLM_PROVIDER = 'realtimexai';
188
+ static DEFAULT_LLM_MODEL = 'gpt-4.1-mini';
189
+ static DEFAULT_EMBED_PROVIDER = 'realtimexai';
190
+ static DEFAULT_EMBED_MODEL = 'text-embedding-3-small';
191
+ /**
192
+ * Resolve LLM provider/model - use settings if available, otherwise use defaults
193
+ */
194
+ static async resolveChatProvider(settings) {
195
+ // If both provider and model are set in settings, use them
196
+ if (settings.llm_provider && settings.llm_model) {
197
+ return { provider: settings.llm_provider, model: settings.llm_model };
198
+ }
199
+ // Try to get from SDK discovery first
200
+ try {
201
+ return await this.getDefaultChatProvider();
202
+ }
203
+ catch (error) {
204
+ // Fallback to hardcoded defaults if SDK discovery fails
205
+ console.log(`[SDKService] Using default LLM: ${this.DEFAULT_LLM_PROVIDER}/${this.DEFAULT_LLM_MODEL}`);
206
+ return { provider: this.DEFAULT_LLM_PROVIDER, model: this.DEFAULT_LLM_MODEL };
207
+ }
208
+ }
209
+ /**
210
+ * Resolve embedding provider/model - use settings if available, otherwise use defaults
211
+ */
212
+ static async resolveEmbedProvider(settings) {
213
+ // If both provider and model are set in settings, use them
214
+ if (settings.embedding_provider && settings.embedding_model) {
215
+ return { provider: settings.embedding_provider, model: settings.embedding_model };
216
+ }
217
+ // Try to get from SDK discovery first
218
+ try {
219
+ return await this.getDefaultEmbedProvider();
220
+ }
221
+ catch (error) {
222
+ // Fallback to hardcoded defaults if SDK discovery fails
223
+ console.log(`[SDKService] Using default embedding: ${this.DEFAULT_EMBED_PROVIDER}/${this.DEFAULT_EMBED_MODEL}`);
224
+ return { provider: this.DEFAULT_EMBED_PROVIDER, model: this.DEFAULT_EMBED_MODEL };
225
+ }
226
+ }
227
+ /**
228
+ * Clear provider cache (useful when providers change)
229
+ */
230
+ static clearProviderCache() {
231
+ this.defaultChatProvider = null;
232
+ this.defaultEmbedProvider = null;
233
+ }
110
234
  /**
111
235
  * Helper to wrap a promise with a timeout
112
236
  */
@@ -147,7 +271,6 @@ export class SDKService {
147
271
  throw new Error('RealTimeX SDK not linked. Cannot get app data dir.');
148
272
  }
149
273
  try {
150
- // @ts-ignore - SDK method for getting app data directory
151
274
  return await sdk.getAppDataDir();
152
275
  }
153
276
  catch (error) {
@@ -225,9 +225,11 @@ export class TransmuteService {
225
225
  if (!sdk) {
226
226
  throw new Error('RealTimeX SDK not available. Please ensure the desktop app is running.');
227
227
  }
228
- // Use engine config for model/provider if available, else defaults
229
- const provider = engine.config.llm_provider || 'realtimexai';
230
- const model = engine.config.llm_model || 'gpt-4o-mini';
228
+ // Resolve LLM provider dynamically from SDK using engine config
229
+ const { provider, model } = await SDKService.resolveChatProvider({
230
+ llm_provider: engine.config.llm_provider,
231
+ llm_model: engine.config.llm_model
232
+ });
231
233
  const response = await sdk.llm.chat([
232
234
  { role: 'system', content: systemPrompt },
233
235
  { role: 'user', content: userPrompt }
@@ -273,12 +275,12 @@ export class TransmuteService {
273
275
  continue;
274
276
  }
275
277
  console.log(`[Transmute] Bootstrapping missing category engine: ${title}`);
278
+ // Note: llm_provider and llm_model are intentionally omitted
279
+ // They will be resolved dynamically at runtime via SDKService.resolveChatProvider()
276
280
  const config = {
277
281
  category,
278
282
  execution_mode: 'desktop',
279
283
  schedule: 'Daily',
280
- llm_provider: 'realtimexai',
281
- llm_model: 'gpt-4o',
282
284
  max_signals: 30,
283
285
  custom_prompt: `Create a comprehensive daily newsletter focused on ${category}. Highlight the most important developments, key insights, and actionable takeaways. Use a professional, insight-driven tone with clear structure: start with 'The Big Story' followed by 'Quick Hits' for other notable items.`
284
286
  };
@@ -354,12 +356,12 @@ export class TransmuteService {
354
356
  continue;
355
357
  }
356
358
  console.log(`[Transmute] Creating dynamic tag engine for "${tag}" (${count} signals)`);
359
+ // Note: llm_provider and llm_model are intentionally omitted
360
+ // They will be resolved dynamically at runtime via SDKService.resolveChatProvider()
357
361
  const config = {
358
362
  tag,
359
363
  execution_mode: 'desktop',
360
- schedule: 'Daily',
361
- llm_provider: 'realtimexai',
362
- llm_model: 'gpt-4o'
364
+ schedule: 'Daily'
363
365
  };
364
366
  const { error } = await supabase
365
367
  .from('engines')