/ src / common / LanguageModelProvider / FoundryLocalModelProvider.cs
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  }