From 3d76c98ca099bb41c724a50eef096a9fdb71a661 Mon Sep 17 00:00:00 2001 From: Dennis Adolfi Date: Fri, 15 Aug 2025 09:42:39 +0200 Subject: [PATCH 1/3] - Added test for StartModelAsync_Succeeds_WhenModelIsInCatalogAndCache to verify that model can be started from local cache. - Added test for StartModelAsync_Succeeds_WhenModelIsDownloaded to verify that model can be started from empty cache which downloads model. --- .../FoundryLocalManagerTest.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs b/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs index 8465830..1a4a769 100644 --- a/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs +++ b/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs @@ -399,6 +399,83 @@ public async Task LoadModelAsync_Succeeds_WhenModelIsInCatalogAndCache() Assert.Equal(modelId, result.ModelId); } + [Fact] + public async Task StartModelAsync_Succeeds_WhenModelIsInCatalogAndCache() + { + // Arrange + var modelId = "model1"; + var model = new ModelInfo + { + ModelId = modelId, + Alias = "alias1", + Uri = "http://model", + ProviderType = "huggingface", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = ExecutionProvider.WebGpuExecutionProvider } + }; + + _mockHttp.When("/openai/models").Respond("application/json", $"[\"{modelId}\"]"); + _mockHttp.When(HttpMethod.Get, $"/openai/load/{modelId}*").Respond("application/json", "{}"); + + // Inject catalog dictionary with the model + typeof(FoundryLocalManager) + .GetField("_catalogDictionary", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(_manager, new Dictionary + { + { modelId, model }, + { model.Alias, model } + }); + + // Inject local cache list with the model + typeof(FoundryLocalManager) + .GetField("_catalogModels", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(_manager, new List { model }); + + // Act + var result = await _manager.StartModelAsync(modelId); + + // Assert + Assert.NotNull(result); + Assert.Equal(modelId, result.ModelId); + } + + [Fact] + public async Task StartModelAsync_Succeeds_WhenModelIsDownloaded() + { + // Arrange + var modelId = "model1"; + var model = new ModelInfo + { + ModelId = modelId, + Alias = "alias1", + Uri = "http://model", + ProviderType = "huggingface", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = ExecutionProvider.WebGpuExecutionProvider } + }; + + var mockJsonResponse = "some log text... {\"success\": true, \"errorMessage\": null}"; + _mockHttp.When("/openai/download").Respond("application/json", mockJsonResponse); + _mockHttp.When("/openai/models").Respond("application/json", $"[\"{modelId}\"]"); + _mockHttp.When(HttpMethod.Get, $"/openai/load/{modelId}*").Respond("application/json", "{}"); + + var catalogJson = JsonSerializer.Serialize(new List { model }); + _mockHttp.When(HttpMethod.Get, "/foundry/list").Respond("application/json", catalogJson); + + // Inject catalog dictionary and models with null to ensure model is not previously loaded + typeof(FoundryLocalManager) + .GetField("_catalogDictionary", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(_manager, null); + typeof(FoundryLocalManager) + .GetField("_catalogModels", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(_manager, null); + + // Act + var result = await _manager.StartModelAsync(modelId); + + // Assert + Assert.NotNull(result); + Assert.Equal(modelId, result.ModelId); + } + [Fact] public async Task LoadModelAsync_ThrowsIfNotInCache() { From fe45236e69b8f97115e13de37ee7ce107ccfc7e9 Mon Sep 17 00:00:00 2001 From: Dennis Adolfi Date: Fri, 15 Aug 2025 09:43:33 +0200 Subject: [PATCH 2/3] Refactored StartModelAsync to non-static and use same code structure as alla other methods in FoundryLocalManager --- sdk/cs/src/FoundryLocalManager.cs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/sdk/cs/src/FoundryLocalManager.cs b/sdk/cs/src/FoundryLocalManager.cs index 7e68179..2dd82c5 100644 --- a/sdk/cs/src/FoundryLocalManager.cs +++ b/sdk/cs/src/FoundryLocalManager.cs @@ -83,23 +83,14 @@ public FoundryLocalManager() .ToDictionary(x => x.provider, x => x.index); } - public static async Task StartModelAsync(string aliasOrModelId, CancellationToken ct = default) + public async Task StartModelAsync(string aliasOrModelId, CancellationToken ct = default) { - var manager = new FoundryLocalManager(); - try - { - await manager.StartServiceAsync(ct); - var modelInfo = await manager.GetModelInfoAsync(aliasOrModelId, ct) + await StartServiceAsync(ct); + var modelInfo = await GetModelInfoAsync(aliasOrModelId, ct) ?? throw new InvalidOperationException($"Model {aliasOrModelId} not found in catalog."); - await manager.DownloadModelAsync(modelInfo.ModelId, ct: ct); - await manager.LoadModelAsync(aliasOrModelId, ct: ct); - return manager; - } - catch - { - manager.Dispose(); - throw; - } + await DownloadModelAsync(modelInfo.ModelId, ct: ct); + await LoadModelAsync(aliasOrModelId, ct: ct); + return modelInfo; } public async Task StartServiceAsync(CancellationToken ct = default) From 70350ee6ba57fd72a2d776aa9b7393c2124eee24 Mon Sep 17 00:00:00 2001 From: Dennis Adolfi Date: Wed, 24 Sep 2025 09:25:36 +0200 Subject: [PATCH 3/3] Resolved merge conflicts. Removed FoundryLocalManager constructor according to latest main. Updated tests to work with new main --- sdk/cs/src/FoundryLocalManager.cs | 25 ++----------------- .../FoundryLocalManagerTest.cs | 24 ++++-------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/sdk/cs/src/FoundryLocalManager.cs b/sdk/cs/src/FoundryLocalManager.cs index d773ba3..8e5b206 100644 --- a/sdk/cs/src/FoundryLocalManager.cs +++ b/sdk/cs/src/FoundryLocalManager.cs @@ -51,34 +51,13 @@ public partial class FoundryLocalManager : IDisposable, IAsyncDisposable // Sees if the service is already running public bool IsServiceRunning => _serviceUri != null; - public FoundryLocalManager() - { - var preferredOrder = new List - { - ExecutionProvider.QNNExecutionProvider, - ExecutionProvider.CUDAExecutionProvider, - ExecutionProvider.CPUExecutionProvider, - ExecutionProvider.WebGpuExecutionProvider - }; - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - preferredOrder.Remove(ExecutionProvider.CPUExecutionProvider); - preferredOrder.Add(ExecutionProvider.CPUExecutionProvider); - } - - _priorityMap = preferredOrder - .Select((provider, index) => new { provider, index }) - .ToDictionary(x => x.provider, x => x.index); - } - public async Task StartModelAsync(string aliasOrModelId, DeviceType? device = null, CancellationToken ct = default) { await StartServiceAsync(ct); var modelInfo = await GetModelInfoAsync(aliasOrModelId, device, ct) ?? throw new InvalidOperationException($"Model {aliasOrModelId} not found in catalog."); - await manager.DownloadModelAsync(modelInfo.ModelId, device: device, token: null, force: false,ct: ct); - await manager.LoadModelAsync(aliasOrModelId, device: device, ct: ct); + await DownloadModelAsync(modelInfo.ModelId, device: device, token: null, force: false,ct: ct); + await LoadModelAsync(aliasOrModelId, device: device, ct: ct); return modelInfo; } diff --git a/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs b/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs index de28222..29f5236 100644 --- a/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs +++ b/sdk/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs @@ -522,7 +522,7 @@ public async Task LoadModelAsync_Succeeds_AndPassesEpOverrideWhenCudaPresent() } [Fact] - public async Task StartModelAsync_Succeeds_WhenModelIsInCatalogAndCache() + public async Task StartModelAsync_Succeeds_WhenModelIsInCatalogModelsCache() { // Arrange var modelId = "model1"; @@ -532,22 +532,12 @@ public async Task StartModelAsync_Succeeds_WhenModelIsInCatalogAndCache() Alias = "alias1", Uri = "http://model", ProviderType = "huggingface", - Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = ExecutionProvider.WebGpuExecutionProvider } + Runtime = new Runtime { DeviceType = DeviceType.GPU } }; _mockHttp.When("/openai/models").Respond("application/json", $"[\"{modelId}\"]"); _mockHttp.When(HttpMethod.Get, $"/openai/load/{modelId}*").Respond("application/json", "{}"); - // Inject catalog dictionary with the model - typeof(FoundryLocalManager) - .GetField("_catalogDictionary", BindingFlags.NonPublic | BindingFlags.Instance)! - .SetValue(_manager, new Dictionary - { - { modelId, model }, - { model.Alias, model } - }); - - // Inject local cache list with the model typeof(FoundryLocalManager) .GetField("_catalogModels", BindingFlags.NonPublic | BindingFlags.Instance)! .SetValue(_manager, new List { model }); @@ -561,7 +551,7 @@ public async Task StartModelAsync_Succeeds_WhenModelIsInCatalogAndCache() } [Fact] - public async Task StartModelAsync_Succeeds_WhenModelIsDownloaded() + public async Task StartModelAsync_Succeeds_WhenModelIsDownloadedButNotInCatalogModelsCache() { // Arrange var modelId = "model1"; @@ -571,7 +561,7 @@ public async Task StartModelAsync_Succeeds_WhenModelIsDownloaded() Alias = "alias1", Uri = "http://model", ProviderType = "huggingface", - Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = ExecutionProvider.WebGpuExecutionProvider } + Runtime = new Runtime { DeviceType = DeviceType.GPU } }; var mockJsonResponse = "some log text... {\"success\": true, \"errorMessage\": null}"; @@ -581,11 +571,7 @@ public async Task StartModelAsync_Succeeds_WhenModelIsDownloaded() var catalogJson = JsonSerializer.Serialize(new List { model }); _mockHttp.When(HttpMethod.Get, "/foundry/list").Respond("application/json", catalogJson); - - // Inject catalog dictionary and models with null to ensure model is not previously loaded - typeof(FoundryLocalManager) - .GetField("_catalogDictionary", BindingFlags.NonPublic | BindingFlags.Instance)! - .SetValue(_manager, null); + typeof(FoundryLocalManager) .GetField("_catalogModels", BindingFlags.NonPublic | BindingFlags.Instance)! .SetValue(_manager, null);