@memberjunction/ai-prompts 5.4.1 → 5.6.0

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.
@@ -1,7 +1,7 @@
1
1
  import { BaseLLM, ChatParams, ChatMessageRole, GetAIAPIKey, ErrorAnalyzer } from '@memberjunction/ai';
2
2
  import { AIModelSelectionInfo } from '@memberjunction/ai-core-plus';
3
3
  import { LogErrorEx, LogStatus, LogStatusEx, IsVerboseLoggingEnabled, Metadata } from '@memberjunction/core';
4
- import { CleanJSON, MJGlobal, JSONValidator, ValidationResult, ValidationErrorInfo, ValidationErrorType } from '@memberjunction/global';
4
+ import { CleanJSON, MJGlobal, JSONValidator, ValidationResult, ValidationErrorInfo, ValidationErrorType, UUIDsEqual, NormalizeUUID } from '@memberjunction/global';
5
5
  import { CredentialEngine } from '@memberjunction/credentials';
6
6
  import { TemplateEngineServer } from '@memberjunction/templates';
7
7
  import { ExecutionPlanner } from './ExecutionPlanner.js';
@@ -90,9 +90,9 @@ export class AIPromptRunner {
90
90
  if (!inferenceProviderType) {
91
91
  // Fallback to checking if it's not a model developer (should rarely happen)
92
92
  const modelDeveloperType = AIEngine.Instance.VendorTypeDefinitions.find(vt => vt.Name === 'Model Developer');
93
- return modelVendor.TypeID !== modelDeveloperType?.ID;
93
+ return !UUIDsEqual(modelVendor.TypeID, modelDeveloperType?.ID);
94
94
  }
95
- return modelVendor.TypeID === inferenceProviderType.ID;
95
+ return UUIDsEqual(modelVendor.TypeID, inferenceProviderType.ID);
96
96
  }
97
97
  /**
98
98
  * Resolves credentials for AI model execution using a hierarchical resolution system.
@@ -125,7 +125,7 @@ export class AIPromptRunner {
125
125
  await CredentialEngine.Instance.Config(false, params.contextUser);
126
126
  // Priority 2: PromptModel bindings (most specific) - with failover
127
127
  if (promptId && modelId) {
128
- const promptModel = AIEngine.Instance.PromptModels.find(pm => pm.PromptID === promptId && pm.ModelID === modelId);
128
+ const promptModel = AIEngine.Instance.PromptModels.find(pm => UUIDsEqual(pm.PromptID, promptId) && UUIDsEqual(pm.ModelID, modelId));
129
129
  if (promptModel) {
130
130
  const bindings = AIEngine.Instance.GetCredentialBindingsForTarget('PromptModel', promptModel.ID);
131
131
  const result = await this.tryCredentialBindingsWithFailover(bindings, 'AICredentialBinding(PromptModel)', params, verbose);
@@ -135,7 +135,7 @@ export class AIPromptRunner {
135
135
  }
136
136
  // Priority 3: ModelVendor bindings - with failover
137
137
  if (modelId && vendorId) {
138
- const modelVendor = AIEngine.Instance.ModelVendors.find(mv => mv.ModelID === modelId && mv.VendorID === vendorId && mv.Status === 'Active');
138
+ const modelVendor = AIEngine.Instance.ModelVendors.find(mv => UUIDsEqual(mv.ModelID, modelId) && UUIDsEqual(mv.VendorID, vendorId) && mv.Status === 'Active');
139
139
  if (modelVendor) {
140
140
  const bindings = AIEngine.Instance.GetCredentialBindingsForTarget('ModelVendor', modelVendor.ID);
141
141
  const result = await this.tryCredentialBindingsWithFailover(bindings, 'AICredentialBinding(ModelVendor)', params, verbose);
@@ -153,7 +153,7 @@ export class AIPromptRunner {
153
153
  // Priority 5: Type-based default credential
154
154
  // If the vendor declares a CredentialTypeID, try to find a default credential of that type
155
155
  if (vendorId) {
156
- const vendor = AIEngine.Instance.Vendors.find(v => v.ID === vendorId);
156
+ const vendor = AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, vendorId));
157
157
  if (vendor?.CredentialTypeID) {
158
158
  const defaultCredential = this.findDefaultCredentialByType(vendor.CredentialTypeID);
159
159
  if (defaultCredential) {
@@ -271,7 +271,7 @@ export class AIPromptRunner {
271
271
  */
272
272
  findDefaultCredentialByType(credentialTypeId) {
273
273
  const credentials = CredentialEngine.Instance.Credentials;
274
- return credentials.find(c => c.CredentialTypeID === credentialTypeId &&
274
+ return credentials.find(c => UUIDsEqual(c.CredentialTypeID, credentialTypeId) &&
275
275
  c.IsDefault === true &&
276
276
  c.IsActive === true &&
277
277
  (!c.ExpiresAt || new Date(c.ExpiresAt) > new Date())) || null;
@@ -305,14 +305,14 @@ export class AIPromptRunner {
305
305
  }
306
306
  // Priority 2: PromptModel bindings
307
307
  if (promptId && modelId) {
308
- const promptModel = AIEngine.Instance.PromptModels.find(pm => pm.PromptID === promptId && pm.ModelID === modelId);
308
+ const promptModel = AIEngine.Instance.PromptModels.find(pm => UUIDsEqual(pm.PromptID, promptId) && UUIDsEqual(pm.ModelID, modelId));
309
309
  if (promptModel && AIEngine.Instance.HasCredentialBindings('PromptModel', promptModel.ID)) {
310
310
  return true;
311
311
  }
312
312
  }
313
313
  // Priority 3: ModelVendor bindings
314
314
  if (modelId && vendorId) {
315
- const modelVendor = AIEngine.Instance.ModelVendors.find(mv => mv.ModelID === modelId && mv.VendorID === vendorId && mv.Status === 'Active');
315
+ const modelVendor = AIEngine.Instance.ModelVendors.find(mv => UUIDsEqual(mv.ModelID, modelId) && UUIDsEqual(mv.VendorID, vendorId) && mv.Status === 'Active');
316
316
  if (modelVendor && AIEngine.Instance.HasCredentialBindings('ModelVendor', modelVendor.ID)) {
317
317
  return true;
318
318
  }
@@ -325,7 +325,7 @@ export class AIPromptRunner {
325
325
  }
326
326
  // Priority 5: Type-based default credential
327
327
  if (vendorId) {
328
- const vendor = AIEngine.Instance.Vendors.find(v => v.ID === vendorId);
328
+ const vendor = AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, vendorId));
329
329
  if (vendor?.CredentialTypeID) {
330
330
  const defaultCredential = this.findDefaultCredentialByType(vendor.CredentialTypeID);
331
331
  if (defaultCredential) {
@@ -562,8 +562,8 @@ export class AIPromptRunner {
562
562
  // we received model selection info, need to lookup vendor driver class and api name from there
563
563
  const vendorID = modelSelectionInfo.vendorSelected?.ID;
564
564
  const modelID = modelSelectionInfo.modelSelected.ID;
565
- const modelVendor = AIEngine.Instance.ModelVendors.find(mv => mv.VendorID === vendorID &&
566
- mv.ModelID === modelID);
565
+ const modelVendor = AIEngine.Instance.ModelVendors.find(mv => UUIDsEqual(mv.VendorID, vendorID) &&
566
+ UUIDsEqual(mv.ModelID, modelID));
567
567
  if (modelVendor) {
568
568
  vendorDriverClass = modelVendor.DriverClass;
569
569
  vendorApiName = modelVendor.APIName;
@@ -684,9 +684,9 @@ export class AIPromptRunner {
684
684
  this.logStatus(` Using prompt "${modelSelectionPrompt.Name}" for model selection in parallel execution`, true, params);
685
685
  }
686
686
  // Get prompt-specific model associations using the model selection prompt
687
- const promptModels = AIEngine.Instance.PromptModels.filter((pm) => pm.PromptID === modelSelectionPrompt.ID &&
687
+ const promptModels = AIEngine.Instance.PromptModels.filter((pm) => UUIDsEqual(pm.PromptID, modelSelectionPrompt.ID) &&
688
688
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
689
- (!params.configurationId || !pm.ConfigurationID || pm.ConfigurationID === params.configurationId));
689
+ (!params.configurationId || !pm.ConfigurationID || UUIDsEqual(pm.ConfigurationID, params.configurationId)));
690
690
  // Create execution plan using the modelSelectionPrompt for model configurations
691
691
  executionTasks = this._executionPlanner.createExecutionPlan(modelSelectionPrompt, promptModels, AIEngine.Instance.Models, renderedPromptText, params.contextUser, params.configurationId, params.conversationMessages, params.templateMessageRole || 'system');
692
692
  }
@@ -875,7 +875,7 @@ export class AIPromptRunner {
875
875
  async loadTemplate(templateId, _contextUser) {
876
876
  try {
877
877
  // Use the template engine to find the template
878
- const template = this._templateEngine.Templates.find((t) => t.ID === templateId);
878
+ const template = this._templateEngine.Templates.find((t) => UUIDsEqual(t.ID, templateId));
879
879
  return template || null;
880
880
  }
881
881
  catch (error) {
@@ -1089,7 +1089,7 @@ export class AIPromptRunner {
1089
1089
  }
1090
1090
  // Get configuration info if provided
1091
1091
  if (configurationId) {
1092
- configuration = AIEngine.Instance.Configurations.find(c => c.ID === configurationId);
1092
+ configuration = AIEngine.Instance.Configurations.find(c => UUIDsEqual(c.ID, configurationId));
1093
1093
  configurationName = configuration?.Name;
1094
1094
  }
1095
1095
  // Build unified list of model-vendor candidates
@@ -1170,7 +1170,7 @@ export class AIPromptRunner {
1170
1170
  // Get selected vendor entity
1171
1171
  let selectedVendor;
1172
1172
  if (selected.vendorId) {
1173
- selectedVendor = AIEngine.Instance.Vendors.find(v => v.ID === selected.vendorId);
1173
+ selectedVendor = AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, selected.vendorId));
1174
1174
  }
1175
1175
  return {
1176
1176
  model: selected.model,
@@ -1246,12 +1246,12 @@ export class AIPromptRunner {
1246
1246
  * Returns candidates for the single model if it's active and compatible.
1247
1247
  */
1248
1248
  buildCandidatesForExplicitModel(explicitModelId, prompt, preferredVendorId) {
1249
- const model = AIEngine.Instance.Models.find(m => m.ID === explicitModelId);
1249
+ const model = AIEngine.Instance.Models.find(m => UUIDsEqual(m.ID, explicitModelId));
1250
1250
  if (!model || !model.IsActive) {
1251
1251
  return [];
1252
1252
  }
1253
1253
  // Check model type compatibility
1254
- if (prompt.AIModelTypeID && model.AIModelTypeID !== prompt.AIModelTypeID) {
1254
+ if (prompt.AIModelTypeID && !UUIDsEqual(model.AIModelTypeID, prompt.AIModelTypeID)) {
1255
1255
  return [];
1256
1256
  }
1257
1257
  const candidates = this.createCandidatesForModel(model, 20000, 'explicit', preferredVendorId);
@@ -1266,7 +1266,7 @@ export class AIPromptRunner {
1266
1266
  */
1267
1267
  buildCandidatesForSpecificStrategy(prompt, configurationId, verbose) {
1268
1268
  // Get all active AIPromptModel records for this prompt
1269
- const allPromptModels = AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID && (pm.Status === 'Active' || pm.Status === 'Preview'));
1269
+ const allPromptModels = AIEngine.Instance.PromptModels.filter(pm => UUIDsEqual(pm.PromptID, prompt.ID) && (pm.Status === 'Active' || pm.Status === 'Preview'));
1270
1270
  // Filter by configuration matching rules
1271
1271
  const promptModels = this.filterPromptModelsByConfiguration(allPromptModels, configurationId);
1272
1272
  // Sort: config-specific before universal, then by priority DESC within each group
@@ -1290,7 +1290,7 @@ export class AIPromptRunner {
1290
1290
  */
1291
1291
  buildCandidatesForGeneralSelection(prompt, configurationId, preferredVendorId, verbose) {
1292
1292
  const preferredVendorName = preferredVendorId ?
1293
- AIEngine.Instance.Vendors.find(v => v.ID === preferredVendorId)?.Name : undefined;
1293
+ AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, preferredVendorId))?.Name : undefined;
1294
1294
  // Get prompt models for configuration
1295
1295
  const promptModels = this.getPromptModelsForConfiguration(prompt, configurationId);
1296
1296
  const candidates = [];
@@ -1318,9 +1318,9 @@ export class AIPromptRunner {
1318
1318
  if (configurationId) {
1319
1319
  // Get the configuration inheritance chain
1320
1320
  const chain = AIEngine.Instance.GetConfigurationChain(configurationId);
1321
- const chainIds = new Set(chain.map(c => c.ID));
1321
+ const chainIds = new Set(chain.map(c => NormalizeUUID(c.ID)));
1322
1322
  // Include models matching any config in the chain, plus null-config (universal fallback)
1323
- return allPromptModels.filter(pm => (pm.ConfigurationID && chainIds.has(pm.ConfigurationID)) ||
1323
+ return allPromptModels.filter(pm => (pm.ConfigurationID && chainIds.has(NormalizeUUID(pm.ConfigurationID))) ||
1324
1324
  pm.ConfigurationID === null);
1325
1325
  }
1326
1326
  else {
@@ -1359,7 +1359,7 @@ export class AIPromptRunner {
1359
1359
  buildCandidatesFromPromptModels(promptModels) {
1360
1360
  const candidates = [];
1361
1361
  for (const pm of promptModels) {
1362
- const model = AIEngine.Instance.Models.find(m => m.ID === pm.ModelID);
1362
+ const model = AIEngine.Instance.Models.find(m => UUIDsEqual(m.ID, pm.ModelID));
1363
1363
  if (!model || !model.IsActive)
1364
1364
  continue;
1365
1365
  if (pm.VendorID) {
@@ -1381,8 +1381,8 @@ export class AIPromptRunner {
1381
1381
  * Helper: Create candidate for specific vendor from AIPromptModel.
1382
1382
  */
1383
1383
  createCandidateForSpecificVendor(model, promptModel) {
1384
- const modelVendor = AIEngine.Instance.ModelVendors.find(mv => mv.ModelID === promptModel.ModelID &&
1385
- mv.VendorID === promptModel.VendorID &&
1384
+ const modelVendor = AIEngine.Instance.ModelVendors.find(mv => UUIDsEqual(mv.ModelID, promptModel.ModelID) &&
1385
+ UUIDsEqual(mv.VendorID, promptModel.VendorID) &&
1386
1386
  mv.Status === 'Active' &&
1387
1387
  this.isInferenceProvider(mv));
1388
1388
  if (!modelVendor)
@@ -1405,7 +1405,7 @@ export class AIPromptRunner {
1405
1405
  */
1406
1406
  createCandidatesForAllVendors(model) {
1407
1407
  const vendors = AIEngine.Instance.ModelVendors
1408
- .filter(mv => mv.ModelID === model.ID &&
1408
+ .filter(mv => UUIDsEqual(mv.ModelID, model.ID) &&
1409
1409
  mv.Status === 'Active' &&
1410
1410
  this.isInferenceProvider(mv))
1411
1411
  .sort((a, b) => (b.Priority || 0) - (a.Priority || 0));
@@ -1448,9 +1448,9 @@ export class AIPromptRunner {
1448
1448
  const chain = AIEngine.Instance.GetConfigurationChain(configurationId);
1449
1449
  // Walk the chain looking for prompt models
1450
1450
  for (const config of chain) {
1451
- const promptModels = AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
1451
+ const promptModels = AIEngine.Instance.PromptModels.filter(pm => UUIDsEqual(pm.PromptID, prompt.ID) &&
1452
1452
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
1453
- pm.ConfigurationID === config.ID);
1453
+ UUIDsEqual(pm.ConfigurationID, config.ID));
1454
1454
  if (promptModels.length > 0) {
1455
1455
  return promptModels;
1456
1456
  }
@@ -1459,7 +1459,7 @@ export class AIPromptRunner {
1459
1459
  LogStatus(`No models found in configuration chain for "${configurationId}", falling back to default models`);
1460
1460
  }
1461
1461
  // Return null-config (universal) models
1462
- return AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
1462
+ return AIEngine.Instance.PromptModels.filter(pm => UUIDsEqual(pm.PromptID, prompt.ID) &&
1463
1463
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
1464
1464
  !pm.ConfigurationID);
1465
1465
  }
@@ -1468,7 +1468,7 @@ export class AIPromptRunner {
1468
1468
  */
1469
1469
  addPromptSpecificCandidates(candidates, promptModels, preferredVendorId) {
1470
1470
  for (const pm of promptModels) {
1471
- const model = AIEngine.Instance.Models.find(m => m.ID === pm.ModelID);
1471
+ const model = AIEngine.Instance.Models.find(m => UUIDsEqual(m.ID, pm.ModelID));
1472
1472
  if (model && model.IsActive) {
1473
1473
  const modelCandidates = this.createCandidatesForModel(model, 5000, 'prompt-model', preferredVendorId, pm.Priority);
1474
1474
  candidates.push(...modelCandidates);
@@ -1484,14 +1484,14 @@ export class AIPromptRunner {
1484
1484
  // Add models from parent configs (skip index 0 which is the direct config, already handled)
1485
1485
  for (let i = 1; i < chain.length; i++) {
1486
1486
  const parentConfig = chain[i];
1487
- const parentModels = AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
1487
+ const parentModels = AIEngine.Instance.PromptModels.filter(pm => UUIDsEqual(pm.PromptID, prompt.ID) &&
1488
1488
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
1489
- pm.ConfigurationID === parentConfig.ID);
1489
+ UUIDsEqual(pm.ConfigurationID, parentConfig.ID));
1490
1490
  if (parentModels.length > 0 && verbose) {
1491
1491
  LogStatus(`Adding ${parentModels.length} models from parent config "${parentConfig.Name}" as fallback`);
1492
1492
  }
1493
1493
  for (const pm of parentModels) {
1494
- const model = AIEngine.Instance.Models.find(m => m.ID === pm.ModelID);
1494
+ const model = AIEngine.Instance.Models.find(m => UUIDsEqual(m.ID, pm.ModelID));
1495
1495
  if (model && model.IsActive) {
1496
1496
  // Decrease base priority for each level up the chain (3000, 2500, 2000, etc.)
1497
1497
  const basePriority = 3000 - (i * 500);
@@ -1501,14 +1501,14 @@ export class AIPromptRunner {
1501
1501
  }
1502
1502
  }
1503
1503
  // Finally add NULL config models (universal fallback) with lowest priority
1504
- const nullConfigModels = AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
1504
+ const nullConfigModels = AIEngine.Instance.PromptModels.filter(pm => UUIDsEqual(pm.PromptID, prompt.ID) &&
1505
1505
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
1506
1506
  !pm.ConfigurationID);
1507
1507
  if (nullConfigModels.length > 0 && verbose) {
1508
1508
  LogStatus(`Adding ${nullConfigModels.length} NULL configuration models as universal fallback`);
1509
1509
  }
1510
1510
  for (const pm of nullConfigModels) {
1511
- const model = AIEngine.Instance.Models.find(m => m.ID === pm.ModelID);
1511
+ const model = AIEngine.Instance.Models.find(m => UUIDsEqual(m.ID, pm.ModelID));
1512
1512
  if (model && model.IsActive) {
1513
1513
  const modelCandidates = this.createCandidatesForModel(model, 1000, // Lowest priority tier
1514
1514
  'prompt-model', preferredVendorId, pm.Priority);
@@ -1534,9 +1534,9 @@ export class AIPromptRunner {
1534
1534
  */
1535
1535
  getModelPoolForStrategy(prompt, preferredVendorName) {
1536
1536
  return AIEngine.Instance.Models.filter(m => m.IsActive &&
1537
- (!prompt.AIModelTypeID || m.AIModelTypeID === prompt.AIModelTypeID) &&
1537
+ (!prompt.AIModelTypeID || UUIDsEqual(m.AIModelTypeID, prompt.AIModelTypeID)) &&
1538
1538
  (!preferredVendorName ||
1539
- AIEngine.Instance.ModelVendors.some(mv => mv.ModelID === m.ID &&
1539
+ AIEngine.Instance.ModelVendors.some(mv => UUIDsEqual(mv.ModelID, m.ID) &&
1540
1540
  mv.Status === 'Active' &&
1541
1541
  mv.Vendor === preferredVendorName &&
1542
1542
  this.isInferenceProvider(mv))));
@@ -1580,11 +1580,11 @@ export class AIPromptRunner {
1580
1580
  const modelCandidates = [];
1581
1581
  // Get all vendors for this model - filter for inference providers only
1582
1582
  const modelVendors = AIEngine.Instance.ModelVendors
1583
- .filter(mv => mv.ModelID === model.ID && mv.Status === 'Active' && this.isInferenceProvider(mv))
1583
+ .filter(mv => UUIDsEqual(mv.ModelID, model.ID) && mv.Status === 'Active' && this.isInferenceProvider(mv))
1584
1584
  .sort((a, b) => b.Priority - a.Priority);
1585
1585
  // First, add preferred vendor if it exists
1586
1586
  if (preferredVendorId) {
1587
- const preferredVendor = modelVendors.find(mv => mv.VendorID === preferredVendorId);
1587
+ const preferredVendor = modelVendors.find(mv => UUIDsEqual(mv.VendorID, preferredVendorId));
1588
1588
  if (preferredVendor) {
1589
1589
  modelCandidates.push({
1590
1590
  model,
@@ -1601,7 +1601,7 @@ export class AIPromptRunner {
1601
1601
  }
1602
1602
  // Then add other vendors in priority order
1603
1603
  for (const vendor of modelVendors) {
1604
- if (vendor.VendorID !== preferredVendorId) {
1604
+ if (!UUIDsEqual(vendor.VendorID, preferredVendorId)) {
1605
1605
  modelCandidates.push({
1606
1606
  model,
1607
1607
  vendorId: vendor.VendorID,
@@ -1654,8 +1654,8 @@ export class AIPromptRunner {
1654
1654
  return validModels.map(considered => {
1655
1655
  // Find matching model vendor for driver and API info
1656
1656
  const modelVendor = considered.vendor
1657
- ? AIEngine.Instance.ModelVendors.find(mv => mv.ModelID === considered.model.ID &&
1658
- mv.VendorID === considered.vendor.ID)
1657
+ ? AIEngine.Instance.ModelVendors.find(mv => UUIDsEqual(mv.ModelID, considered.model.ID) &&
1658
+ UUIDsEqual(mv.VendorID, considered.vendor.ID))
1659
1659
  : undefined;
1660
1660
  return {
1661
1661
  model: considered.model,
@@ -1702,7 +1702,7 @@ export class AIPromptRunner {
1702
1702
  // Get vendor entity from AIEngine cache if vendorId is available
1703
1703
  let vendorEntity;
1704
1704
  if (candidate.vendorId) {
1705
- vendorEntity = AIEngine.Instance.Vendors.find(v => v.ID === candidate.vendorId);
1705
+ vendorEntity = AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, candidate.vendorId));
1706
1706
  }
1707
1707
  // Track this model as considered with availability status
1708
1708
  consideredModels.push({
@@ -1715,8 +1715,8 @@ export class AIPromptRunner {
1715
1715
  }
1716
1716
  // Select the first available candidate (highest priority with API key)
1717
1717
  const selected = consideredModels.find(m => m.available);
1718
- const selectedCandidate = selected ? candidates.find(c => c.model.ID === selected.model.ID &&
1719
- c.vendorId === selected.vendor?.ID) : null;
1718
+ const selectedCandidate = selected ? candidates.find(c => UUIDsEqual(c.model.ID, selected.model.ID) &&
1719
+ UUIDsEqual(c.vendorId, selected.vendor?.ID)) : null;
1720
1720
  if (selectedCandidate) {
1721
1721
  const validCount = consideredModels.filter(m => m.available).length;
1722
1722
  this.logStatus(` Selected model ${selectedCandidate.model.Name} with ${selectedCandidate.vendorName || 'default'} vendor (driver: ${selectedCandidate.driverClass})`, true);
@@ -1807,7 +1807,7 @@ export class AIPromptRunner {
1807
1807
  else {
1808
1808
  // Fallback: grab the highest priority AI Model Vendor record for this model (inference providers only)
1809
1809
  const modelVendors = AIEngine.Instance.ModelVendors
1810
- .filter((mv) => mv.ModelID === model.ID && mv.Status === 'Active' && this.isInferenceProvider(mv))
1810
+ .filter((mv) => UUIDsEqual(mv.ModelID, model.ID) && mv.Status === 'Active' && this.isInferenceProvider(mv))
1811
1811
  .sort((a, b) => b.Priority - a.Priority);
1812
1812
  if (modelVendors.length > 0) {
1813
1813
  promptRun.VendorID = modelVendors[0].VendorID;
@@ -2100,7 +2100,7 @@ export class AIPromptRunner {
2100
2100
  let allModels;
2101
2101
  if (prompt.AIModelTypeID) {
2102
2102
  // Find the model type from the prompt
2103
- const modelType = aiEngine.ModelTypes.find(mt => mt.ID === prompt.AIModelTypeID);
2103
+ const modelType = aiEngine.ModelTypes.find(mt => UUIDsEqual(mt.ID, prompt.AIModelTypeID));
2104
2104
  if (!modelType) {
2105
2105
  throw new Error(`Model type ${prompt.AIModelTypeID} not found`);
2106
2106
  }
@@ -2172,10 +2172,10 @@ export class AIPromptRunner {
2172
2172
  promptRun.FailoverDurations = JSON.stringify(failoverAttempts.map(a => a.duration));
2173
2173
  promptRun.TotalFailoverDuration = failoverAttempts.reduce((sum, a) => sum + a.duration, 0);
2174
2174
  // Update ModelID if we ended up using a different model
2175
- if (currentModel.ID !== promptRun.OriginalModelID) {
2175
+ if (!UUIDsEqual(currentModel.ID, promptRun.OriginalModelID)) {
2176
2176
  promptRun.ModelID = currentModel.ID;
2177
2177
  }
2178
- if (currentVendorId && currentVendorId !== promptRun.VendorID) {
2178
+ if (currentVendorId && !UUIDsEqual(currentVendorId, promptRun.VendorID)) {
2179
2179
  promptRun.VendorID = currentVendorId;
2180
2180
  }
2181
2181
  }
@@ -2254,7 +2254,7 @@ export class AIPromptRunner {
2254
2254
  supportsEffortLevel = model.SupportsEffortLevel ?? false;
2255
2255
  if (vendorId) {
2256
2256
  // Find the AIModelVendor record for this specific vendor - must be an inference provider
2257
- const modelVendor = AIEngine.Instance.ModelVendors.find((mv) => mv.ModelID === model.ID && mv.VendorID === vendorId && mv.Status === 'Active' && this.isInferenceProvider(mv));
2257
+ const modelVendor = AIEngine.Instance.ModelVendors.find((mv) => UUIDsEqual(mv.ModelID, model.ID) && UUIDsEqual(mv.VendorID, vendorId) && mv.Status === 'Active' && this.isInferenceProvider(mv));
2258
2258
  if (modelVendor) {
2259
2259
  driverClass = modelVendor.DriverClass || driverClass;
2260
2260
  apiName = modelVendor.APIName || apiName;
@@ -2635,7 +2635,7 @@ export class AIPromptRunner {
2635
2635
  const filteredCandidates = allCandidates.filter(c => (c.vendorId || 'default') !== failedVendorId);
2636
2636
  const removedCount = beforeCount - filteredCandidates.length;
2637
2637
  if (removedCount > 0) {
2638
- const vendorName = AIEngine.Instance.Vendors.find(v => v.ID === failedVendorId)?.Name || failedVendorId;
2638
+ const vendorName = AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, failedVendorId))?.Name || failedVendorId;
2639
2639
  const remainingCount = filteredCandidates.length;
2640
2640
  // Log appropriate message based on error type
2641
2641
  let reason;
@@ -2666,8 +2666,8 @@ export class AIPromptRunner {
2666
2666
  return false; // Not a rate limit error
2667
2667
  }
2668
2668
  // Count how many times we've retried this specific model/vendor for rate limits
2669
- const rateLimitRetryCount = failoverAttempts.filter(a => a.modelId === currentModel.ID &&
2670
- a.vendorId === currentVendorId &&
2669
+ const rateLimitRetryCount = failoverAttempts.filter(a => UUIDsEqual(a.modelId, currentModel.ID) &&
2670
+ UUIDsEqual(a.vendorId, currentVendorId) &&
2671
2671
  a.errorType === 'RateLimit').length;
2672
2672
  // Use MaxRetries from prompt configuration, default to 3 if not set
2673
2673
  const maxRetries = prompt.MaxRetries ?? 3;
@@ -2676,7 +2676,7 @@ export class AIPromptRunner {
2676
2676
  if (shouldRetry) {
2677
2677
  const modelName = currentModel.Name;
2678
2678
  const vendorName = currentVendorId
2679
- ? AIEngine.Instance.Vendors.find(v => v.ID === currentVendorId)?.Name || 'default'
2679
+ ? AIEngine.Instance.Vendors.find(v => UUIDsEqual(v.ID, currentVendorId))?.Name || 'default'
2680
2680
  : 'default';
2681
2681
  this.logStatus(` ⏳ Rate limit hit - retrying ${modelName} (${vendorName}) with backoff (attempt ${rateLimitRetryCount}/${maxRetries})`, true);
2682
2682
  this.logFailoverAttempt(prompt.ID, failoverAttempt, true);
@@ -3662,27 +3662,27 @@ export class AIPromptRunner {
3662
3662
  switch (strategy) {
3663
3663
  case 'SameModelDifferentVendor':
3664
3664
  // Only consider same model with different vendors
3665
- candidates = availableCandidates.filter(c => c.model.ID === currentModel.ID && c.vendorId !== currentVendorId);
3665
+ candidates = availableCandidates.filter(c => UUIDsEqual(c.model.ID, currentModel.ID) && !UUIDsEqual(c.vendorId, currentVendorId));
3666
3666
  break;
3667
3667
  case 'NextBestModel':
3668
3668
  // Consider all models, apply model strategy preference
3669
3669
  candidates = availableCandidates;
3670
3670
  if (modelStrategy === 'RequireSameModel') {
3671
- candidates = candidates.filter(c => c.model.ID === currentModel.ID);
3671
+ candidates = candidates.filter(c => UUIDsEqual(c.model.ID, currentModel.ID));
3672
3672
  }
3673
3673
  else if (modelStrategy === 'PreferSameModel') {
3674
3674
  // Sort to put same model first
3675
3675
  candidates.sort((a, b) => {
3676
- const aSameModel = a.model.ID === currentModel.ID ? 1 : 0;
3677
- const bSameModel = b.model.ID === currentModel.ID ? 1 : 0;
3676
+ const aSameModel = UUIDsEqual(a.model.ID, currentModel.ID) ? 1 : 0;
3677
+ const bSameModel = UUIDsEqual(b.model.ID, currentModel.ID) ? 1 : 0;
3678
3678
  return bSameModel - aSameModel;
3679
3679
  });
3680
3680
  }
3681
3681
  else if (modelStrategy === 'PreferDifferentModel') {
3682
3682
  // Sort to put different models first
3683
3683
  candidates.sort((a, b) => {
3684
- const aDiffModel = a.model.ID !== currentModel.ID ? 1 : 0;
3685
- const bDiffModel = b.model.ID !== currentModel.ID ? 1 : 0;
3684
+ const aDiffModel = !UUIDsEqual(a.model.ID, currentModel.ID) ? 1 : 0;
3685
+ const bDiffModel = !UUIDsEqual(b.model.ID, currentModel.ID) ? 1 : 0;
3686
3686
  return bDiffModel - aDiffModel;
3687
3687
  });
3688
3688
  }