From ea94bcdd6eba104e76973a7d20abf2a7f251ac2d Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Sun, 16 Nov 2025 15:26:05 +0800
Subject: [PATCH] Advanced paste: Add more error handle for foundry local
(#43600)
## Summary of the Pull Request
Foundry local sdk will not run models that is not in catalog, when
catalog removes some, the old ones will fail executing,
so add error hint for users to re-configure the models in settings.
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
## Detailed Description of the Pull Request / Additional comments
## Validation Steps Performed
---
.../FoundryLocal/FoundryClient.cs | 48 ++++---------
.../FoundryLocalModelProvider.cs | 71 ++++++++++---------
.../FoundryLocalPasteProvider.cs | 22 +++---
.../Strings/en-us/Resources.resw | 4 ++
.../ModelPicker/FoundryLocalModelPicker.xaml | 4 +-
.../Settings.UI/Strings/en-us/Resources.resw | 4 +-
6 files changed, 72 insertions(+), 81 deletions(-)
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