From d14e9d238eb29cc89cacd73edbb384a41ca1e221 Mon Sep 17 00:00:00 2001 From: Scott McKay Date: Fri, 14 Nov 2025 18:26:49 +1000 Subject: [PATCH 1/4] Various updates - use consistent app name so all downloaded models go to the same location - add some utilities - ensure EPs are downloaded to make it clear that that is what takes time on the first run - add more space in the code to separate sections - reorder some things to try and make the output a bit more intuitive if you haven't read the code yet - target net9* everywhere - the SDK targets net8 as a lower common denominator. app can target anything later than that - try and clarify the model/model variant options in HelloFoundryLocalSdk - keep other apps simple and alias based --- .../GettingStarted/Directory.Packages.props | 4 +- .../AudioTranscriptionExample.csproj | 3 +- .../FoundryLocalWebServer.csproj | 3 +- .../HelloFoundryLocalSdk.csproj | 3 +- samples/cs/GettingStarted/nuget.config | 2 +- .../src/AudioTranscriptionExample/Program.cs | 30 ++++---- .../src/FoundryLocalWebServer/Program.cs | 30 ++++---- .../src/HelloFoundryLocalSdk/Program.cs | 68 ++++++++++++++----- samples/cs/GettingStarted/src/Shared/Utils.cs | 49 +++++++++++++ .../AudioTranscriptionExample.csproj | 5 +- .../FoundryLocalWebServer.csproj | 5 +- .../HelloFoundryLocalSdk.csproj | 3 +- 12 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 samples/cs/GettingStarted/src/Shared/Utils.cs diff --git a/samples/cs/GettingStarted/Directory.Packages.props b/samples/cs/GettingStarted/Directory.Packages.props index 78d3d53..ec621b5 100644 --- a/samples/cs/GettingStarted/Directory.Packages.props +++ b/samples/cs/GettingStarted/Directory.Packages.props @@ -3,8 +3,8 @@ true - - + + diff --git a/samples/cs/GettingStarted/cross-platform/AudioTranscriptionExample/AudioTranscriptionExample.csproj b/samples/cs/GettingStarted/cross-platform/AudioTranscriptionExample/AudioTranscriptionExample.csproj index 78cb019..144a38d 100644 --- a/samples/cs/GettingStarted/cross-platform/AudioTranscriptionExample/AudioTranscriptionExample.csproj +++ b/samples/cs/GettingStarted/cross-platform/AudioTranscriptionExample/AudioTranscriptionExample.csproj @@ -9,7 +9,8 @@ - + + diff --git a/samples/cs/GettingStarted/cross-platform/FoundryLocalWebServer/FoundryLocalWebServer.csproj b/samples/cs/GettingStarted/cross-platform/FoundryLocalWebServer/FoundryLocalWebServer.csproj index 9312382..4a23db4 100644 --- a/samples/cs/GettingStarted/cross-platform/FoundryLocalWebServer/FoundryLocalWebServer.csproj +++ b/samples/cs/GettingStarted/cross-platform/FoundryLocalWebServer/FoundryLocalWebServer.csproj @@ -9,7 +9,8 @@ - + + diff --git a/samples/cs/GettingStarted/cross-platform/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj b/samples/cs/GettingStarted/cross-platform/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj index 20dcc7c..239e5b4 100644 --- a/samples/cs/GettingStarted/cross-platform/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj +++ b/samples/cs/GettingStarted/cross-platform/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj @@ -9,7 +9,8 @@ - + + diff --git a/samples/cs/GettingStarted/nuget.config b/samples/cs/GettingStarted/nuget.config index 8a62f07..eaea6c3 100644 --- a/samples/cs/GettingStarted/nuget.config +++ b/samples/cs/GettingStarted/nuget.config @@ -2,7 +2,7 @@ - + diff --git a/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs b/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs index 15c5c44..51b1a5d 100644 --- a/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs +++ b/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs @@ -1,28 +1,32 @@ using Microsoft.AI.Foundry.Local; -using Microsoft.Extensions.Logging; var config = new Configuration { - AppName = "my-audio-app", - LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Debug + AppName = "foundry_local_samples", + LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information }; -using var loggerFactory = LoggerFactory.Create(builder => -{ - builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); -}); -var logger = loggerFactory.CreateLogger(); // Initialize the singleton instance. -await FoundryLocalManager.CreateAsync(config, logger); +await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger()); 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()); + + // Get the model catalog var catalog = await mgr.GetCatalogAsync(); + // Get a model using an alias var model = await catalog.GetModelAsync("whisper-tiny") ?? throw new System.Exception("Model not found"); + // Download the model (the method skips download if already cached) await model.DownloadAsync(progress => { @@ -33,26 +37,28 @@ await model.DownloadAsync(progress => } }); + // Load the model Console.Write($"Loading model {model.Id}..."); await model.LoadAsync(); Console.WriteLine("done."); + // Get a chat client var audioClient = await model.GetAudioClientAsync(); -// get a cancellation token -CancellationToken ct = new CancellationToken(); // Get a transcription with streaming outputs Console.WriteLine("Transcribing audio with streaming output:"); -var response = audioClient.TranscribeAudioStreamingAsync("Recording.mp3", ct); +var response = audioClient.TranscribeAudioStreamingAsync("Recording.mp3", CancellationToken.None); await foreach (var chunk in response) { Console.Write(chunk.Text); Console.Out.Flush(); } + Console.WriteLine(); + // Tidy up - unload the model await model.UnloadAsync(); \ No newline at end of file diff --git a/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs b/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs index 7a75288..fa5ce51 100644 --- a/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs +++ b/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs @@ -1,35 +1,38 @@ using Microsoft.AI.Foundry.Local; -using Microsoft.Extensions.Logging; using OpenAI; using System.ClientModel; var config = new Configuration { - AppName = "my-app-name", - LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Debug, + AppName = "foundry_local_samples", + LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information, Web = new Configuration.WebService { Urls = "http://127.0.0.1:55588" } }; -using var loggerFactory = LoggerFactory.Create(builder => -{ - builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); -}); - -var logger = loggerFactory.CreateLogger(); // Initialize the singleton instance. -await FoundryLocalManager.CreateAsync(config, logger); +await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger()); 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()); + + // Get the model catalog var catalog = await mgr.GetCatalogAsync(); + // Get a model using an alias -//var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found"); -var model = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:3") ?? throw new Exception("Model not found"); +var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found") + ?? throw new Exception("Model not found"); + // Download the model (the method skips download if already cached) await model.DownloadAsync(progress => @@ -41,11 +44,13 @@ await model.DownloadAsync(progress => } }); + // Load the model Console.Write($"Loading model {model.Id}..."); await model.LoadAsync(); Console.WriteLine("done."); + // Start the web service Console.Write($"Starting web service on {config.Web.Urls}..."); await mgr.StartWebServiceAsync(); @@ -61,7 +66,6 @@ await model.DownloadAsync(progress => }); var chatClient = client.GetChatClient(model.Id); - var completionUpdates = chatClient.CompleteChatStreaming("Why is the sky blue?"); Console.Write($"[ASSISTANT]: "); diff --git a/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs b/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs index 289f588..0764f63 100644 --- a/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs +++ b/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs @@ -1,28 +1,32 @@ using Microsoft.AI.Foundry.Local; using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Microsoft.Extensions.Logging; +using System.Diagnostics; CancellationToken ct = new CancellationToken(); var config = new Configuration { - AppName = "my-app-name", - LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Debug + AppName = "foundry_local_samples", + LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information }; -using var loggerFactory = LoggerFactory.Create(builder => -{ - builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); -}); -var logger = loggerFactory.CreateLogger(); // Initialize the singleton instance. -await FoundryLocalManager.CreateAsync(config, logger); +await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger()); 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()); + + // Get the model catalog var catalog = await mgr.GetCatalogAsync(); + // List available models Console.WriteLine("Available models for your hardware:"); var models = await catalog.ListModelsAsync(); @@ -34,21 +38,53 @@ } } -// Get a model using an alias -//var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found"); -var model = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:3") ?? throw new Exception("Model not found"); - -// is model cached -Console.WriteLine($"Is model cached: {await model.IsCachedAsync()}"); // print out cached models var cachedModels = await catalog.GetCachedModelsAsync(); -Console.WriteLine("Cached models:"); +Console.WriteLine("\nCached models:"); foreach (var cachedModel in cachedModels) { Console.WriteLine($"- {cachedModel.Alias} ({cachedModel.Id})"); } + +// Get a model using an alias. +var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found"); + +// `model.SelectedVariant` indicates which variant will be used by default. +// +// Models in Model.Variants are ordered by priority, with the highest priority first. +// The first downloaded model is selected by default. +// The highest priority is selected if no models have been downloaded. +Console.WriteLine("\nThe default selected model variant is: " + model.Id); +if (model.SelectedVariant != model.Variants.First()) +{ + Debug.Assert(await model.SelectedVariant.IsCachedAsync()); + Console.WriteLine("The model variant was selected due to being locally cached."); +} + + +// OPTIONAL: `model` can be used directly and `model.SelectedVariant` will be used as the default. +// You can explicitly select or use a specific ModelVariant if you want more control +// over the device and/or execution provider used. +// +// Options: +// - Use a ModelVariant directly from the catalog if you know the variant Id +// - `var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-gpu:3")` +// +// - Get the ModelVariant from Model.Variants +// - `var modelVariant = model.Variants.First(v => v.Id == "qwen2.5-0.5b-instruct-generic-cpu:4")` +// - `var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.GPU)` +// - Update selected variant in `model`: `model.SelectVariant(modelVariant);` + +// For this example we explicitly select the CPU variant, and call SelectVariant so all the following example code +// uses the `model` instance. +// You can also use modelVariant for download/load/get client/unload. +Console.WriteLine("Selecting CPU variant of model"); +var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); +model.SelectVariant(modelVariant); + + // Download the model (the method skips download if already cached) await model.DownloadAsync(progress => { diff --git a/samples/cs/GettingStarted/src/Shared/Utils.cs b/samples/cs/GettingStarted/src/Shared/Utils.cs new file mode 100644 index 0000000..4f8037a --- /dev/null +++ b/samples/cs/GettingStarted/src/Shared/Utils.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using System.Text; + +internal static class Utils +{ + /// + /// Get a dummy application logger. + /// + /// ILogger + internal static ILogger GetAppLogger() + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); + }); + + return loggerFactory.CreateLogger("FoundryLocalSamples"); + } + + internal static async Task RunWithSpinner(string msg, T workTask) where T : Task + { + // Start the spinner + using var cts = new CancellationTokenSource(); + var spinnerTask = ShowSpinner(msg, cts.Token); + + await workTask; // wait for the real work to finish + cts.Cancel(); // stop the spinner + await spinnerTask; // wait for spinner to exit + } + + private static async Task ShowSpinner(string msg, CancellationToken token) + { + Console.OutputEncoding = Encoding.UTF8; + + var sequence = new[] { '◴','◷','◶','◵' }; + + int counter = 0; + + while (!token.IsCancellationRequested) + { + Console.Write($"{msg}\t{sequence[counter % sequence.Length]}"); + Console.SetCursorPosition(0, Console.CursorTop); + counter++; + await Task.Delay(200, token).ContinueWith(_ => { }); + } + + Console.WriteLine($"\nDone.\n"); + } +} \ No newline at end of file diff --git a/samples/cs/GettingStarted/windows/AudioTranscriptionExample/AudioTranscriptionExample.csproj b/samples/cs/GettingStarted/windows/AudioTranscriptionExample/AudioTranscriptionExample.csproj index 7165bd6..9278878 100644 --- a/samples/cs/GettingStarted/windows/AudioTranscriptionExample/AudioTranscriptionExample.csproj +++ b/samples/cs/GettingStarted/windows/AudioTranscriptionExample/AudioTranscriptionExample.csproj @@ -5,13 +5,14 @@ enable enable - net8.0-windows10.0.26100 + net9.0-windows10.0.26100 true ARM64;x64 - + + diff --git a/samples/cs/GettingStarted/windows/FoundryLocalWebServer/FoundryLocalWebServer.csproj b/samples/cs/GettingStarted/windows/FoundryLocalWebServer/FoundryLocalWebServer.csproj index f72b28d..584e393 100644 --- a/samples/cs/GettingStarted/windows/FoundryLocalWebServer/FoundryLocalWebServer.csproj +++ b/samples/cs/GettingStarted/windows/FoundryLocalWebServer/FoundryLocalWebServer.csproj @@ -5,13 +5,14 @@ enable enable - net8.0-windows10.0.26100 + net9.0-windows10.0.26100 true x64;ARM64 - + + diff --git a/samples/cs/GettingStarted/windows/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj b/samples/cs/GettingStarted/windows/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj index cacd0c4..adf2100 100644 --- a/samples/cs/GettingStarted/windows/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj +++ b/samples/cs/GettingStarted/windows/HelloFoundryLocalSdk/HelloFoundryLocalSdk.csproj @@ -5,13 +5,14 @@ enable enable - net8.0-windows10.0.26100 + net9.0-windows10.0.26100 true ARM64;x64 + From 9f9335a109c15007cb2f50aaedd49074222c471e Mon Sep 17 00:00:00 2001 From: Scott McKay Date: Fri, 14 Nov 2025 18:40:21 +1000 Subject: [PATCH 2/4] Update comments --- .../cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs b/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs index 0764f63..6d74900 100644 --- a/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs +++ b/samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs @@ -67,19 +67,21 @@ // OPTIONAL: `model` can be used directly and `model.SelectedVariant` will be used as the default. // You can explicitly select or use a specific ModelVariant if you want more control // over the device and/or execution provider used. +// Model and ModelVariant can be used interchangeably in methods such as +// DownloadAsync, LoadAsync, UnloadAsync and GetChatClientAsync. // -// Options: +// Choices: // - Use a ModelVariant directly from the catalog if you know the variant Id // - `var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-gpu:3")` // // - Get the ModelVariant from Model.Variants // - `var modelVariant = model.Variants.First(v => v.Id == "qwen2.5-0.5b-instruct-generic-cpu:4")` // - `var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.GPU)` -// - Update selected variant in `model`: `model.SelectVariant(modelVariant);` +// - optional: update selected variant in `model` using `model.SelectVariant(modelVariant);` if you wish to use +// `model` in your code. // For this example we explicitly select the CPU variant, and call SelectVariant so all the following example code // uses the `model` instance. -// You can also use modelVariant for download/load/get client/unload. Console.WriteLine("Selecting CPU variant of model"); var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); model.SelectVariant(modelVariant); From 92bb176920ec8e2a893962efd2fe089d84ac5dba Mon Sep 17 00:00:00 2001 From: Scott McKay Date: Fri, 14 Nov 2025 20:18:08 +1000 Subject: [PATCH 3/4] Update samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../cs/GettingStarted/src/FoundryLocalWebServer/Program.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs b/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs index fa5ce51..2be8296 100644 --- a/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs +++ b/samples/cs/GettingStarted/src/FoundryLocalWebServer/Program.cs @@ -30,10 +30,7 @@ // Get a model using an alias -var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found") - ?? throw new Exception("Model not found"); - - +var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found"); // Download the model (the method skips download if already cached) await model.DownloadAsync(progress => { From e971de9f47995ac58b0a4df28dd90ded198ff19c Mon Sep 17 00:00:00 2001 From: Scott McKay Date: Fri, 14 Nov 2025 20:22:48 +1000 Subject: [PATCH 4/4] Fix LoggerFactory lifetime --- samples/cs/GettingStarted/src/Shared/Utils.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/samples/cs/GettingStarted/src/Shared/Utils.cs b/samples/cs/GettingStarted/src/Shared/Utils.cs index 4f8037a..b9c0fcf 100644 --- a/samples/cs/GettingStarted/src/Shared/Utils.cs +++ b/samples/cs/GettingStarted/src/Shared/Utils.cs @@ -3,18 +3,23 @@ internal static class Utils { + private static readonly ILoggerFactory _loggerFactory; + + static Utils() + { + _loggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information); + }); + } + /// /// Get a dummy application logger. /// /// ILogger internal static ILogger GetAppLogger() { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); - }); - - return loggerFactory.CreateLogger("FoundryLocalSamples"); + return _loggerFactory.CreateLogger("FoundryLocalSamples"); } internal static async Task RunWithSpinner(string msg, T workTask) where T : Task