From 448aa7fec7475775716b67157c21effa41ab67e4 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Tue, 16 Sep 2025 19:52:32 +0900 Subject: [PATCH 01/11] feat: Add GoogleVertexAIConnector with settings validation and stubbed chat client --- .../Connectors/GoogleVertexAIConnector.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs diff --git a/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs new file mode 100644 index 00000000..934c658b --- /dev/null +++ b/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs @@ -0,0 +1,50 @@ +using System.ClientModel; + +using Microsoft.Extensions.AI; + +using OpenChat.PlaygroundApp.Abstractions; +using OpenChat.PlaygroundApp.Configurations; + +namespace OpenChat.PlaygroundApp.Connectors; + +/// +/// This represents the connector entity for Google Vertex AI. +/// +public class GoogleVertexAIConnector(AppSettings settings) : LanguageModelConnector(settings.GoogleVertexAI) +{ + /// + public override bool EnsureLanguageModelSettingsValid() + { + var settings = this.Settings as GoogleVertexAISettings; + if (settings is null) + { + throw new InvalidOperationException("Missing configuration: GoogleVertexAI."); + } + + if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()) == true) + { + throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey."); + } + + if (string.IsNullOrWhiteSpace(settings.Model?.Trim()) == true) + { + throw new InvalidOperationException("Missing configuration: GoogleVertexAI:Model."); + } + + return true; + } + + /// + public override async Task GetChatClientAsync() + { + var settings = this.Settings as GoogleVertexAISettings; + + // TODO: Replace with actual Google Vertex AI client implementation + // var credential = new ApiKeyCredential(settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey.")); + // var client = new GoogleVertexAIClient(credential); + // var chatClient = client.GetChatClient(settings.Model).AsIChatClient(); + // return await Task.FromResult(chatClient).ConfigureAwait(false); + + throw new NotImplementedException("Google Vertex AI client integration is not implemented yet."); + } +} From 389481a443bdbdd3a878914a8443d29f356636f8 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Wed, 17 Sep 2025 20:46:57 +0900 Subject: [PATCH 02/11] feat: Add GoogleVertexAIConnector to LanguageModelConnector factory --- .../Abstractions/LanguageModelConnector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs index 4fd14e5e..fa202705 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs @@ -38,6 +38,7 @@ public static async Task CreateChatClientAsync(AppSettings settings { ConnectorType.GitHubModels => new GitHubModelsConnector(settings), ConnectorType.OpenAI => new OpenAIConnector(settings), + ConnectorType.GoogleVertexAI => new GoogleVertexAIConnector(settings), _ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.") }; From 12d2bd604362774a2f14b8a0b67fd730fcbbacde Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Wed, 17 Sep 2025 21:09:36 +0900 Subject: [PATCH 03/11] feat: Add Google Vertex AI parameters and environment variables to infra bicep templates --- infra/main.bicep | 5 +++++ infra/main.parameters.json | 6 ++++++ infra/resources.bicep | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/infra/main.bicep b/infra/main.bicep index af45b7bb..f499fb69 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -19,6 +19,9 @@ param githubModelsModel string = '' @secure() param githubModelsToken string = '' // Google Vertex AI +param googleVertexAIModel string = '' +@secure() +param googleVertexAIApiKey string = '' // Docker Model Runner // Foundry Local // Hugging Face @@ -64,6 +67,8 @@ module resources 'resources.bicep' = { connectorType: connectorType githubModelsModel: githubModelsModel githubModelsToken: githubModelsToken + googleVertexAIModel: googleVertexAIModel + googleVertexAIApiKey: googleVertexAIApiKey openchatPlaygroundappExists: openchatPlaygroundappExists } } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 6747ca43..f767c78e 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -17,6 +17,12 @@ "githubModelsToken": { "value": "${GH_MODELS_TOKEN}" }, + "googleVertexAIModel": { + "value": "${GOOGLE_VERTEX_AI_MODEL}" + }, + "googleVertexAIApiKey": { + "value": "${GOOGLE_VERTEX_AI_API_KEY}" + }, "openchatPlaygroundappExists": { "value": "${SERVICE_OPENCHAT_PLAYGROUNDAPP_RESOURCE_EXISTS=false}" }, diff --git a/infra/resources.bicep b/infra/resources.bicep index 9e2c0b71..295b12cb 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -14,6 +14,9 @@ param githubModelsModel string = '' @secure() param githubModelsToken string = '' // Google Vertex AI +param googleVertexAIModel string = '' +@secure() +param googleVertexAIApiKey string = '' // Docker Model Runner // Foundry Local // Hugging Face @@ -114,6 +117,17 @@ var envGitHubModels = (connectorType == '' || connectorType == 'GitHubModels') ? } ]) : [] // Google Vertex AI +var envGoogleVertexAI = (connectorType == '' || connectorType == 'GoogleVertexAI') ? concat(googleVertexAIModel != '' ? [ + { + name: 'GoogleVertexAI__Model' + value: googleVertexAIModel + } +] : [], googleVertexAIApiKey != '' ? [ + { + name: 'GoogleVertexAI__ApiKey' + secretRef: 'google-vertex-ai-api-key' + } +] : []) : [] // Docker Model Runner // Foundry Local // Hugging Face From 0be2a59ab57d486c26c3ef739e642a2faa437422 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Tue, 23 Sep 2025 20:10:35 +0900 Subject: [PATCH 04/11] feat: Implement GoogleVertexAIConnector using GeminiChatClient --- .../Connectors/GoogleVertexAIConnector.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs index 934c658b..4cda22ec 100644 --- a/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs +++ b/src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.AI; +using Mscc.GenerativeAI.Microsoft; + using OpenChat.PlaygroundApp.Abstractions; using OpenChat.PlaygroundApp.Configurations; @@ -39,12 +41,11 @@ public override async Task GetChatClientAsync() { var settings = this.Settings as GoogleVertexAISettings; - // TODO: Replace with actual Google Vertex AI client implementation - // var credential = new ApiKeyCredential(settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey.")); - // var client = new GoogleVertexAIClient(credential); - // var chatClient = client.GetChatClient(settings.Model).AsIChatClient(); - // return await Task.FromResult(chatClient).ConfigureAwait(false); + var apiKey = settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey."); + var model = settings?.Model ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:Model."); + + var chatClient = new GeminiChatClient(apiKey, model); - throw new NotImplementedException("Google Vertex AI client integration is not implemented yet."); + return await Task.FromResult(chatClient).ConfigureAwait(false); } } From 4c28c4c50442141ae2acb61c60d3144e6fb03b44 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Tue, 23 Sep 2025 20:18:07 +0900 Subject: [PATCH 05/11] test: Add unit tests for GoogleVertexAIConnector --- .../GoogleVertexAIConnectorTests.cs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs new file mode 100644 index 00000000..d7c844af --- /dev/null +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs @@ -0,0 +1,126 @@ +using OpenChat.PlaygroundApp.Configurations; +using OpenChat.PlaygroundApp.Connectors; + +namespace OpenChat.PlaygroundApp.Tests.Connectors; + +public class GoogleVertexAIConnectorTests +{ + private static AppSettings BuildAppSettings(string? apiKey = "test-api-key", string? model = "test-model") + { + return new AppSettings + { + ConnectorType = ConnectorType.GoogleVertexAI, + GoogleVertexAI = new GoogleVertexAISettings + { + ApiKey = apiKey, + Model = model + } + }; + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw() + { + // Arrange + var appSettings = new AppSettings { ConnectorType = ConnectorType.GoogleVertexAI, GoogleVertexAI = null }; + var connector = new GoogleVertexAIConnector(appSettings); + + // Act + var ex = Assert.Throws(() => connector.EnsureLanguageModelSettingsValid()); + + // Assert + ex.Message.ShouldContain("GoogleVertexAI"); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of object")] + [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] + [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] + public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedType, string expectedMessage) + { + // Arrange + var appSettings = BuildAppSettings(apiKey: apiKey); + var connector = new GoogleVertexAIConnector(appSettings); + + // Act + var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); + + // Assert + ex.Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of object")] + [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:Model")] + [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:Model")] + public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage) + { + // Arrange + var appSettings = BuildAppSettings(model: model); + var connector = new GoogleVertexAIConnector(appSettings); + + // Act + var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); + + // Assert + ex.Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True() + { + // Arrange + var appSettings = BuildAppSettings(); + var connector = new GoogleVertexAIConnector(appSettings); + + // Act + var result = connector.EnsureLanguageModelSettingsValid(); + + // Assert + result.ShouldBeTrue(); + } + + [Trait("Category", "UnitTest")] + [Fact] + public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should_Return_ChatClient() + { + var settings = BuildAppSettings(); + var connector = new GoogleVertexAIConnector(settings); + + var client = await connector.GetChatClientAsync(); + + client.ShouldNotBeNull(); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] + [InlineData("", typeof(ArgumentException), "apiKey")] + public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should_Throw(string? apiKey, Type expected, string message) + { + var settings = BuildAppSettings(apiKey: apiKey); + var connector = new GoogleVertexAIConnector(settings); + + var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync); + + ex.Message.ShouldContain(message); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(ArgumentNullException), "model")] + [InlineData("", typeof(ArgumentException), "model")] + public async Task Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message) + { + var settings = BuildAppSettings(model: model); + var connector = new GoogleVertexAIConnector(settings); + + var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync); + + ex.Message.ShouldContain(message); + } +} From 319f6beb0225bbc0ab919dbd2892717f9af763d5 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Tue, 23 Sep 2025 20:40:05 +0900 Subject: [PATCH 06/11] test: Fix failing tests for GoogleVertexAIConnector when model is missing --- .../Connectors/GoogleVertexAIConnectorTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs index d7c844af..b6d7bf3d 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs @@ -5,7 +5,7 @@ namespace OpenChat.PlaygroundApp.Tests.Connectors; public class GoogleVertexAIConnectorTests { - private static AppSettings BuildAppSettings(string? apiKey = "test-api-key", string? model = "test-model") + private static AppSettings BuildAppSettings(string? apiKey = "AIzaSyA1234567890abcdefgHIJKLMNOpqrstuv", string? model = "test-model") { return new AppSettings { @@ -35,7 +35,7 @@ public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of object")] + [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedType, string expectedMessage) @@ -53,7 +53,7 @@ public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_T [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of object")] + [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:Model")] [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:Model")] [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:Model")] public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage) @@ -99,7 +99,6 @@ public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should [Trait("Category", "UnitTest")] [Theory] [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] - [InlineData("", typeof(ArgumentException), "apiKey")] public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should_Throw(string? apiKey, Type expected, string message) { var settings = BuildAppSettings(apiKey: apiKey); @@ -112,8 +111,7 @@ public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(ArgumentNullException), "model")] - [InlineData("", typeof(ArgumentException), "model")] + [InlineData(null, typeof(InvalidOperationException), "model")] public async Task Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message) { var settings = BuildAppSettings(model: model); From 2dff61a7b99839d8f2c4b38da73f80628564bf28 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Fri, 26 Sep 2025 23:27:02 +0900 Subject: [PATCH 07/11] refactor: Replace magic strings with private const string in GoogleVertexAIConnectorTests --- .../Abstractions/LanguageModelConnector.cs | 2 +- .../Connectors/GoogleVertexAIConnectorTests.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs index d8d6212f..1f07eb72 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs @@ -37,9 +37,9 @@ public static async Task CreateChatClientAsync(AppSettings settings LanguageModelConnector connector = settings.ConnectorType switch { ConnectorType.GitHubModels => new GitHubModelsConnector(settings), + ConnectorType.GoogleVertexAI => new GoogleVertexAIConnector(settings), ConnectorType.HuggingFace => new HuggingFaceConnector(settings), ConnectorType.OpenAI => new OpenAIConnector(settings), - ConnectorType.GoogleVertexAI => new GoogleVertexAIConnector(settings), _ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.") }; diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs index b6d7bf3d..a5111c30 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs @@ -5,7 +5,9 @@ namespace OpenChat.PlaygroundApp.Tests.Connectors; public class GoogleVertexAIConnectorTests { - private static AppSettings BuildAppSettings(string? apiKey = "AIzaSyA1234567890abcdefgHIJKLMNOpqrstuv", string? model = "test-model") + private const string ApiKey = "AIzaSyA1234567890abcdefgHIJKLMNOpqrstuv"; + private const string Model = "test-model"; + private static AppSettings BuildAppSettings(string? apiKey = ApiKey, string? model = Model) { return new AppSettings { From d2e7f6d62ce246c8173db8486bf2a77fffc8ce6e Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Thu, 9 Oct 2025 00:43:48 +0900 Subject: [PATCH 08/11] test: Add new test cases to GoogleVertexAIConnectorTests --- .../GoogleVertexAIConnectorTests.cs | 89 ++++++++++++++++--- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs index a5111c30..c6ed261e 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.AI; + +using OpenChat.PlaygroundApp.Abstractions; using OpenChat.PlaygroundApp.Configurations; using OpenChat.PlaygroundApp.Connectors; @@ -29,10 +32,11 @@ public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked var connector = new GoogleVertexAIConnector(appSettings); // Act - var ex = Assert.Throws(() => connector.EnsureLanguageModelSettingsValid()); + Action action = () => connector.EnsureLanguageModelSettingsValid(); // Assert - ex.Message.ShouldContain("GoogleVertexAI"); + action.ShouldThrow() + .Message.ShouldContain("GoogleVertexAI"); } [Trait("Category", "UnitTest")] @@ -47,10 +51,11 @@ public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_T var connector = new GoogleVertexAIConnector(appSettings); // Act - var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); + Action action = () => connector.EnsureLanguageModelSettingsValid(); // Assert - ex.Message.ShouldContain(expectedMessage); + action.ShouldThrow() + .Message.ShouldContain(expectedMessage); } [Trait("Category", "UnitTest")] @@ -61,14 +66,15 @@ public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_T public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage) { // Arrange - var appSettings = BuildAppSettings(model: model); + var appSettings = BuildAppSettings(apiKey: "valid-key", model: model); var connector = new GoogleVertexAIConnector(appSettings); // Act - var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); + Action action = () => connector.EnsureLanguageModelSettingsValid(); // Assert - ex.Message.ShouldContain(expectedMessage); + action.ShouldThrow() + .Message.ShouldContain(expectedMessage); } [Trait("Category", "UnitTest")] @@ -98,17 +104,37 @@ public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should client.ShouldNotBeNull(); } + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Should_Throw() + { + // Arrange + var appSettings = new AppSettings { ConnectorType = ConnectorType.GoogleVertexAI, GoogleVertexAI = null }; + var connector = new GoogleVertexAIConnector(appSettings); + + // Act + Func func = async () => await connector.GetChatClientAsync(); + + // Assert + func.ShouldThrow(); + } + [Trait("Category", "UnitTest")] [Theory] [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] + [InlineData("", typeof(ArgumentException), "key")] public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should_Throw(string? apiKey, Type expected, string message) { + // Arrange var settings = BuildAppSettings(apiKey: apiKey); var connector = new GoogleVertexAIConnector(settings); - var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync); + // Act + Func func = async () => await connector.GetChatClientAsync(); - ex.Message.ShouldContain(message); + // Assert + func.ShouldThrow(expected) + .Message.ShouldContain(message); } [Trait("Category", "UnitTest")] @@ -116,11 +142,52 @@ public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should [InlineData(null, typeof(InvalidOperationException), "model")] public async Task Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message) { + // Arrange var settings = BuildAppSettings(model: model); var connector = new GoogleVertexAIConnector(settings); - var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync); + // Act + Func func = async () => await connector.GetChatClientAsync(); - ex.Message.ShouldContain(message); + // Assert + func.ShouldThrow(expected) + .Message.ShouldContain(message); + } + + [Trait("Category", "UnitTest")] + [Fact] + public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Return_ChatClient() + { + // Arrange + var settings = BuildAppSettings(); + + // Act + var result = await LanguageModelConnector.CreateChatClientAsync(settings); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeAssignableTo(); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Invalid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw() + { + // Arrange + var settings = new AppSettings + { + ConnectorType = ConnectorType.GoogleVertexAI, + GoogleVertexAI = new GoogleVertexAISettings + { + ApiKey = null, + Model = "test-model" + } + }; + + // Act + Func func = async () => await LanguageModelConnector.CreateChatClientAsync(settings); + + // Assert + func.ShouldThrow(); } } From 148ba3329d6003654a74fc921130abbaf10a6195 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Fri, 10 Oct 2025 17:00:31 +0900 Subject: [PATCH 09/11] test: Enable GoogleVertexAIConnector inheritance check in LanguageModelConnectorTests --- .../Abstractions/LanguageModelConnectorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs index a23e9ee5..52249ec0 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs @@ -58,7 +58,7 @@ public async Task Given_Unsupported_ConnectorType_When_CreateChatClient_Invoked_ // [InlineData(typeof(AmazonBedrockConnector))] // [InlineData(typeof(AzureAIFoundryConnector))] [InlineData(typeof(GitHubModelsConnector))] - // [InlineData(typeof(GoogleVertexAIConnector))] + [InlineData(typeof(GoogleVertexAIConnector))] // [InlineData(typeof(DockerModelRunnerConnector))] // [InlineData(typeof(FoundryLocalConnector))] // [InlineData(typeof(HuggingFaceConnector))] From 282cc6f31877f6b859482a1da9fca8476e362a93 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Mon, 13 Oct 2025 21:27:23 +0900 Subject: [PATCH 10/11] refactor: Remove unnecessary function parameter --- .../Connectors/GoogleVertexAIConnectorTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs index c6ed261e..df22be4d 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs @@ -41,10 +41,10 @@ public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] - [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] - [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")] - public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedType, string expectedMessage) + [InlineData(null, "GoogleVertexAI:ApiKey")] + [InlineData("", "GoogleVertexAI:ApiKey")] + [InlineData(" ", "GoogleVertexAI:ApiKey")] + public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, string expectedMessage) { // Arrange var appSettings = BuildAppSettings(apiKey: apiKey); @@ -60,10 +60,10 @@ public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_T [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:Model")] - [InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:Model")] - [InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:Model")] - public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage) + [InlineData(null, "GoogleVertexAI:Model")] + [InlineData("", "GoogleVertexAI:Model")] + [InlineData(" ", "GoogleVertexAI:Model")] + public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, string expectedMessage) { // Arrange var appSettings = BuildAppSettings(apiKey: "valid-key", model: model); From e87a2186a8bbb851a64910af7fe731c406ab0ef3 Mon Sep 17 00:00:00 2001 From: hxcva1 Date: Wed, 15 Oct 2025 21:32:11 +0900 Subject: [PATCH 11/11] docs: Add Google Vertex AI (draft) --- docs/google-vertex-ai.md | 179 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 docs/google-vertex-ai.md diff --git a/docs/google-vertex-ai.md b/docs/google-vertex-ai.md new file mode 100644 index 00000000..b900ff9b --- /dev/null +++ b/docs/google-vertex-ai.md @@ -0,0 +1,179 @@ +# OpenChat Playground with Google Vertex AI + +This page describes how to run OpenChat Playground (OCP) with Google Vertex AI integration. + +## Get the repository root + +1. Get the repository root. + +```bash +# bash/zsh +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +``` + +```powershell +# PowerShell +$REPOSITORY_ROOT = git rev-parse --show-toplevel +``` + +## Run on local machine + +1. Make sure you are at the repository root. + +```bash +cd $REPOSITORY_ROOT +``` + +1. Add Google Vertex AI API Key. Replace `{{GOOGLE_VERTEX_AI_API_KEY}}` with your key. + +```bash +# bash/zsh +dotnet user-secrets --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp \ + set GoogleVertexAI:ApiKey "{{GOOGLE_VERTEX_AI_API_KEY}}" +``` + +```powershell +# PowerShell +dotnet user-secrets --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp ` + set GoogleVertexAI:ApiKey "{{GOOGLE_VERTEX_AI_API_KEY}}" +``` + +> Note: code and tests in this repository use the config keys `GoogleVertexAI:ApiKey` and `GoogleVertexAI:Model`. + +1. Run the app with the `GoogleVertexAI` connector. Optionally pass `--model` to override the configured model/deployment name: + +```bash +# bash/zsh +dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- \ + --connector-type GoogleVertexAI \ + --api-key $GOOGLE_VERTEX_AI_API_KEY \ + --model your-vertex-model-name +``` + +```powershell +# PowerShell +dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- ` + --connector-type GoogleVertexAI ` + --api-key $env:GOOGLE_VERTEX_AI_API_KEY ` + --model your-vertex-model-name +``` + +1. Open your web browser at `http://localhost:5280` and start entering prompts. + +## Run in local container + +1. Make sure you are at the repository root. + +```bash +cd $REPOSITORY_ROOT +``` + +1. Build a container image. + +```bash +docker build -f Dockerfile -t openchat-playground:latest . +``` + +1. Get the API key from user secrets (example): + +```bash +API_KEY=$(dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | \ + sed -n '/^\/\//d; p' | jq -r '."GoogleVertexAI:ApiKey"') +``` + +1. Run the app with the built image, passing the connector flags: + +```bash +# bash/zsh - from locally built container +docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type GoogleVertexAI \ + --api-key $API_KEY \ + --model your-vertex-model-name +``` + +```powershell +# PowerShell - from locally built container +docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type GoogleVertexAI ` + --api-key $API_KEY ` + --model your-vertex-model-name +``` + +You can also run a published image (for example from a registry) the same way, passing the flags above. + +## Run on Azure + +1. Make sure you are at the repository root. + +```bash +cd $REPOSITORY_ROOT +``` + +1. Login to Azure: + +```bash +azd auth login +``` + +1. Initialize `azd` template if needed: + +```bash +azd init +``` + +1. Configure azd environment variables. Example: read the API key from user-secrets and set it in `azd`: + +```bash +API_KEY=$(dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | \ + sed -n '/^\/\//d; p' | jq -r '."GoogleVertexAI:ApiKey"') + +azd env set GOOGLE_VERTEX_AI_API_KEY $API_KEY +# optionally set model +azd env set GOOGLE_VERTEX_AI_MODEL your-vertex-model-name + +# set connector type +azd env set CONNECTOR_TYPE GoogleVertexAI +``` + +1. Provision and deploy: + +```bash +azd up +``` + +1. Clean up: + +```bash +azd down --force --purge +``` + +## Notes & troubleshooting + +- Config keys used by the project and tests: + - `GoogleVertexAI:ApiKey` (app settings / user-secrets) + - `GoogleVertexAI:Model` (app settings) + - infra uses environment variable names `GOOGLE_VERTEX_AI_API_KEY` and `GOOGLE_VERTEX_AI_MODEL` in `main.parameters.json` and Bicep templates. + +- Helper package referenced in the repo: `Mscc.GenerativeAI.Microsoft` — follow that package's docs if you choose to use it for client initialization. Otherwise you can call Vertex AI via Google Cloud SDKs (REST/gRPC) or custom HTTP calls. + +- Common issues: + - Ensure `GoogleVertexAI:ApiKey` is set and non-empty. + - Ensure `GoogleVertexAI:Model` is the correct model/deployment identifier. + - When running in container or on Azure, confirm secrets are wired to container environment variables (Bicep templates expose them as `GoogleVertexAI__ApiKey`). + +## Tests + +To run only tests related to Vertex AI you can filter by class/name: + +```bash +dotnet test --filter "GoogleVertexAI" +``` + +Or run UnitTests only: + +```bash +dotnet test --filter Category=UnitTest +``` + +## References + +- Mscc.GenerativeAI.Microsoft (NuGet) — helper/adapter libraries (if used in your project) +- Google Vertex AI docs — model names, endpoints and authentication