diff --git a/sdk/cs/src/Catalog.cs b/sdk/cs/src/Catalog.cs index eb9ba0d7..5cdb050f 100644 --- a/sdk/cs/src/Catalog.cs +++ b/sdk/cs/src/Catalog.cs @@ -52,51 +52,59 @@ internal static async Task CreateAsync(IModelLoadManager modelManager, return catalog; } - public async Task> ListModelsAsync(CancellationToken? ct = null) + public async Task> ListModelsAsync(CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => ListModelsImplAsync(ct), "Error listing models.", _logger).ConfigureAwait(false); } - public async Task> GetCachedModelsAsync(CancellationToken? ct = null) + public async Task> GetCachedModelsAsync(CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => GetCachedModelsImplAsync(ct), "Error getting cached models.", _logger).ConfigureAwait(false); } - public async Task> GetLoadedModelsAsync(CancellationToken? ct = null) + public async Task> GetLoadedModelsAsync(CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => GetLoadedModelsImplAsync(ct), "Error getting loaded models.", _logger).ConfigureAwait(false); } - public async Task GetModelAsync(string modelAlias, CancellationToken? ct = null) + public async Task GetModelAsync(string modelAlias, CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => GetModelImplAsync(modelAlias, ct), $"Error getting model with alias '{modelAlias}'.", _logger) .ConfigureAwait(false); } - public async Task GetModelVariantAsync(string modelId, CancellationToken? ct = null) + public async Task GetModelVariantAsync(string modelId, CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => GetModelVariantImplAsync(modelId, ct), $"Error getting model variant with ID '{modelId}'.", _logger) .ConfigureAwait(false); } - private async Task> ListModelsImplAsync(CancellationToken? ct = null) + public async Task GetLatestVersionAsync(IModel modelOrModelVariant, CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling( + () => GetLatestVersionImplAsync(modelOrModelVariant, ct), + $"Error getting latest version for model with name '{modelOrModelVariant.Info.Name}'.", + _logger).ConfigureAwait(false); + } + + private async Task> ListModelsImplAsync(CancellationToken? ct = null) { await UpdateModels(ct).ConfigureAwait(false); using var disposable = await _lock.LockAsync().ConfigureAwait(false); - return _modelAliasToModel.Values.OrderBy(m => m.Alias).ToList(); + return _modelAliasToModel.Values.OrderBy(m => m.Alias).Cast().ToList(); } - private async Task> GetCachedModelsImplAsync(CancellationToken? ct = null) + private async Task> GetCachedModelsImplAsync(CancellationToken? ct = null) { var cachedModelIds = await Utils.GetCachedModelIdsAsync(_coreInterop, ct).ConfigureAwait(false); - List cachedModels = new(); + List cachedModels = []; foreach (var modelId in cachedModelIds) { if (_modelIdToModelVariant.TryGetValue(modelId, out ModelVariant? modelVariant)) @@ -108,10 +116,10 @@ private async Task> GetCachedModelsImplAsync(CancellationToke return cachedModels; } - private async Task> GetLoadedModelsImplAsync(CancellationToken? ct = null) + private async Task> GetLoadedModelsImplAsync(CancellationToken? ct = null) { var loadedModelIds = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false); - List loadedModels = new(); + List loadedModels = []; foreach (var modelId in loadedModelIds) { @@ -143,6 +151,45 @@ private async Task> GetLoadedModelsImplAsync(CancellationToke return modelVariant; } + private async Task GetLatestVersionImplAsync(IModel modelOrModelVariant, CancellationToken? ct) + { + Model? model; + + if (modelOrModelVariant is ModelVariant) + { + // For ModelVariant, resolve the owning Model via alias. + model = await GetModelImplAsync(modelOrModelVariant.Alias, ct); + } + else + { + // Try to use the concrete Model instance if this is our SDK type. + model = modelOrModelVariant as Model; + + // If this is a different IModel implementation (e.g., a test stub), + // fall back to resolving the Model via alias. + if (model == null) + { + model = await GetModelImplAsync(modelOrModelVariant.Alias, ct); + } + } + + if (model == null) + { + throw new FoundryLocalException($"Model with alias '{modelOrModelVariant.Alias}' not found in catalog.", + _logger); + } + + // variants are sorted by version, so the first one matching the name is the latest version for that variant. + var latest = model!.Variants.FirstOrDefault(v => v.Info.Name == modelOrModelVariant.Info.Name) ?? + // should not be possible given we internally manage all the state involved + throw new FoundryLocalException($"Internal error. Mismatch between model (alias:{model.Alias}) and " + + $"model variant (alias:{modelOrModelVariant.Alias}).", _logger); + + // if input was the latest return the input (could be model or model variant) + // otherwise return the latest model variant + return latest.Id == modelOrModelVariant.Id ? modelOrModelVariant : latest; + } + private async Task UpdateModels(CancellationToken? ct) { // TODO: make this configurable diff --git a/sdk/cs/src/Model.cs b/sdk/cs/src/Detail/Model.cs similarity index 74% rename from sdk/cs/src/Model.cs rename to sdk/cs/src/Detail/Model.cs index bbbbcb5b..8fc86bd7 100644 --- a/sdk/cs/src/Model.cs +++ b/sdk/cs/src/Detail/Model.cs @@ -12,11 +12,13 @@ public class Model : IModel { private readonly ILogger _logger; - public List Variants { get; internal set; } - public ModelVariant SelectedVariant { get; internal set; } = default!; + private readonly List _variants; + public IReadOnlyList Variants => _variants; + public IModel SelectedVariant { get; internal set; } = default!; public string Alias { get; init; } public string Id => SelectedVariant.Id; + public ModelInfo Info => SelectedVariant.Info; /// /// Is the currently selected variant cached locally? @@ -33,7 +35,7 @@ internal Model(ModelVariant modelVariant, ILogger logger) _logger = logger; Alias = modelVariant.Alias; - Variants = new() { modelVariant }; + _variants = [modelVariant]; // variants are sorted by Core, so the first one added is the default SelectedVariant = modelVariant; @@ -48,7 +50,7 @@ internal void AddVariant(ModelVariant variant) _logger); } - Variants.Add(variant); + _variants.Add(variant); // prefer the highest priority locally cached variant if (variant.Info.Cached && !SelectedVariant.Info.Cached) @@ -62,31 +64,15 @@ internal void AddVariant(ModelVariant variant) /// /// Model variant to select. Must be one of the variants in . /// If variant is not valid for this model. - public void SelectVariant(ModelVariant variant) + public void SelectVariant(IModel variant) { _ = Variants.FirstOrDefault(v => v == variant) ?? - // user error so don't log - throw new FoundryLocalException($"Model {Alias} does not have a {variant.Id} variant."); + // user error so don't log. + throw new FoundryLocalException($"Input variant was not found in Variants."); SelectedVariant = variant; } - /// - /// Get the latest version of the specified model variant. - /// - /// Model variant. - /// ModelVariant for latest version. Same as `variant` if that is the latest version. - /// If variant is not valid for this model. - public ModelVariant GetLatestVersion(ModelVariant variant) - { - // variants are sorted by version, so the first one matching the name is the latest version for that variant. - var latest = Variants.FirstOrDefault(v => v.Info.Name == variant.Info.Name) ?? - // user error so don't log - throw new FoundryLocalException($"Model {Alias} does not have a {variant.Id} variant."); - - return latest; - } - public async Task GetPathAsync(CancellationToken? ct = null) { return await SelectedVariant.GetPathAsync(ct).ConfigureAwait(false); diff --git a/sdk/cs/src/ModelVariant.cs b/sdk/cs/src/Detail/ModelVariant.cs similarity index 95% rename from sdk/cs/src/ModelVariant.cs rename to sdk/cs/src/Detail/ModelVariant.cs index 6ca7cda7..78f7a56c 100644 --- a/sdk/cs/src/ModelVariant.cs +++ b/sdk/cs/src/Detail/ModelVariant.cs @@ -9,7 +9,7 @@ namespace Microsoft.AI.Foundry.Local; using Microsoft.AI.Foundry.Local.Detail; using Microsoft.Extensions.Logging; -public class ModelVariant : IModel +internal class ModelVariant : IModel { private readonly IModelLoadManager _modelLoadManager; private readonly ICoreInterop _coreInterop; @@ -22,6 +22,9 @@ public class ModelVariant : IModel public string Alias => Info.Alias; public int Version { get; init; } // parsed from Info.Version if possible, else 0 + public IReadOnlyList Variants => [this]; + public IModel SelectedVariant => this; + internal ModelVariant(ModelInfo modelInfo, IModelLoadManager modelLoadManager, ICoreInterop coreInterop, ILogger logger) { @@ -190,4 +193,11 @@ private async Task GetAudioClientImplAsync(CancellationToken? return new OpenAIAudioClient(Id); } + + public void SelectVariant(IModel variant) + { + throw new FoundryLocalException( + $"SelectVariant is not supported on a ModelVariant. " + + $"Call Catalog.GetModelAsync(\"{Alias}\") to get a Model with all variants available."); + } } diff --git a/sdk/cs/src/ICatalog.cs b/sdk/cs/src/ICatalog.cs index 35285736..dce2f70c 100644 --- a/sdk/cs/src/ICatalog.cs +++ b/sdk/cs/src/ICatalog.cs @@ -18,36 +18,44 @@ public interface ICatalog /// List the available models in the catalog. /// /// Optional CancellationToken. - /// List of Model instances. - Task> ListModelsAsync(CancellationToken? ct = null); + /// List of IModel instances. + Task> ListModelsAsync(CancellationToken? ct = null); /// /// Lookup a model by its alias. /// /// Model alias. /// Optional CancellationToken. - /// The matching Model, or null if no model with the given alias exists. - Task GetModelAsync(string modelAlias, CancellationToken? ct = null); + /// The matching IModel, or null if no model with the given alias exists. + Task GetModelAsync(string modelAlias, CancellationToken? ct = null); /// /// Lookup a model variant by its unique model id. /// /// Model id. /// Optional CancellationToken. - /// The matching ModelVariant, or null if no variant with the given id exists. - Task GetModelVariantAsync(string modelId, CancellationToken? ct = null); + /// The matching IModel, or null if no variant with the given id exists. + Task GetModelVariantAsync(string modelId, CancellationToken? ct = null); /// /// Get a list of currently downloaded models from the model cache. /// /// Optional CancellationToken. - /// List of ModelVariant instances. - Task> GetCachedModelsAsync(CancellationToken? ct = null); + /// List of IModel instances. + Task> GetCachedModelsAsync(CancellationToken? ct = null); /// /// Get a list of the currently loaded models. /// /// Optional CancellationToken. - /// List of ModelVariant instances. - Task> GetLoadedModelsAsync(CancellationToken? ct = null); + /// List of IModel instances. + Task> GetLoadedModelsAsync(CancellationToken? ct = null); + + /// + /// Get the latest version of a model. + /// This is used to check if a newer version of a model is available in the catalog for download. + /// + /// The model to check for the latest version. + /// The latest version of the model. Will match the input if it is the latest version. + Task GetLatestVersionAsync(IModel model, CancellationToken? ct = null); } diff --git a/sdk/cs/src/IModel.cs b/sdk/cs/src/IModel.cs index c3acba61..98245933 100644 --- a/sdk/cs/src/IModel.cs +++ b/sdk/cs/src/IModel.cs @@ -16,6 +16,8 @@ public interface IModel Justification = "Alias is a suitable name in this context.")] string Alias { get; } + ModelInfo Info { get; } + Task IsCachedAsync(CancellationToken? ct = null); Task IsLoadedAsync(CancellationToken? ct = null); @@ -67,4 +69,21 @@ Task DownloadAsync(Action? downloadProgress = null, /// Optional cancellation token. /// OpenAI.AudioClient Task GetAudioClientAsync(CancellationToken? ct = null); + + /// + /// Variants of the model that are available. Variants of the model are optimized for different devices. + /// + IReadOnlyList Variants { get; } + + /// + /// Currently selected model variant in use. + /// + IModel SelectedVariant { get; } + + /// + /// Select a specific model variant from to use for operations. + /// + /// Model variant to select. Must be one of the variants in . + /// If variant is not valid for this model. + void SelectVariant(IModel variant); } diff --git a/sdk/cs/test/FoundryLocal.Tests/AudioClientTests.cs b/sdk/cs/test/FoundryLocal.Tests/AudioClientTests.cs index ec4ab4c9..5c4cc8d6 100644 --- a/sdk/cs/test/FoundryLocal.Tests/AudioClientTests.cs +++ b/sdk/cs/test/FoundryLocal.Tests/AudioClientTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.AI.Foundry.Local.Tests; internal sealed class AudioClientTests { - private static Model? model; + private static IModel? model; [Before(Class)] public static async Task Setup() diff --git a/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs b/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs new file mode 100644 index 00000000..d270ac15 --- /dev/null +++ b/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs @@ -0,0 +1,121 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging.Abstractions; + +using Moq; + +internal sealed class CatalogTests +{ + [Test] + public async Task GetLatestVersion_Works() + { + // Create test data with 3 entries for a model with different versions + // Sorted by version (descending), so version 3 is first (latest) + var testModelInfos = new List + { + new() + { + Id = "test-model:3", + Name = "test-model", + Version = 3, + Alias = "test-alias", + DisplayName = "Test Model", + ProviderType = "test", + Uri = "test://model/3", + ModelType = "ONNX", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Cached = false + }, + new() + { + Id = "test-model:2", + Name = "test-model", + Version = 2, + Alias = "test-alias", + DisplayName = "Test Model", + ProviderType = "test", + Uri = "test://model/2", + ModelType = "ONNX", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Cached = false + }, + new() + { + Id = "test-model:1", + Name = "test-model", + Version = 1, + Alias = "test-alias", + DisplayName = "Test Model", + ProviderType = "test", + Uri = "test://model/1", + ModelType = "ONNX", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Cached = false + } + }; + + // Serialize the test data + var modelListJson = JsonSerializer.Serialize(testModelInfos, JsonSerializationContext.Default.ListModelInfo); + + // Create mock ICoreInterop + var mockCoreInterop = new Mock(); + + // Mock get_catalog_name + mockCoreInterop.Setup(x => x.ExecuteCommand("get_catalog_name", It.IsAny())) + .Returns(new ICoreInterop.Response { Data = "TestCatalog", Error = null }); + + // Mock get_model_list + mockCoreInterop.Setup(x => x.ExecuteCommandAsync("get_model_list", It.IsAny(), It.IsAny())) + .ReturnsAsync(new ICoreInterop.Response { Data = modelListJson, Error = null }); + + // Create mock IModelLoadManager + var mockLoadManager = new Mock(); + + // Create Catalog instance directly (internals are visible to test project) + var catalog = await Catalog.CreateAsync(mockLoadManager.Object, mockCoreInterop.Object, + NullLogger.Instance, null); + + // Get the model + var model = await catalog.GetModelAsync("test-alias"); + await Assert.That(model).IsNotNull(); + + // Verify we have 3 variants + await Assert.That(model!.Variants).HasCount().EqualTo(3); + + // Get the variants - they should be sorted by version (descending) + var variants = model.Variants.ToList(); + var latestVariant = variants[0]; // version 3 + var middleVariant = variants[1]; // version 2 + var oldestVariant = variants[2]; // version 1 + + await Assert.That(latestVariant.Id).IsEqualTo("test-model:3"); + await Assert.That(middleVariant.Id).IsEqualTo("test-model:2"); + await Assert.That(oldestVariant.Id).IsEqualTo("test-model:1"); + + // Test GetLatestVersionAsync with all 3 variants - should always return the first (version 3) + var result1 = await catalog.GetLatestVersionAsync(latestVariant); + await Assert.That(result1.Id).IsEqualTo("test-model:3"); + + var result2 = await catalog.GetLatestVersionAsync(middleVariant); + await Assert.That(result2.Id).IsEqualTo("test-model:3"); + + var result3 = await catalog.GetLatestVersionAsync(oldestVariant); + await Assert.That(result3.Id).IsEqualTo("test-model:3"); + + // Test with Model input - when latest is selected, should get Model not ModelVariant back + model.SelectVariant(latestVariant); + var result4 = await catalog.GetLatestVersionAsync(model); + await Assert.That(result4).IsEqualTo(model); + } +} diff --git a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs index b7a91190..2624f98a 100644 --- a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs +++ b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.AI.Foundry.Local.Tests; internal sealed class ChatCompletionsTests { - private static Model? model; + private static IModel? model; [Before(Class)] public static async Task Setup() @@ -24,11 +24,10 @@ public static async Task Setup() var catalog = await manager.GetCatalogAsync(); // Load the specific cached model variant directly - var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:4").ConfigureAwait(false); - await Assert.That(modelVariant).IsNotNull(); + var model = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:4").ConfigureAwait(false); + await Assert.That(model).IsNotNull(); - var model = new Model(modelVariant!, manager.Logger); - await model.LoadAsync().ConfigureAwait(false); + await model!.LoadAsync().ConfigureAwait(false); await Assert.That(await model.IsLoadedAsync()).IsTrue(); ChatCompletionsTests.model = model; diff --git a/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs b/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs index 80ab4c0a..56c70769 100644 --- a/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs +++ b/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs @@ -29,8 +29,9 @@ public async Task EndToEndTest_Succeeds() await Assert.That(modelVariant).IsNotNull(); await Assert.That(modelVariant!.Alias).IsEqualTo("qwen2.5-0.5b"); - // Create model from the specific variant - var model = new Model(modelVariant, manager.Logger); + // Get Model for variant and select the variant so `model` and `modelVariant` should be equivalent + var model = await catalog.GetModelAsync(modelVariant.Alias); + model!.SelectVariant(modelVariant); // uncomment this to remove the model first to test the download progress // only do this when manually testing as other tests expect the model to be cached diff --git a/sdk/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md b/sdk/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md index 1145cd9d..1b4a71e7 100644 --- a/sdk/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md +++ b/sdk/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md @@ -6,10 +6,14 @@ The test model cache directory name is configured in `sdk/cs/test/FoundryLocal.T ```json { - "TestModelCacheDirName": "/path/to/model/cache" + "TestModelCacheDirName": "test-data-shared" } ``` +If the value is a directory name it will be resolved as /../{TestModelCacheDirName}. +Otherwise the value will be resolved using Path.GetFullPath, which allows for absolute paths or +relative paths based on the current working directory. + ## Run the tests The tests will automatically find the models in the configured test model cache directory. @@ -17,21 +21,4 @@ The tests will automatically find the models in the configured test model cache ```bash cd /path/to/parent-dir/foundry-local-sdk/sdk/cs/test/FoundryLocal.Tests dotnet test Microsoft.AI.Foundry.Local.Tests.csproj --configuration Release# Running Local Model Tests - -## Configuration - -The test model cache directory name is configured in `sdk/cs/test/FoundryLocal.Tests/appsettings.Test.json`: - -```json -{ - "TestModelCacheDirName": "/path/to/model/cache" -} ``` - -## Run the tests - -The tests will automatically find the models in the configured test model cache directory. - -```bash -cd /path/to/parent-dir/foundry-local-sdk/sdk/cs/test/FoundryLocal.Tests -dotnet test Microsoft.AI.Foundry.Local.Tests.csproj --configuration Release \ No newline at end of file diff --git a/sdk/cs/test/FoundryLocal.Tests/ModelTests.cs b/sdk/cs/test/FoundryLocal.Tests/ModelTests.cs deleted file mode 100644 index b5a49657..00000000 --- a/sdk/cs/test/FoundryLocal.Tests/ModelTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) Microsoft. All rights reserved. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace Microsoft.AI.Foundry.Local.Tests; -using System.Collections.Generic; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging.Abstractions; - -using Moq; - -internal sealed class ModelTests -{ - [Test] - public async Task GetLastestVersion_Works() - { - var loadManager = new Mock(); - var coreInterop = new Mock(); - var logger = NullLogger.Instance; - - var createModelInfo = (string name, int version) => new ModelInfo - { - Id = $"{name}:{version}", - Alias = "model", - Name = name, - Version = version, - Uri = "local://model", - ProviderType = "local", - ModelType = "test" - }; - - var variants = new List - { - new(createModelInfo("model_a", 4), loadManager.Object, coreInterop.Object, logger), - new(createModelInfo("model_b", 3), loadManager.Object, coreInterop.Object, logger), - new(createModelInfo("model_b", 2), loadManager.Object, coreInterop.Object, logger), - }; - - var model = new Model(variants[0], NullLogger.Instance); - foreach (var variant in variants.Skip(1)) - { - model.AddVariant(variant); - } - - var latestA = model.GetLatestVersion(variants[0]); - await Assert.That(latestA).IsEqualTo(variants[0]); - - var latestB = model.GetLatestVersion(variants[2]); - await Assert.That(latestB).IsEqualTo(variants[1]); - } -} diff --git a/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs b/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs index ac536d12..2136a8eb 100644 --- a/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs +++ b/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs @@ -15,16 +15,20 @@ public static async Task Cleanup(AssemblyHookContext _) { try { - // ensure any loaded models are unloaded - var manager = FoundryLocalManager.Instance; // initialized by Utils - var catalog = await manager.GetCatalogAsync(); - var models = await catalog.GetLoadedModelsAsync().ConfigureAwait(false); - - foreach (var model in models) + // if running individual test/s they may not have used the Utils class which creates FoundryLocalManager + if (FoundryLocalManager.IsInitialized) { - await Assert.That(await model.IsLoadedAsync()).IsTrue(); - await model.UnloadAsync().ConfigureAwait(false); - await Assert.That(await model.IsLoadedAsync()).IsFalse(); + // ensure any loaded models are unloaded + var manager = FoundryLocalManager.Instance; // initialized by Utils + var catalog = await manager.GetCatalogAsync(); + var models = await catalog.GetLoadedModelsAsync().ConfigureAwait(false); + + foreach (var model in models) + { + await Assert.That(await model.IsLoadedAsync()).IsTrue(); + await model.UnloadAsync().ConfigureAwait(false); + await Assert.That(await model.IsLoadedAsync()).IsFalse(); + } } } catch (Exception ex) diff --git a/sdk/cs/test/FoundryLocal.Tests/Utils.cs b/sdk/cs/test/FoundryLocal.Tests/Utils.cs index 6313b0d5..9611d0d4 100644 --- a/sdk/cs/test/FoundryLocal.Tests/Utils.cs +++ b/sdk/cs/test/FoundryLocal.Tests/Utils.cs @@ -55,7 +55,7 @@ public static void AssemblyInit(AssemblyHookContext _) .AddJsonFile("appsettings.Test.json", optional: true, reloadOnChange: false) .Build(); - var testModelCacheDirName = "test-data-shared"; + var testModelCacheDirName = configuration["TestModelCacheDirName"] ?? "test-data-shared"; string testDataSharedPath; if (Path.IsPathRooted(testModelCacheDirName) || testModelCacheDirName.Contains(Path.DirectorySeparatorChar) || @@ -74,6 +74,8 @@ public static void AssemblyInit(AssemblyHookContext _) if (!Directory.Exists(testDataSharedPath)) { + // need to ensure there's a user visible error when running in VS. + logger.LogCritical($"Test model cache directory does not exist: {testDataSharedPath}"); throw new DirectoryNotFoundException($"Test model cache directory does not exist: {testDataSharedPath}"); } diff --git a/sdk/cs/test/FoundryLocal.Tests/appsettings.Test.json b/sdk/cs/test/FoundryLocal.Tests/appsettings.Test.json index 87410c33..d42d8789 100644 --- a/sdk/cs/test/FoundryLocal.Tests/appsettings.Test.json +++ b/sdk/cs/test/FoundryLocal.Tests/appsettings.Test.json @@ -1,3 +1,3 @@ { - "TestModelCacheDirName": "/path/to/test/model/cache" + "TestModelCacheDirName": "test-data-shared" }