diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs index c2b0f4ab36..287588a71e 100644 --- a/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs +++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs @@ -169,41 +169,23 @@ internal sealed class FoundryClient public async Task EnsureModelLoaded(string modelId) { - try + Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}"); + + // Check if already loaded + if (await IsModelLoaded(modelId).ConfigureAwait(false)) { - Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}"); - - // Check if already loaded - if (await IsModelLoaded(modelId).ConfigureAwait(false)) - { - Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}"); - return true; - } - - // Check if model exists in cache - var cachedModels = await ListCachedModels().ConfigureAwait(false); - Logger.LogInfo($"[FoundryClient] Cached models: {string.Join(", ", cachedModels.Select(m => m.Name))}"); - - if (!cachedModels.Any(m => m.Name == modelId)) - { - Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}"); - return false; - } - - // Load the model - Logger.LogInfo($"[FoundryClient] Loading model: {modelId}"); - await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false); - - // Verify it's loaded - var loaded = await IsModelLoaded(modelId).ConfigureAwait(false); - Logger.LogInfo($"[FoundryClient] Model load result: {loaded}"); - return loaded; - } - catch (Exception ex) - { - Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}"); - return false; + Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}"); + return true; } + + // Load the model + Logger.LogInfo($"[FoundryClient] Loading model: {modelId}"); + await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false); + + // Verify it's loaded + var loaded = await IsModelLoaded(modelId).ConfigureAwait(false); + Logger.LogInfo($"[FoundryClient] Model load result: {loaded}"); + return loaded; } public async Task EnsureRunning() diff --git a/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs index 3c9c618f7f..2447d818a2 100644 --- a/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs +++ b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs @@ -13,6 +13,7 @@ namespace LanguageModelProvider; public sealed class FoundryLocalModelProvider : ILanguageModelProvider { private IEnumerable? _downloadedModels; + private IEnumerable? _catalogModels; private FoundryClient? _foundryClient; private string? _serviceUrl; @@ -24,22 +25,8 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider public IChatClient? GetIChatClient(string modelId) { - try - { - Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}"); - InitializeAsync().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}"); - return null; - } - - if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryClient == null) - { - Logger.LogError("[FoundryLocal] Service URL or manager is null"); - return null; - } + Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}"); + InitializeAsync().GetAwaiter().GetResult(); if (string.IsNullOrWhiteSpace(modelId)) { @@ -47,35 +34,43 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider return null; } - // Ensure the model is loaded before returning chat client - try + // Check if model is in catalog + var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false; + if (!isInCatalog) { - var isLoaded = _foundryClient.EnsureModelLoaded(modelId).GetAwaiter().GetResult(); - if (!isLoaded) - { - Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}"); - return null; - } - - Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}"); + var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings."; + Logger.LogError($"[FoundryLocal] {errorMessage}"); + throw new InvalidOperationException(errorMessage); } - catch (Exception ex) + + // Check if model is cached + var isInCache = _downloadedModels?.Any(m => m.ProviderModelDetails is FoundryCachedModel cached && cached.Name == modelId) ?? false; + if (!isInCache) { - Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}"); - return null; + var errorMessage = $"The requested model '{modelId}' is not cached. Please download it using Foundry Local."; + Logger.LogError($"[FoundryLocal] {errorMessage}"); + throw new InvalidOperationException(errorMessage); + } + + // Ensure the model is loaded before returning chat client + var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult(); + if (!isLoaded) + { + Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}"); + throw new InvalidOperationException($"Failed to load the model '{modelId}'."); } // Use ServiceUri instead of Endpoint since Endpoint already includes /v1 var baseUri = _foundryClient.GetServiceUri(); if (baseUri == null) { - Logger.LogError("[FoundryLocal] Service URI is null"); - return null; + const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running."; + Logger.LogError($"[FoundryLocal] {message}"); + throw new InvalidOperationException(message); } var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1"); Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}"); - Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}"); return new OpenAIClient( new ApiKeyCredential("none"), @@ -122,12 +117,13 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider private void Reset() { _downloadedModels = null; + _catalogModels = null; _ = InitializeAsync(); } private async Task InitializeAsync(CancellationToken cancelationToken = default) { - if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any()) + if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any() && _catalogModels != null && _catalogModels.Any()) { await _foundryClient.EnsureRunning().ConfigureAwait(false); return; @@ -138,13 +134,18 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider if (_foundryClient == null) { - Logger.LogError("[FoundryLocal] Failed to create Foundry client"); - return; + const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running."; + Logger.LogError($"[FoundryLocal] {message}"); + throw new InvalidOperationException(message); } _serviceUrl ??= await _foundryClient.GetServiceUrl(); Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}"); + var catalogModels = await _foundryClient.ListCatalogModels(); + Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models"); + _catalogModels = catalogModels; + var cachedModels = await _foundryClient.ListCachedModels(); Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models"); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs index a24032ff31..43481eddae 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using AdvancedPaste.Helpers; using AdvancedPaste.Models; using LanguageModelProvider; using Microsoft.Extensions.AI; @@ -33,10 +34,6 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider _config = config; } - public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey(); - - public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model; - public async Task IsAvailableAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -76,13 +73,20 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider } cancellationToken.ThrowIfCancellationRequested(); - var chatClient = _modelProvider.GetIChatClient(modelReference); - if (chatClient is null) + + IChatClient chatClient; + try { + chatClient = _modelProvider.GetIChatClient(modelReference); + } + catch (InvalidOperationException ex) + { + // GetIChatClient throws InvalidOperationException for user-facing errors + var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference); throw new PasteActionException( - $"Unable to load Foundry Local model: {modelReference}", - new InvalidOperationException("Chat client resolution failed"), - aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings."); + errorMessage, + ex, + aiServiceMessage: ex.Message); } var userMessageContent = $""" diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw index 521c1d60ba..ad62665f30 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -368,4 +368,8 @@ Local Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally + + Unable to load Foundry Local model: {0} + {0} is the model identifier. Do not translate {0}. + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml index 292113dd42..765cd88ff0 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml @@ -106,7 +106,7 @@ @@ -152,7 +152,7 @@ Spacing="8"> Foundry Local model Do not localize "Foundry Local", it's a product name - + Use the Foundry Local CLI to download models that run locally on-device. They'll appear here. Do not localize "Foundry Local", it's a product name Refresh model list - + Foundry Local is not available on this device yet. Do not localize "Foundry Local", it's a product name