FoundryLocalModelProvider.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.ClientModel; 6 using LanguageModelProvider.FoundryLocal; 7 using ManagedCommon; 8 using Microsoft.Extensions.AI; 9 using OpenAI; 10 11 namespace LanguageModelProvider; 12 13 public sealed class FoundryLocalModelProvider : ILanguageModelProvider 14 { 15 private FoundryClient? _foundryClient; 16 private IEnumerable<FoundryCatalogModel>? _catalogModels; 17 private string? _serviceUrl; 18 19 public static FoundryLocalModelProvider Instance { get; } = new(); 20 21 public string Name => "FoundryLocal"; 22 23 public string ProviderDescription => "The model will run locally via Foundry Local"; 24 25 public IChatClient? GetIChatClient(string modelId) 26 { 27 Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}"); 28 InitializeAsync().GetAwaiter().GetResult(); 29 30 if (string.IsNullOrWhiteSpace(modelId)) 31 { 32 Logger.LogError("[FoundryLocal] Model ID is empty after extraction"); 33 return null; 34 } 35 36 // Check if model is in catalog 37 var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false; 38 if (!isInCatalog) 39 { 40 var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings."; 41 Logger.LogError($"[FoundryLocal] {errorMessage}"); 42 throw new InvalidOperationException(errorMessage); 43 } 44 45 // Ensure the model is loaded before returning chat client 46 var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult(); 47 if (!isLoaded) 48 { 49 Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}"); 50 throw new InvalidOperationException($"Failed to load the model '{modelId}'."); 51 } 52 53 // Use ServiceUri instead of Endpoint since Endpoint already includes /v1 54 var baseUri = _foundryClient.GetServiceUri(); 55 if (baseUri == null) 56 { 57 const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running."; 58 Logger.LogError($"[FoundryLocal] {message}"); 59 throw new InvalidOperationException(message); 60 } 61 62 var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1"); 63 Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}"); 64 65 return new OpenAIClient( 66 new ApiKeyCredential("none"), 67 new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) }) 68 .GetChatClient(modelId) 69 .AsIChatClient(); 70 } 71 72 public string GetIChatClientString(string url) 73 { 74 try 75 { 76 InitializeAsync().GetAwaiter().GetResult(); 77 } 78 catch 79 { 80 return string.Empty; 81 } 82 83 var modelId = url.Split('/').LastOrDefault(); 84 85 if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId)) 86 { 87 return string.Empty; 88 } 89 90 return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()"; 91 } 92 93 public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default) 94 { 95 await InitializeAsync(cancelationToken); 96 97 if (_foundryClient == null) 98 { 99 return Array.Empty<ModelDetails>(); 100 } 101 102 var cachedModels = await _foundryClient.ListCachedModels(); 103 List<ModelDetails> downloadedModels = []; 104 105 foreach (var model in cachedModels) 106 { 107 Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}"); 108 downloadedModels.Add(new ModelDetails 109 { 110 Id = $"fl-{model.Name}", 111 Name = model.Name, 112 Url = $"fl://{model.Name}", 113 Description = $"{model.Name} running locally with Foundry Local", 114 HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL], 115 ProviderModelDetails = model, 116 }); 117 } 118 119 return downloadedModels; 120 } 121 122 private async Task InitializeAsync(CancellationToken cancelationToken = default) 123 { 124 if (_foundryClient != null && _catalogModels != null && _catalogModels.Any()) 125 { 126 await _foundryClient.EnsureRunning().ConfigureAwait(false); 127 return; 128 } 129 130 Logger.LogInfo("[FoundryLocal] Initializing provider"); 131 _foundryClient ??= await FoundryClient.CreateAsync(); 132 133 if (_foundryClient == null) 134 { 135 const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running."; 136 Logger.LogError($"[FoundryLocal] {message}"); 137 throw new InvalidOperationException(message); 138 } 139 140 _serviceUrl ??= await _foundryClient.GetServiceUrl(); 141 Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}"); 142 143 var catalogModels = await _foundryClient.ListCatalogModels(); 144 Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models"); 145 _catalogModels = catalogModels; 146 } 147 148 public async Task<bool> IsAvailable() 149 { 150 Logger.LogInfo("[FoundryLocal] Checking availability"); 151 await InitializeAsync(); 152 var available = _foundryClient != null; 153 Logger.LogInfo($"[FoundryLocal] Available: {available}"); 154 return available; 155 } 156 }