Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
// Download and register all execution providers.
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Get the model catalog
Expand Down Expand Up @@ -48,6 +45,7 @@ await model.DownloadAsync(progress =>

// Get a chat client
var audioClient = await model.GetAudioClientAsync();
audioClient.Settings.Language = "en";


// Get a transcription with streaming outputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
// Download and register all execution providers.
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Get the model catalog
Expand Down
12 changes: 10 additions & 2 deletions samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// Discover available execution providers and their registration status.
var eps = mgr.DiscoverEps();
Console.WriteLine("Available execution providers:");
foreach (var ep in eps)
{
Console.WriteLine($" {ep.Name} (registered: {ep.IsRegistered})");
}

// Download and register all execution providers.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Get the model catalog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
// Download and register all execution providers.
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Model catalog operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
// Download and register all execution providers.
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Get the model catalog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
var mgr = FoundryLocalManager.Instance;


// Ensure that any Execution Provider (EP) downloads run and are completed.
// EP packages include dependencies and may be large.
// Download is only required again if a new version of the EP is released.
// For cross platform builds there is no dynamic EP download and this will return immediately.
await Utils.RunWithSpinner("Registering execution providers", mgr.EnsureEpsDownloadedAsync());
// Download and register all execution providers.
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());


// Get the model catalog
Expand Down
22 changes: 17 additions & 5 deletions sdk/cs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,33 @@ dotnet build src/Microsoft.AI.Foundry.Local.csproj /p:UseWinML=true

### Triggering EP download

EP download can be time-consuming. Call `EnsureEpsDownloadedAsync` early (after initialization) to separate the download step from catalog access:
EP management is explicit via two methods:

- **`DiscoverEps()`** — returns an array of `EpInfo` describing each available EP and whether it is already registered.
- **`DownloadAndRegisterEpsAsync(names?, ct?)`** — downloads and registers the specified EPs (or all available EPs if no names are given). This is a blocking call that returns an `EpDownloadResult`.

```csharp
// Initialize the manager first (see Quick Start)
await FoundryLocalManager.CreateAsync(
new Configuration { AppName = "my-app" },
NullLogger.Instance);

await FoundryLocalManager.Instance.EnsureEpsDownloadedAsync();
var mgr = FoundryLocalManager.Instance;

// Now catalog access won't trigger an EP download
var catalog = await FoundryLocalManager.Instance.GetCatalogAsync();
// Discover what EPs are available
var eps = mgr.DiscoverEps();
foreach (var ep in eps)
Console.WriteLine($"{ep.Name} — registered: {ep.IsRegistered}");

// Download and register all EPs
var result = await mgr.DownloadAndRegisterEpsAsync();
Console.WriteLine($"Success: {result.Success}, Status: {result.Status}");

// Or download only specific EPs
var result2 = await mgr.DownloadAndRegisterEpsAsync(new[] { eps[0].Name });
```

If you skip this step, EPs are downloaded automatically the first time you access the catalog. Once cached, subsequent calls are fast.
Catalog access no longer blocks on EP downloads. Call `DownloadAndRegisterEpsAsync` explicitly when you need hardware-accelerated execution providers.

## Quick Start

Expand Down
34 changes: 23 additions & 11 deletions sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ The model catalog.
**Remarks:**

The catalog is populated on first use.
If you are using a WinML build this will trigger a one-off execution provider download if not already done.
It is recommended to call [FoundryLocalManager.EnsureEpsDownloadedAsync(Nullable<CancellationToken>)](./microsoft.ai.foundry.local.foundrylocalmanager.md#ensureepsdownloadedasyncnullablecancellationtoken) first to separate out the two steps.
Catalog access no longer blocks on execution provider downloads.
Call [FoundryLocalManager.DownloadAndRegisterEpsAsync](./microsoft.ai.foundry.local.foundrylocalmanager.md#downloadandregisterepsasync) to explicitly download and register execution providers when hardware acceleration is needed.

### **StartWebServiceAsync(Nullable<CancellationToken>)**

Expand Down Expand Up @@ -141,27 +141,39 @@ Optional cancellation token.
[Task](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task)<br>
Task stopping the web service.

### **EnsureEpsDownloadedAsync(Nullable&lt;CancellationToken&gt;)**
### **DiscoverEps()**

Ensure execution providers are downloaded and registered.
Only relevant when using WinML.

Execution provider download can be time consuming due to the size of the packages.
Once downloaded, EPs are not re-downloaded unless a new version is available, so this method will be fast
on subsequent calls.
Discover available execution providers and their registration status.

```csharp
public Task EnsureEpsDownloadedAsync(Nullable<CancellationToken> ct)
public EpInfo[] DiscoverEps()
```

#### Returns

`EpInfo[]`<br>
An array of `EpInfo` objects, each with `Name` and `IsRegistered` properties.

### **DownloadAndRegisterEpsAsync(IEnumerable&lt;string&gt;?, Nullable&lt;CancellationToken&gt;)**

Download and register execution providers. This is a blocking call.

```csharp
public Task<EpDownloadResult> DownloadAndRegisterEpsAsync(IEnumerable<string>? names = null, CancellationToken? ct = null)
```

#### Parameters

`names` [IEnumerable&lt;string&gt;?](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1)<br>
Optional list of EP names to download. If null or empty, all available EPs are downloaded.

`ct` [Nullable&lt;CancellationToken&gt;](https://docs.microsoft.com/en-us/dotnet/api/system.nullable-1)<br>
Optional cancellation token.

#### Returns

[Task](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task)<br>
[Task&lt;EpDownloadResult&gt;](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)<br>
An `EpDownloadResult` with `Success`, `Status`, `RegisteredEps`, and `FailedEps` properties.

### **Dispose()**

Expand Down
11 changes: 0 additions & 11 deletions sdk/cs/src/Catalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ internal sealed class Catalog : ICatalog, IDisposable
{
private readonly Dictionary<string, Model> _modelAliasToModel = new();
private readonly Dictionary<string, ModelVariant> _modelIdToModelVariant = new();
private DateTime _lastFetch;

private readonly IModelLoadManager _modelLoadManager;
private readonly ICoreInterop _coreInterop;
Expand All @@ -32,8 +31,6 @@ private Catalog(IModelLoadManager modelLoadManager, ICoreInterop coreInterop, IL
_coreInterop = coreInterop;
_logger = logger;

_lastFetch = DateTime.MinValue;

CoreInteropRequest? input = null;
var response = coreInterop.ExecuteCommand("get_catalog_name", input);
if (response.Error != null)
Expand Down Expand Up @@ -145,12 +142,6 @@ private async Task<List<ModelVariant>> GetLoadedModelsImplAsync(CancellationToke

private async Task UpdateModels(CancellationToken? ct)
{
// TODO: make this configurable
if (DateTime.Now - _lastFetch < TimeSpan.FromHours(6))
{
return;
}

CoreInteropRequest? input = null;
var result = await _coreInterop.ExecuteCommandAsync("get_model_list", input, ct).ConfigureAwait(false);

Expand Down Expand Up @@ -189,8 +180,6 @@ private async Task UpdateModels(CancellationToken? ct)

_modelIdToModelVariant[variant.Id] = variant;
}

_lastFetch = DateTime.Now;
}

public void Dispose()
Expand Down
2 changes: 2 additions & 0 deletions sdk/cs/src/Detail/JsonSerializationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ namespace Microsoft.AI.Foundry.Local.Detail;
[JsonSerializable(typeof(AudioCreateTranscriptionRequest))]
[JsonSerializable(typeof(AudioCreateTranscriptionResponse))]
[JsonSerializable(typeof(string[]))] // list loaded or cached models
[JsonSerializable(typeof(EpInfo[]))]
[JsonSerializable(typeof(EpDownloadResult))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(ResponseFormatExtended))]
[JsonSerializable(typeof(ToolChoice))]
Expand Down
45 changes: 45 additions & 0 deletions sdk/cs/src/EpInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Microsoft.AI.Foundry.Local;

using System.Text.Json.Serialization;

/// <summary>
/// Describes a discoverable execution provider bootstrapper.
/// </summary>
public record EpInfo
{
/// <summary>The identifier of the bootstrapper/execution provider (e.g. "CUDAExecutionProvider").</summary>
[JsonPropertyName("Name")]
public required string Name { get; init; }

/// <summary>True if this EP has already been successfully downloaded and registered.</summary>
[JsonPropertyName("IsRegistered")]
public required bool IsRegistered { get; init; }
}

/// <summary>
/// Result of an explicit EP download and registration operation.
/// </summary>
public record EpDownloadResult
{
/// <summary>True if all requested EPs were successfully downloaded and registered.</summary>
[JsonPropertyName("Success")]
public required bool Success { get; init; }

/// <summary>Human-readable status message.</summary>
[JsonPropertyName("Status")]
public required string Status { get; init; }

/// <summary>Names of EPs that were successfully registered.</summary>
[JsonPropertyName("RegisteredEps")]
public required string[] RegisteredEps { get; init; }

/// <summary>Names of EPs that failed to register.</summary>
[JsonPropertyName("FailedEps")]
public required string[] FailedEps { get; init; }
}
72 changes: 55 additions & 17 deletions sdk/cs/src/FoundryLocalManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ public static async Task CreateAsync(Configuration configuration, ILogger logger
/// <param name="ct">Optional cancellation token.</param>
/// <returns>The model catalog.</returns>
/// <remarks>
/// The catalog is populated on first use.
/// If you are using a WinML build this will trigger a one-off execution provider download if not already done.
/// It is recommended to call <see cref="EnsureEpsDownloadedAsync"/> first to separate out the two steps.
/// The catalog is populated on first use and returns models based on currently available execution providers.
/// To ensure all hardware-accelerated models are listed, call <see cref="DownloadAndRegisterEpsAsync"/> first to
/// register execution providers, then access the catalog.
/// </remarks>
public async Task<ICatalog> GetCatalogAsync(CancellationToken? ct = null)
{
Expand Down Expand Up @@ -135,19 +135,42 @@ await Utils.CallWithExceptionHandling(() => StopWebServiceImplAsync(ct),
}

/// <summary>
/// Ensure execution providers are downloaded and registered.
/// Only relevant when using WinML.
///
/// Execution provider download can be time consuming due to the size of the packages.
/// Once downloaded, EPs are not re-downloaded unless a new version is available, so this method will be fast
/// on subsequent calls.
/// Discovers all available execution provider bootstrappers.
/// Returns metadata about each EP including whether it is already registered.
/// </summary>
/// <returns>Array of EP bootstrapper info describing available EPs.</returns>
public EpInfo[] DiscoverEps()
{
var result = _coreInterop!.ExecuteCommand("discover_eps");
if (result.Error != null)
{
throw new FoundryLocalException($"Error discovering execution providers: {result.Error}", _logger);
}

return JsonSerializer.Deserialize(result.Data!, JsonSerializationContext.Default.EpInfoArray)
?? Array.Empty<EpInfo>();
Comment on lines +150 to +151
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiscoverEps() can throw a raw JsonException if the core returns invalid JSON (or unexpected shape). Other public manager APIs wrap unexpected exceptions via Utils.CallWithExceptionHandling, but this method currently returns JsonSerializer.Deserialize(...) directly. Consider catching JsonException (and/or validating result.Data is non-null) and rethrowing a FoundryLocalException with context so callers get consistent error semantics.

Suggested change
return JsonSerializer.Deserialize(result.Data!, JsonSerializationContext.Default.EpInfoArray)
?? Array.Empty<EpInfo>();
if (string.IsNullOrWhiteSpace(result.Data))
{
throw new FoundryLocalException(
"Error discovering execution providers: core returned no data.",
_logger);
}
try
{
return JsonSerializer.Deserialize(result.Data, JsonSerializationContext.Default.EpInfoArray)
?? Array.Empty<EpInfo>();
}
catch (JsonException ex)
{
throw new FoundryLocalException(
"Error discovering execution providers: invalid JSON payload from core.",
ex,
_logger);
}

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// Downloads and registers execution providers. This is a blocking call that completes when all
/// requested EPs have been processed.
/// </summary>
/// <param name="names">
/// Optional subset of EP bootstrapper names to download (as returned by <see cref="DiscoverEps"/>).
/// If null or empty, all discoverable EPs are downloaded.
/// </param>
/// <param name="ct">Optional cancellation token.</param>
public async Task EnsureEpsDownloadedAsync(CancellationToken? ct = null)
/// <returns>Result describing which EPs succeeded and which failed.</returns>
/// <remarks>
/// Catalog and model requests use whatever EPs are currently registered and do not block on EP downloads.
/// After downloading new EPs, re-fetch the model catalog to include models requiring the newly registered EPs.
/// </remarks>
public async Task<EpDownloadResult> DownloadAndRegisterEpsAsync(IEnumerable<string>? names = null,
CancellationToken? ct = null)
{
await Utils.CallWithExceptionHandling(() => EnsureEpsDownloadedImplAsync(ct),
"Error ensuring execution providers downloaded.", _logger)
.ConfigureAwait(false);
return await Utils.CallWithExceptionHandling(() => DownloadAndRegisterEpsImplAsync(names, ct),
"Error downloading execution providers.", _logger)
.ConfigureAwait(false);
}

private FoundryLocalManager(Configuration configuration, ILogger logger)
Expand Down Expand Up @@ -259,17 +282,32 @@ private async Task StopWebServiceImplAsync(CancellationToken? ct = null)
Urls = null;
}

private async Task EnsureEpsDownloadedImplAsync(CancellationToken? ct = null)
private async Task<EpDownloadResult> DownloadAndRegisterEpsImplAsync(IEnumerable<string>? names = null,
CancellationToken? ct = null)
{

using var disposable = await asyncLock.LockAsync().ConfigureAwait(false);

CoreInteropRequest? input = null;
var result = await _coreInterop!.ExecuteCommandAsync("ensure_eps_downloaded", input, ct);
if (names != null)
{
var namesList = string.Join(",", names);
if (!string.IsNullOrEmpty(namesList))
{
input = new CoreInteropRequest
{
Params = new Dictionary<string, string> { { "Names", namesList } }
};
}
}

var result = await _coreInterop!.ExecuteCommandAsync("download_and_register_eps", input, ct).ConfigureAwait(false);
if (result.Error != null)
{
throw new FoundryLocalException($"Error ensuring execution providers downloaded: {result.Error}", _logger);
throw new FoundryLocalException($"Error downloading execution providers: {result.Error}", _logger);
}

return JsonSerializer.Deserialize(result.Data!, JsonSerializationContext.Default.EpDownloadResult)
?? throw new FoundryLocalException("Failed to deserialize EP download result.", _logger);
}

protected virtual void Dispose(bool disposing)
Expand Down
Loading
Loading