diff --git a/README.md b/README.md index c3dac65f..d3936414 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Open Chat Playground (OCP) is a web UI that is able to connect virtually any LLM - [x] [Hugging Face](https://huggingface.co/docs) - [x] [Ollama](https://github.com/ollama/ollama/tree/main/docs) - [ ] [Anthropic](https://docs.anthropic.com) -- [ ] [Naver](https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary) +- [ ] ~~[Naver](https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary)~~ - [x] [LG](https://github.com/LG-AI-EXAONE) - [x] [OpenAI](https://openai.com/api) - [x] [Upstage](https://console.upstage.ai/docs/getting-started) @@ -77,7 +77,7 @@ Open Chat Playground (OCP) is a web UI that is able to connect virtually any LLM - [Use Azure AI Foundry](./docs/azure-ai-foundry.md#run-in-local-container) - [Use GitHub Models](./docs/github-models.md#run-in-local-container) - [Use Docker Model Runner](./docs/docker-model-runner.md#run-in-local-container) -- ~~Use Foundry Local~~ 👉 NOT SUPPORTED +- [Use Foundry Local](./docs/foundry-local.md#run-in-local-container) - [Use Hugging Face](./docs/hugging-face.md#run-in-local-container) - [Use Ollama](./docs/ollama.md#run-on-local-container) - [Use LG](./docs/lg.md#run-in-local-container) diff --git a/docs/foundry-local.md b/docs/foundry-local.md index 53a933f6..0bb9df4c 100644 --- a/docs/foundry-local.md +++ b/docs/foundry-local.md @@ -1,6 +1,6 @@ # OpenChat Playground with Foundry Local -This page describes how to run OpenChat Playground (OCP) with Foundry Local models integration. +This page describes how to run OpenChat Playground (OCP) with [Foundry Local](https://learn.microsoft.com/azure/ai-foundry/foundry-local/what-is-foundry-local) integration. ## Get the repository root @@ -18,7 +18,7 @@ This page describes how to run OpenChat Playground (OCP) with Foundry Local mode ## Run on local machine -1. Make sure the Foundry Local server is up and running. +1. Make sure the Foundry Local server is up and running with the following command. ```bash foundry service start @@ -74,4 +74,109 @@ This page describes how to run OpenChat Playground (OCP) with Foundry Local mode --alias qwen2.5-7b ``` -1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts. \ No newline at end of file +1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts. + +## Run in local container + +1. Make sure the Foundry Local server is up and running. + + ```bash + foundry service start + ``` + +1. Get the Foundry Local service port. + + ```bash + # bash/zsh + FL_PORT_NUMBER=$(foundry service set --show true | sed -n '/^{/,/^}/p' | jq -r ".serviceSettings.port") + ``` + + ```powershell + # PowerShell + $FL_PORT_NUMBER = (foundry service set --show true | ` + ForEach-Object { ` + if ($_ -match '^{') { $capture = $true } ` + if ($capture) { $_ } ` + if ($_ -match '^}') { $capture = $false } ` + } | Out-String | ConvertFrom-Json).serviceSettings.port + ``` + +1. Download the Foundry Local model. The default model OCP uses is `phi-4-mini`. + + ```bash + foundry model download phi-4-mini + ``` + + Alternatively, if you want to run with a different model, say `qwen2.5-7b`, other than the default one, download it first by running the following command. + + ```bash + foundry model download qwen2.5-7b + ``` + + Make sure to follow the model MUST be selected from the CLI output of `foundry model list`. + +1. Load the Foundry Local model. The default model OCP uses is `phi-4-mini`. + + ```bash + foundry model load phi-4-mini + ``` + + Alternatively, if you want to run with a different model, say `qwen2.5-7b`, other than the default one, download it first by running the following command. + + ```bash + foundry model load qwen2.5-7b + ``` + +1. Make sure you are at the repository root. + + ```bash + cd $REPOSITORY_ROOT + ``` + +1. Build a container. + + ```bash + docker build -f Dockerfile -t openchat-playground:latest . + ``` + +1. Run the app. The `{{Model ID}}` refers to the `Model ID` shown in the output of the `foundry service list` command. + + > **NOTE**: Make sure it MUST be the model ID, instead of alias. + + ```bash + # bash/zsh - from locally built container + docker run -i --rm -p 8080:8080 openchat-playground:latest \ + --connector-type FoundryLocal \ + --base-url http://host.docker.internal:$FL_PORT_NUMBER/ \ + --model "Phi-4-mini-instruct-generic-gpu:4" \ + --disable-foundrylocal-manager + ``` + + ```powershell + # PowerShell - from locally built container + docker run -i --rm -p 8080:8080 openchat-playground:latest ` + --connector-type FoundryLocal ` + --base-url http://host.docker.internal:$FL_PORT_NUMBER/ ` + --model {{Model ID}} ` + --disable-foundrylocal-manager + ``` + + ```bash + # bash/zsh - from GitHub Container Registry + docker run -i --rm -p 8080:8080 ghcr.io/aliencube/open-chat-playground/openchat-playground:latest \ + --connector-type FoundryLocal \ + --base-url http://host.docker.internal:$FL_PORT_NUMBER/ \ + --model {{Model ID}} \ + --disable-foundrylocal-manager + ``` + + ```powershell + # PowerShell - from GitHub Container Registry + docker run -i --rm -p 8080:8080 ghcr.io/aliencube/open-chat-playground/openchat-playground:latest ` + --connector-type FoundryLocal ` + --base-url http://host.docker.internal:$FL_PORT_NUMBER/ ` + --model {{Model ID}} ` + --disable-foundrylocal-manager + ``` + +1. Open your web browser, navigate to `http://localhost:8080`, and enter prompts. diff --git a/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs b/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs index 73d58f07..bcbb251a 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs @@ -32,7 +32,11 @@ private static readonly (ConnectorType ConnectorType, string Argument, bool IsSw (ConnectorType.DockerModelRunner, ArgumentOptionConstants.DockerModelRunner.BaseUrl, false), (ConnectorType.DockerModelRunner, ArgumentOptionConstants.DockerModelRunner.Model, false), // Foundry Local + (ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.BaseUrl, false), (ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.Alias, false), + (ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.Model, false), + (ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManager, true), + (ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManagerInShort, true), // Hugging Face (ConnectorType.HuggingFace, ArgumentOptionConstants.HuggingFace.BaseUrl, false), (ConnectorType.HuggingFace, ArgumentOptionConstants.HuggingFace.Model, false), @@ -212,9 +216,11 @@ public static AppSettings Parse(IConfiguration config, string[] args) case FoundryLocalArgumentOptions foundryLocal: settings.FoundryLocal ??= new FoundryLocalSettings(); - settings.FoundryLocal.Alias = foundryLocal.Alias ?? settings.FoundryLocal.Alias; + settings.FoundryLocal.BaseUrl = foundryLocal.BaseUrl ?? settings.FoundryLocal.BaseUrl; + settings.FoundryLocal.AliasOrModel = foundryLocal.AliasOrModel ?? settings.FoundryLocal.AliasOrModel; + settings.FoundryLocal.DisableFoundryLocalManager = foundryLocal.DisableFoundryLocalManager; - settings.Model = foundryLocal.Alias ?? settings.FoundryLocal.Alias; + settings.Model = foundryLocal.AliasOrModel ?? settings.FoundryLocal.AliasOrModel; break; case HuggingFaceArgumentOptions huggingFace: @@ -361,10 +367,10 @@ private static void DisplayHelpForAmazonBedrock() Console.WriteLine(" ** Amazon Bedrock: **"); Console.ForegroundColor = foregroundColor; - Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.AccessKeyId} The AWSCredentials Access Key ID."); - Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.SecretAccessKey} The AWSCredentials Secret Access Key."); - Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.Region} The AWS region."); - Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.ModelId} The model ID. Default to 'anthropic.claude-sonnet-4-20250514-v1:0'"); + Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.AccessKeyId} The AWSCredentials Access Key ID."); + Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.SecretAccessKey} The AWSCredentials Secret Access Key."); + Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.Region} The AWS region."); + Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.ModelId} The model ID. Default to 'anthropic.claude-sonnet-4-20250514-v1:0'"); Console.WriteLine(); } @@ -424,7 +430,10 @@ private static void DisplayHelpForFoundryLocal() Console.WriteLine(" ** Foundry Local: **"); Console.ForegroundColor = foregroundColor; - Console.WriteLine(" TBD"); + Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.BaseUrl} The endpoint URL. Default to 'http://localhost:/'"); + Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.Alias}|{ArgumentOptionConstants.FoundryLocal.Model} The alias or model ID. Default to 'phi-4-mini'"); + Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManager}|{ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManagerInShort} Disable the built-in Foundry local manager."); + Console.WriteLine($" When this flag is set, you must specify '--base-url'."); Console.WriteLine(); } @@ -471,7 +480,8 @@ private static void DisplayHelpForLG() Console.WriteLine(" ** LG: **"); Console.ForegroundColor = foregroundColor; - Console.WriteLine(" TBD"); + Console.WriteLine($" {ArgumentOptionConstants.LG.BaseUrl} The baseURL. Default to 'http://localhost:11434'"); + Console.WriteLine($" {ArgumentOptionConstants.LG.Model} The model name. Default to 'hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF'"); Console.WriteLine(); } @@ -493,7 +503,7 @@ private static void DisplayHelpForOpenAI() Console.WriteLine(" ** OpenAI: **"); Console.ForegroundColor = foregroundColor; - Console.WriteLine($" {ArgumentOptionConstants.OpenAI.ApiKey} The OpenAI API key. (Env: OPENAI_API_KEY)"); + Console.WriteLine($" {ArgumentOptionConstants.OpenAI.ApiKey} The OpenAI API key."); Console.WriteLine($" {ArgumentOptionConstants.OpenAI.Model} The OpenAI model name. Default to 'gpt-4.1-mini'"); Console.WriteLine(); } diff --git a/src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs b/src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs index 5555b800..2ad0023e 100644 --- a/src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs +++ b/src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs @@ -17,7 +17,17 @@ public partial class AppSettings public class FoundryLocalSettings : LanguageModelSettings { /// - /// Gets or sets the alias of FoundryLocal. + /// Gets or sets the Base URL of Foundry Local. If `DisableFoundryLocalManager` is set, this value must be provided. /// - public string? Alias { get; set; } + public string? BaseUrl { get; set; } + + /// + /// Gets or sets either alias or model ID of Foundry Local. + /// + public string? AliasOrModel { get; set; } + + /// + /// Gets or sets a value indicating whether to disable the automatic Foundry Local manager and use a manually configured endpoint. + /// + public bool DisableFoundryLocalManager { get; set; } } \ No newline at end of file diff --git a/src/OpenChat.PlaygroundApp/Connectors/FoundryLocalConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/FoundryLocalConnector.cs index c3c36e4b..923448a4 100644 --- a/src/OpenChat.PlaygroundApp/Connectors/FoundryLocalConnector.cs +++ b/src/OpenChat.PlaygroundApp/Connectors/FoundryLocalConnector.cs @@ -1,4 +1,5 @@ using System.ClientModel; +using System.Text.RegularExpressions; using Microsoft.AI.Foundry.Local; using Microsoft.Extensions.AI; @@ -16,6 +17,10 @@ namespace OpenChat.PlaygroundApp.Connectors; /// instance. public class FoundryLocalConnector(AppSettings settings) : LanguageModelConnector(settings.FoundryLocal) { + private const string ApiKey = "OPENAI_API_KEY"; + + private static readonly Regex modelIdSuffix = new(":[0-9]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); /// @@ -26,9 +31,21 @@ public override bool EnsureLanguageModelSettingsValid() throw new InvalidOperationException("Missing configuration: FoundryLocal."); } - if (string.IsNullOrWhiteSpace(settings.Alias!.Trim())) + if (settings.DisableFoundryLocalManager == true && + string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true) { - throw new InvalidOperationException("Missing configuration: FoundryLocal:Alias."); + throw new InvalidOperationException("Missing configuration: FoundryLocal:BaseUrl is required when DisableFoundryLocalManager is enabled."); + } + + if (string.IsNullOrWhiteSpace(settings.AliasOrModel!.Trim()) == true) + { + throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel."); + } + + if (settings.DisableFoundryLocalManager == true && + modelIdSuffix.IsMatch(settings.AliasOrModel!.Trim()!) == false) + { + throw new InvalidOperationException("When DisableFoundryLocalManager is true, FoundryLocal:AliasOrModel must be the exact model name with version suffix."); } return true; @@ -38,23 +55,53 @@ public override bool EnsureLanguageModelSettingsValid() public override async Task GetChatClientAsync() { var settings = this.Settings as FoundryLocalSettings; - var alias = settings!.Alias!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:Alias."); - var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias).ConfigureAwait(false); - var model = await manager.GetModelInfoAsync(aliasOrModelId: alias).ConfigureAwait(false); + (Uri? endpoint, string? modelId) = settings!.DisableFoundryLocalManager == true + ? ParseFromModelId(settings) + : await ParseFromManagerAsync(settings).ConfigureAwait(false); - var credential = new ApiKeyCredential(manager.ApiKey); + var credential = new ApiKeyCredential(ApiKey); var options = new OpenAIClientOptions() { - Endpoint = manager.Endpoint, + Endpoint = endpoint, }; var client = new OpenAIClient(credential, options); - var chatClient = client.GetChatClient(model?.ModelId) + var chatClient = client.GetChatClient(modelId) .AsIChatClient(); - Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {alias}"); + Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {modelId}"); return chatClient; } + + private static (Uri? endpoint, string? modelId) ParseFromModelId(FoundryLocalSettings settings) + { + var baseUrl = settings.BaseUrl!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:BaseUrl."); + if (Uri.IsWellFormedUriString(baseUrl, UriKind.Absolute) == false) + { + throw new UriFormatException($"Invalid URI: The Foundry Local base URL '{baseUrl}' is not a valid URI."); + } + + var endpoint = new Uri($"{baseUrl.TrimEnd('/')}/v1"); + var modelId = settings.AliasOrModel!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel."); + if (modelIdSuffix.IsMatch(modelId) == false) + { + throw new InvalidOperationException("When DisableFoundryLocalManager is true, FoundryLocal:AliasOrModel must be the exact model name with version suffix."); + } + + return (endpoint, modelId); + } + + private static async Task<(Uri? endpoint, string? modelId)> ParseFromManagerAsync(FoundryLocalSettings settings) + { + var alias = settings!.AliasOrModel!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel."); + var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias).ConfigureAwait(false); + var model = await manager.GetModelInfoAsync(aliasOrModelId: alias).ConfigureAwait(false); + + var endpoint = manager.Endpoint; + var modelId = model!.ModelId; + + return (endpoint, modelId); + } } \ No newline at end of file diff --git a/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs b/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs index c3f88624..97baa207 100644 --- a/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs +++ b/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs @@ -130,10 +130,30 @@ public static class DockerModelRunner /// public static class FoundryLocal { + /// + /// Defines the constant for '--base-url'. + /// + public const string BaseUrl = "--base-url"; + /// /// Defines the constant for '--alias'. /// public const string Alias = "--alias"; + + /// + /// Defines the constant for '--model'. + /// + public const string Model = "--model"; + + /// + /// Defines the constant for '--disable-foundry-local-manager'. + /// + public const string DisableFoundryLocalManager = "--disable-foundry-local-manager"; + + /// + /// Defines the constant for '--disable-flm'. + /// + public const string DisableFoundryLocalManagerInShort = "--disable-flm"; } /// diff --git a/src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs b/src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs index d19b209a..03b9563b 100644 --- a/src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs +++ b/src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs @@ -10,9 +10,19 @@ namespace OpenChat.PlaygroundApp.Options; public class FoundryLocalArgumentOptions : ArgumentOptions { /// - /// Gets or sets the alias of Foundry Local. + /// Gets or sets the base URL of Foundry Local. If `DisableFoundryLocalManager` is set, this value must be provided. /// - public string? Alias { get; set; } + public string? BaseUrl { get; set; } + + /// + /// Gets or sets either alias or model ID of Foundry Local. + /// + public string? AliasOrModel { get; set; } + + /// + /// Gets or sets a value indicating whether to disable the automatic FoundryLocal manager and use a manually configured endpoint. + /// + public bool DisableFoundryLocalManager { get; set; } /// protected override void ParseOptions(IConfiguration config, string[] args) @@ -22,19 +32,34 @@ protected override void ParseOptions(IConfiguration config, string[] args) var foundryLocal = settings.FoundryLocal; - this.Alias ??= foundryLocal?.Alias; + this.BaseUrl ??= foundryLocal?.BaseUrl; + this.AliasOrModel ??= foundryLocal?.AliasOrModel; + this.DisableFoundryLocalManager = foundryLocal?.DisableFoundryLocalManager ?? false; for (var i = 0; i < args.Length; i++) { switch (args[i]) { + case ArgumentOptionConstants.FoundryLocal.BaseUrl: + if (i + 1 < args.Length) + { + this.BaseUrl = args[++i]; + } + break; + case ArgumentOptionConstants.FoundryLocal.Alias: + case ArgumentOptionConstants.FoundryLocal.Model: if (i + 1 < args.Length) { - this.Alias = args[++i]; + this.AliasOrModel = args[++i]; } break; + case ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManager: + case ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManagerInShort: + this.DisableFoundryLocalManager = true; + break; + default: break; } diff --git a/src/OpenChat.PlaygroundApp/appsettings.json b/src/OpenChat.PlaygroundApp/appsettings.json index 7c909edb..c492b80f 100644 --- a/src/OpenChat.PlaygroundApp/appsettings.json +++ b/src/OpenChat.PlaygroundApp/appsettings.json @@ -41,7 +41,9 @@ }, "FoundryLocal": { - "Alias": "phi-4-mini" + "Endpoint": "http://localhost:55434/", + "AliasOrModel": "phi-4-mini", + "DisableFoundryLocalManager": false }, "HuggingFace": { diff --git a/test/OpenChat.PlaygroundApp.Tests/Abstractions/ArgumentOptionsTests.cs b/test/OpenChat.PlaygroundApp.Tests/Abstractions/ArgumentOptionsTests.cs index e7227bff..c5ee2860 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Abstractions/ArgumentOptionsTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Abstractions/ArgumentOptionsTests.cs @@ -395,7 +395,7 @@ public void Given_Help_When_Parse_Invoked_Then_It_Should_Return_Help(string argu [InlineData(AppSettingConstants.ConnectorType, "GitHubModels", "GitHubModels:Model", "test-github-model", "test-github-model")] [InlineData(AppSettingConstants.ConnectorType, "GoogleVertexAI", "GoogleVertexAI:Model", "test-vertex-model", "test-vertex-model")] [InlineData(AppSettingConstants.ConnectorType, "DockerModelRunner", "DockerModelRunner:Model", "test-dmr-model", "test-dmr-model")] - [InlineData(AppSettingConstants.ConnectorType, "FoundryLocal", "FoundryLocal:Alias", "test-alias", "test-alias")] + [InlineData(AppSettingConstants.ConnectorType, "FoundryLocal", "FoundryLocal:AliasOrModel", "test-alias", "test-alias")] [InlineData(AppSettingConstants.ConnectorType, "HuggingFace", "HuggingFace:Model", "test-hf-model", "test-hf-model")] [InlineData(AppSettingConstants.ConnectorType, "Ollama", "Ollama:Model", "test-ollama-model", "test-ollama-model")] [InlineData(AppSettingConstants.ConnectorType, "Anthropic", "Anthropic:Model", "test-anthropic-model", "test-anthropic-model")] @@ -480,7 +480,7 @@ public void Given_ConnectorType_With_ModelConfig_And_Arguments_When_Parse_Invoke [InlineData(AppSettingConstants.ConnectorType, "GitHubModels", "GitHubModels:Model", "config-model", "config-model")] [InlineData(AppSettingConstants.ConnectorType, "GoogleVertexAI", "GoogleVertexAI:Model", "config-model", "config-model")] [InlineData(AppSettingConstants.ConnectorType, "DockerModelRunner", "DockerModelRunner:Model", "config-model", "config-model")] - [InlineData(AppSettingConstants.ConnectorType, "FoundryLocal", "FoundryLocal:Alias", "config-alias", "config-alias")] + [InlineData(AppSettingConstants.ConnectorType, "FoundryLocal", "FoundryLocal:AliasOrModel", "config-alias", "config-alias")] [InlineData(AppSettingConstants.ConnectorType, "HuggingFace", "HuggingFace:Model", "config-model", "config-model")] [InlineData(AppSettingConstants.ConnectorType, "Ollama", "Ollama:Model", "config-model", "config-model")] [InlineData(AppSettingConstants.ConnectorType, "Anthropic", "Anthropic:Model", "config-model", "config-model")] diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/FoundryLocalConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/FoundryLocalConnectorTests.cs index 8a47a1b9..2356285d 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/FoundryLocalConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/FoundryLocalConnectorTests.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + using Microsoft.Extensions.AI; using OpenChat.PlaygroundApp.Abstractions; @@ -8,16 +10,21 @@ namespace OpenChat.PlaygroundApp.Tests.Connectors; public class FoundryLocalConnectorTests { - private const string Alias = "phi-4-mini"; + private const string BaseUrl = "http://localhost:55434/"; + private const string AliasOrModel = "phi-4-mini"; + private const bool DisableFoundryLocalManager = false; - private static AppSettings BuildAppSettings(string? alias = Alias) + private static AppSettings BuildAppSettings( + string? baseUrl = BaseUrl, string? aliasOrModel = AliasOrModel, bool disableFoundryLocalManager = DisableFoundryLocalManager) { return new AppSettings { ConnectorType = ConnectorType.FoundryLocal, FoundryLocal = new FoundryLocalSettings { - Alias = alias + BaseUrl = baseUrl, + AliasOrModel = aliasOrModel, + DisableFoundryLocalManager = disableFoundryLocalManager } }; } @@ -85,13 +92,34 @@ public void Given_Null_FoundryLocalSettings_When_EnsureLanguageModelSettingsVali [Trait("Category", "UnitTest")] [Theory] [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] - [InlineData("", typeof(InvalidOperationException), "FoundryLocal:Alias")] - [InlineData(" ", typeof(InvalidOperationException), "FoundryLocal:Alias")] - [InlineData("\t\n\r", typeof(InvalidOperationException), "FoundryLocal:Alias")] - public void Given_Invalid_Alias_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? alias, Type expectedType, string expectedMessage) + [InlineData("", typeof(InvalidOperationException), "FoundryLocal:BaseUrl")] + [InlineData(" ", typeof(InvalidOperationException), "FoundryLocal:BaseUrl")] + [InlineData("\t\n\r", typeof(InvalidOperationException), "FoundryLocal:BaseUrl")] + public void Given_Invalid_BaseUrl_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? baseUrl, Type expectedType, string expectedMessage) { // Arrange - var settings = BuildAppSettings(alias: alias); + var settings = BuildAppSettings(baseUrl: baseUrl, disableFoundryLocalManager: true); + var connector = new FoundryLocalConnector(settings); + + // Act + Action action = () => connector.EnsureLanguageModelSettingsValid(); + + // Assert + action.ShouldThrow(expectedType) + .Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, false, typeof(NullReferenceException), "Object reference not set to an instance of an object")] + [InlineData("", false, typeof(InvalidOperationException), "FoundryLocal:AliasOrModel")] + [InlineData(" ", false, typeof(InvalidOperationException), "FoundryLocal:AliasOrModel")] + [InlineData("\t\n\r", false, typeof(InvalidOperationException), "FoundryLocal:AliasOrModel")] + [InlineData("invalid-model", true, typeof(InvalidOperationException), "FoundryLocal:AliasOrModel")] + public void Given_Invalid_Alias_With_DisableFoundryLocalManager_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? alias, bool disableFoundryLocalManager, Type expectedType, string expectedMessage) + { + // Arrange + var settings = BuildAppSettings(aliasOrModel: alias, disableFoundryLocalManager: disableFoundryLocalManager); var connector = new FoundryLocalConnector(settings); // Act @@ -137,13 +165,36 @@ public void Given_Null_FoundryLocalSettings_When_GetChatClient_Invoked_Then_It_S .Message.ShouldContain("Object reference not set to an instance of an object."); } + [Trait("Category", "IntegrationTest")] + [Trait("Category", "LLMRequired")] + [Trait("Category", "IgnoreGitHubActions")] + [Theory] + [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] + [InlineData("", typeof(UriFormatException), "Invalid URI:")] + [InlineData(" ", typeof(UriFormatException), "Invalid URI:")] + [InlineData("\t\r\n", typeof(UriFormatException), "Invalid URI:")] + [InlineData("invalid-uri-format", typeof(UriFormatException), "Invalid URI:")] + public void Given_Invalid_BaseUrl_When_GetChatClient_Invoked_Then_It_Should_Throw(string? baseUrl, Type expected, string message) + { + // Arrange + var settings = BuildAppSettings(baseUrl: baseUrl, disableFoundryLocalManager: true); + var connector = new FoundryLocalConnector(settings); + + // Act + Func func = async () => await connector.GetChatClientAsync(); + + // Assert + func.ShouldThrow(expected) + .Message.ShouldContain(message); + } + [Trait("Category", "UnitTest")] [Theory] [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] public void Given_Null_Alias_When_GetChatClient_Invoked_Then_It_Should_Throw(string? alias, Type expected, string message) { // Arrange - var settings = BuildAppSettings(alias: alias); + var settings = BuildAppSettings(aliasOrModel: alias); var connector = new FoundryLocalConnector(settings); // Act @@ -166,7 +217,7 @@ public void Given_Null_Alias_When_GetChatClient_Invoked_Then_It_Should_Throw(str public void Given_Invalid_Alias_When_GetChatClient_Invoked_Then_It_Should_Throw(string? alias, Type expected, string message) { // Arrange - var settings = BuildAppSettings(alias: alias); + var settings = BuildAppSettings(aliasOrModel: alias, disableFoundryLocalManager: false); var connector = new FoundryLocalConnector(settings); // Act @@ -197,11 +248,18 @@ public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] - [InlineData("", typeof(InvalidOperationException), "Missing configuration: FoundryLocal:Alias")] - [InlineData(" ", typeof(InvalidOperationException), "Missing configuration: FoundryLocal:Alias")] - [InlineData("\t\r\n", typeof(InvalidOperationException), "Missing configuration: FoundryLocal:Alias")] - public void Given_Invalid_Alias_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw(string? alias, Type expected, string expectedMessage) + [InlineData(null, null, false, typeof(NullReferenceException), "Object reference not set to an instance of an object.")] + [InlineData(BaseUrl, null, false, typeof(NullReferenceException), "Object reference not set to an instance of an object.")] + [InlineData(BaseUrl, "", false, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:AliasOrModel")] + [InlineData(BaseUrl, " ", false, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:AliasOrModel")] + [InlineData(BaseUrl, "\t\r\n", false, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:AliasOrModel")] + [InlineData(null, null, true, typeof(NullReferenceException), "Object reference not set to an instance of an object.")] + [InlineData(null, AliasOrModel, true, typeof(NullReferenceException), "Object reference not set to an instance of an object.")] + [InlineData("", AliasOrModel, true, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:BaseUrl")] + [InlineData(" ", AliasOrModel, true, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:BaseUrl")] + [InlineData("\t\r\n", AliasOrModel, true, typeof(InvalidOperationException), "Missing configuration: FoundryLocal:BaseUrl")] + public void Given_Invalid_Alias_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw( + string? baseUrl, string? alias, bool disableFoundryLocalManager, Type expected, string expectedMessage) { // Arrange var settings = new AppSettings @@ -209,7 +267,9 @@ public void Given_Invalid_Alias_When_CreateChatClientAsync_Invoked_Then_It_Shoul ConnectorType = ConnectorType.FoundryLocal, FoundryLocal = new FoundryLocalSettings { - Alias = alias + BaseUrl = baseUrl, + AliasOrModel = alias, + DisableFoundryLocalManager = disableFoundryLocalManager } }; diff --git a/test/OpenChat.PlaygroundApp.Tests/Options/FoundryLocalArgumentOptionsTests.cs b/test/OpenChat.PlaygroundApp.Tests/Options/FoundryLocalArgumentOptionsTests.cs index 4dced43c..4ef46f2c 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Options/FoundryLocalArgumentOptionsTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Options/FoundryLocalArgumentOptionsTests.cs @@ -9,11 +9,11 @@ namespace OpenChat.PlaygroundApp.Tests.Options; public class FoundryLocalArgumentOptionsTests { - private const string Alias = "test-foundry-local-alias"; - private const string AliasConfigKey = "FoundryLocal:Alias"; + private const string AliasOrModel = "test-foundry-local-alias"; + private const string AliasConfigKey = "FoundryLocal:AliasOrModel"; private static IConfiguration BuildConfigWithFoundryLocal( - string? configAlias = Alias, + string? configAlias = AliasOrModel, string? envAlias = null) { // Base configuration values (lowest priority) @@ -73,7 +73,7 @@ public void Given_Nothing_When_Parse_Invoked_Then_It_Should_Set_Config() // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(Alias); + settings.FoundryLocal.AliasOrModel.ShouldBe(AliasOrModel); } [Trait("Category", "UnitTest")] @@ -93,7 +93,7 @@ public void Given_CLI_Alias_When_Parse_Invoked_Then_It_Should_Use_CLI_Alias(stri // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(cliAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(cliAlias); } [Trait("Category", "UnitTest")] @@ -110,7 +110,7 @@ public void Given_CLI_ArgumentWithoutValue_When_Parse_Invoked_Then_It_Should_Use // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(Alias); + settings.FoundryLocal.AliasOrModel.ShouldBe(AliasOrModel); } [Trait("Category", "UnitTest")] @@ -126,7 +126,7 @@ public void Given_Unrelated_CLI_Arguments_When_Parse_Invoked_Then_It_Should_Use_ // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(Alias); + settings.FoundryLocal.AliasOrModel.ShouldBe(AliasOrModel); } [Trait("Category", "UnitTest")] @@ -146,7 +146,7 @@ public void Given_FoundryLocal_With_AliasName_StartingWith_Dashes_When_Parse_Inv // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(alias); + settings.FoundryLocal.AliasOrModel.ShouldBe(alias); } [Trait("Category", "UnitTest")] @@ -163,7 +163,7 @@ public void Given_ConfigValues_And_No_CLI_When_Parse_Invoked_Then_It_Should_Use_ // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(configAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(configAlias); } [Trait("Category", "UnitTest")] @@ -184,7 +184,7 @@ public void Given_ConfigValues_And_CLI_When_Parse_Invoked_Then_It_Should_Use_CLI // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(cliAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(cliAlias); } [Trait("Category", "UnitTest")] @@ -203,7 +203,7 @@ public void Given_EnvironmentVariables_And_No_Config_When_Parse_Invoked_Then_It_ // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(envAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(envAlias); } [Trait("Category", "UnitTest")] @@ -220,7 +220,7 @@ public void Given_ConfigValues_And_EnvironmentVariables_When_Parse_Invoked_Then_ // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(envAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(envAlias); } [Trait("Category", "UnitTest")] @@ -241,7 +241,7 @@ public void Given_ConfigValues_And_EnvironmentVariables_And_CLI_When_Parse_Invok // Assert settings.FoundryLocal.ShouldNotBeNull(); - settings.FoundryLocal.Alias.ShouldBe(cliAlias); + settings.FoundryLocal.AliasOrModel.ShouldBe(cliAlias); }