@@ -6,7 +6,6 @@ import ConversationEngine
66import ConfigurationSystem
77import MCPFramework
88import Logging
9- import Training
109extension Notification . Name {
1110 /// Posted when providers/endpoints are reloaded and available models may have changed.
1211 public static let endpointManagerDidReloadProviders = Notification . Name ( " com.sam.endpointmanager.providersReloaded " )
@@ -90,46 +89,6 @@ public class EndpointManager: ObservableObject {
9089 self . reloadProviderConfigurations ( )
9190 }
9291 }
93-
94- /// Listen for LoRA adapter changes
95- NotificationCenter . default. addObserver (
96- forName: . loraAdaptersDidChange,
97- object: nil ,
98- queue: . main
99- ) { [ weak self] notification in
100- // Extract values outside Task to avoid concurrency issues
101- var adapterId : String ?
102- var adapterName : String ?
103- var baseModelId : String ?
104-
105- if let adapter = notification. object as? LoRAAdapter {
106- adapterId = adapter. id
107- adapterName = adapter. metadata. adapterName
108- baseModelId = adapter. baseModelId
109- }
110-
111- Task { @MainActor in
112- guard let self = self , let localModelManager = self . localModelManager else { return }
113-
114- /// Register adapter with LocalModelManager
115- /// CRITICAL: Use adapter ID as modelName to match ModelListManager format (lora/{uuid})
116- if let aid = adapterId, let aname = adapterName {
117- let adapterDir = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
118- . appendingPathComponent ( " SAM/adapters/ \( aid) " )
119- localModelManager. registerModel (
120- provider: " lora " ,
121- modelName: aid, // Use UUID, not adapter name
122- path: adapterDir. path,
123- sizeBytes: nil ,
124- quantization: " lora "
125- )
126- self . logger. info ( " Registered LoRA adapter: \( aname) (ID: \( aid) ) " )
127- }
128-
129- /// Trigger provider reload to create/remove providers
130- self . reloadProviderConfigurations ( )
131- }
132- }
13392 }
13493
13594 // MARK: - Public Interface
@@ -343,11 +302,6 @@ public class EndpointManager: ObservableObject {
343302
344303 /// Check if a model is a local model (MLX or GGUF) Returns true for local models, false for API-based models.
345304 public func isLocalModel( _ modelId: String ) -> Bool {
346- /// LoRA adapters are always local
347- if modelId. hasPrefix ( " lora/ " ) {
348- return true
349- }
350-
351305 guard let providerType = getProviderTypeForModel ( modelId) else {
352306 return false
353307 }
@@ -1205,151 +1159,6 @@ public class EndpointManager: ObservableObject {
12051159 providers [ providerIdentifier] = provider
12061160 logger. debug ( " Hot reload: Created MLX model provider: \( providerIdentifier) " )
12071161 }
1208-
1209- /// Also handle LoRA adapters (quantization == "lora")
1210- let loraAdapters = registryModels. filter { $0. quantization == " lora " }
1211- logger. debug ( " Hot reload: Found \( loraAdapters. count) LoRA adapters in registry " )
1212-
1213- /// Also scan adapters directory for any unregistered adapters
1214- let adaptersDir = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
1215- . appendingPathComponent ( " SAM/adapters " )
1216- if FileManager . default. fileExists ( atPath: adaptersDir. path) {
1217- let adapterDirs = ( try ? FileManager . default. contentsOfDirectory ( at: adaptersDir, includingPropertiesForKeys: nil ) ) ?? [ ]
1218- for adapterDir in adapterDirs where adapterDir. hasDirectoryPath {
1219- let adapterId = adapterDir. lastPathComponent
1220- /// Check if already registered
1221- if loraAdapters. contains ( where: { $0. path == adapterDir. path } ) {
1222- continue // Already in registry
1223- }
1224-
1225- /// Load metadata to get adapter name and base model
1226- let metadataPath = adapterDir. appendingPathComponent ( " metadata.json " )
1227- guard let metadataData = try ? Data ( contentsOf: metadataPath) ,
1228- let metadataDict = try ? JSONSerialization . jsonObject ( with: metadataData) as? [ String : Any ] ,
1229- let adapterName = metadataDict [ " adapterName " ] as? String ,
1230- let baseModelId = metadataDict [ " baseModelId " ] as? String else {
1231- logger. warning ( " Hot reload: Failed to load metadata for unregistered adapter: \( adapterId) " )
1232- continue
1233- }
1234-
1235- /// Register with LocalModelManager
1236- /// CRITICAL: Use adapter ID (UUID) as modelName to match ModelListManager format
1237- modelManager. registerModel (
1238- provider: " lora " ,
1239- modelName: adapterId, // Use UUID, not adapter name
1240- path: adapterDir. path,
1241- sizeBytes: nil ,
1242- quantization: " lora "
1243- )
1244- logger. info ( " Hot reload: Registered previously unregistered LoRA adapter: \( adapterName) (ID: \( adapterId) ) " )
1245- }
1246- }
1247-
1248- /// Re-query adapters after potential registration
1249- let allLoraAdapters = modelManager. getAllRegistryModels ( ) . filter { $0. quantization == " lora " }
1250- logger. debug ( " Hot reload: Total LoRA adapters after scan: \( allLoraAdapters. count) " )
1251-
1252- for adapter in allLoraAdapters {
1253- let providerIdentifier = adapter. identifier
1254-
1255- /// Skip if provider already exists (prevents duplicates during hot reload)
1256- if providers [ providerIdentifier] != nil {
1257- logger. debug ( " Hot reload: Skipping LoRA adapter \( providerIdentifier) (provider already exists) " )
1258- continue
1259- }
1260-
1261- /// Load adapter metadata to get base model ID
1262- let adapterPath = URL ( fileURLWithPath: adapter. path)
1263- let metadataPath = adapterPath. appendingPathComponent ( " metadata.json " )
1264- guard let metadataData = try ? Data ( contentsOf: metadataPath) ,
1265- let metadataDict = try ? JSONSerialization . jsonObject ( with: metadataData) as? [ String : Any ] ,
1266- let baseModelId = metadataDict [ " baseModelId " ] as? String else {
1267- logger. warning ( " Failed to load metadata for LoRA adapter: \( adapter. modelName) " )
1268- continue
1269- }
1270-
1271- /// Get base model path - look for it in the providers we just created
1272- /// LoRA adapters are created AFTER MLX models, so base model provider should already exist
1273- var baseModelPath : String ?
1274-
1275- /// Check all registry entries for matching model
1276- for entry in modelManager. getAllRegistryModels ( ) {
1277- /// Match by identifier or by model name
1278- if entry. identifier == baseModelId || entry. modelName == baseModelId {
1279- baseModelPath = entry. path
1280- break
1281- }
1282- }
1283-
1284- guard let baseModel = baseModelPath else {
1285- logger. warning ( " Base model not found for LoRA adapter: \( adapter. modelName) (base: \( baseModelId) ) " )
1286- logger. debug ( " Available models in registry: \( modelManager. getAllRegistryModels ( ) . map { $0. identifier } . joined ( separator: " , " ) ) " )
1287- continue
1288- }
1289-
1290- /// For MLX models, ensure path points to directory, not to model.safetensors file
1291- /// The registry might store "/path/to/model/model.safetensors", but MLXProvider needs "/path/to/model"
1292- let modelDirectory : String
1293- if baseModel. hasSuffix ( " /model.safetensors " ) || baseModel. hasSuffix ( " .safetensors " ) {
1294- /// Path points to file - get parent directory
1295- modelDirectory = URL ( fileURLWithPath: baseModel) . deletingLastPathComponent ( ) . path
1296- } else {
1297- /// Path already points to directory
1298- modelDirectory = baseModel
1299- }
1300-
1301- /// Validate that the model directory contains required files
1302- /// This prevents using empty/incomplete model directories as base models
1303- /// Support both single-file and split models
1304- let modelDirURL = URL ( fileURLWithPath: modelDirectory)
1305- let singleFile = modelDirURL. appendingPathComponent ( " model.safetensors " ) . path
1306- let indexFile = modelDirURL. appendingPathComponent ( " model.safetensors.index.json " ) . path
1307- let splitFile = modelDirURL. appendingPathComponent ( " model-00001-of-00002.safetensors " ) . path
1308-
1309- let isValidModel = FileManager . default. fileExists ( atPath: singleFile) ||
1310- FileManager . default. fileExists ( atPath: indexFile) ||
1311- FileManager . default. fileExists ( atPath: splitFile)
1312-
1313- guard isValidModel else {
1314- logger. warning ( " Base model directory is invalid/empty for LoRA adapter: \( adapter. modelName) " )
1315- logger. warning ( " Base model: \( baseModelId) " )
1316- logger. warning ( " Path: \( modelDirectory) " )
1317- logger. warning ( " Skipping adapter (base model incomplete or not downloaded) " )
1318- continue
1319- }
1320-
1321- /// Get adapter ID from path (last component)
1322- let adapterId = adapterPath. lastPathComponent
1323-
1324- /// Create provider-specific config
1325- let providerSpecificConfig = ProviderConfiguration (
1326- providerId: providerIdentifier,
1327- providerType: . localMLX,
1328- isEnabled: config. isEnabled,
1329- baseURL: nil ,
1330- models: [ providerIdentifier] ,
1331- maxTokens: config. maxTokens,
1332- temperature: config. temperature,
1333- customHeaders: config. customHeaders,
1334- timeoutSeconds: config. timeoutSeconds,
1335- retryCount: config. retryCount
1336- )
1337-
1338- /// Create MLX provider with LoRA adapter
1339- let provider = MLXProvider (
1340- config: providerSpecificConfig,
1341- modelPath: modelDirectory,
1342- loraAdapterId: adapterId,
1343- onModelLoadingStarted: { @MainActor [ weak self] providerId, modelName in
1344- self ? . notifyModelLoadingStarted ( providerId: providerId, modelName: modelName)
1345- } ,
1346- onModelLoadingCompleted: { @MainActor [ weak self] providerId in
1347- self ? . notifyModelLoadingCompleted ( providerId: providerId)
1348- }
1349- )
1350- providers [ providerIdentifier] = provider
1351- logger. debug ( " Hot reload: Created LoRA adapter provider: \( providerIdentifier) (base: \( baseModelId) , path: \( modelDirectory) ) " )
1352- }
13531162 }
13541163 } else {
13551164 providers [ providerId] = createProvider ( type: providerType, config: config)
0 commit comments