Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,6 +71,8 @@ module resources 'resources.bicep' = {
connectorType: connectorType
githubModelsModel: githubModelsModel
githubModelsToken: githubModelsToken
googleVertexAIModel: googleVertexAIModel
googleVertexAIApiKey: googleVertexAIApiKey
huggingFaceModel: huggingFaceModel
openAIModel: openAIModel
openAIApiKey: openAIApiKey
Expand Down
6 changes: 6 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"githubModelsToken": {
"value": "${GH_MODELS_TOKEN}"
},
"googleVertexAIModel": {
"value": "${GOOGLE_VERTEX_AI_MODEL}"
},
"googleVertexAIApiKey": {
"value": "${GOOGLE_VERTEX_AI_API_KEY}"
},
"huggingFaceModel": {
"value": "${HUGGING_FACE_MODEL}"
},
Expand Down
14 changes: 14 additions & 0 deletions infra/resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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
Expand Down Expand Up @@ -120,6 +123,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static async Task<IChatClient> 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),
_ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.")
Expand Down
51 changes: 51 additions & 0 deletions src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.ClientModel;

using Microsoft.Extensions.AI;

using Mscc.GenerativeAI.Microsoft;

using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;

namespace OpenChat.PlaygroundApp.Connectors;

/// <summary>
/// This represents the connector entity for Google Vertex AI.
/// </summary>
public class GoogleVertexAIConnector(AppSettings settings) : LanguageModelConnector(settings.GoogleVertexAI)
{
/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public override async Task<IChatClient> GetChatClientAsync()
{
var settings = this.Settings as GoogleVertexAISettings;

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);

return await Task.FromResult(chatClient).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using Microsoft.Extensions.AI;

using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;
using OpenChat.PlaygroundApp.Connectors;

namespace OpenChat.PlaygroundApp.Tests.Connectors;

public class GoogleVertexAIConnectorTests
{
private const string ApiKey = "AIzaSyA1234567890abcdefgHIJKLMNOpqrstuv";
private const string Model = "test-model";
private static AppSettings BuildAppSettings(string? apiKey = ApiKey, string? model = 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
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain("GoogleVertexAI");
}

[Trait("Category", "UnitTest")]
[Theory]
[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);
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Theory]
[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);
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.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")]
[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<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow<InvalidOperationException>();
}

[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)

Check warning on line 126 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 126 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
// Arrange
var settings = BuildAppSettings(apiKey: apiKey);
var connector = new GoogleVertexAIConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "model")]
public async Task Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message)

Check warning on line 143 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 143 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
// Arrange
var settings = BuildAppSettings(model: model);
var connector = new GoogleVertexAIConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// 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<IChatClient>();
}

[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<Task> func = async () => await LanguageModelConnector.CreateChatClientAsync(settings);

// Assert
func.ShouldThrow<InvalidOperationException>();
}
}
Loading