From f0f29004826f1ba30a33dd19e1172c27c466922a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 12:16:59 +0000
Subject: [PATCH 1/7] Initial plan
From 2192206e16b2cd6e04fdf8149a31de4fe97f4ef6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 12:38:45 +0000
Subject: [PATCH 2/7] Add Google.GenAI ChatClient support with IChatClient
polyfill and extension methods
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
---
dotnet/Directory.Packages.props | 1 +
.../Connectors.Google.UnitTests.csproj | 3 +-
...oogleAIServiceCollectionExtensionsTests.cs | 104 ++++
.../Connectors.Google.csproj | 4 +
.../GoogleAIKernelBuilderExtensions.cs | 65 +++
...ollectionExtensions.DependencyInjection.cs | 97 ++++
.../Services/GoogleGenAIChatClient.cs | 508 ++++++++++++++++++
7 files changed, 781 insertions(+), 1 deletion(-)
create mode 100644 dotnet/src/Connectors/Connectors.Google/Services/GoogleGenAIChatClient.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 3c5217f13345..5e7208d61702 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -35,6 +35,7 @@
+
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj b/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
index 4a0ae4032f3e..1b546a8f4db5 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
@@ -8,7 +8,7 @@
enable
disable
false
- $(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050
+ $(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050,SKEXP0070
@@ -25,6 +25,7 @@
+
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
index 844a2341bbc9..6caded591225 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
@@ -113,4 +114,107 @@ public void GoogleAIEmbeddingGeneratorShouldBeRegisteredInServiceCollection()
Assert.NotNull(embeddingsGenerationService);
Assert.IsType(embeddingsGenerationService);
}
+
+#if NET
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act
+ kernelBuilder.AddGoogleAIChatClient("modelId", "apiKey");
+ var kernel = kernelBuilder.Build();
+
+ // Assert
+ var chatClient = kernel.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddGoogleAIChatClient("modelId", "apiKey");
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var chatClient = serviceProvider.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithClient()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+ var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+
+ // Act
+ kernelBuilder.AddGoogleAIChatClient("modelId", googleClient);
+ var kernel = kernelBuilder.Build();
+
+ // Assert
+ var chatClient = kernel.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithClient()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+
+ // Act
+ services.AddGoogleAIChatClient("modelId", googleClient);
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var chatClient = serviceProvider.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldBeRegisteredWithServiceId()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ const string serviceId = "test-service-id";
+
+ // Act
+ services.AddGoogleAIChatClient("modelId", "apiKey", serviceId: serviceId);
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var chatClient = serviceProvider.GetKeyedService(serviceId);
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ [Experimental("SKEXP0070")]
+ public void GoogleAIChatClientShouldResolveFromServiceProviderWhenClientNotProvided()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+ services.AddSingleton(googleClient);
+
+ // Act
+ services.AddGoogleAIChatClient("modelId");
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var chatClient = serviceProvider.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+#endif
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
index e71d80d17a00..760f6322fcc6 100644
--- a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
+++ b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
@@ -24,6 +24,10 @@
+
+
+
+
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
index d6ab3768d0e0..3443f5b549db 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -118,4 +119,68 @@ public static IKernelBuilder AddGoogleAIEmbeddingGenerator(
dimensions: dimensions);
return builder;
}
+
+#if NET
+ ///
+ /// Add Google AI to the .
+ ///
+ /// The kernel builder.
+ /// The model for chat completion.
+ /// The API key for authentication with the Google AI API.
+ /// The optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated kernel builder.
+ [Experimental("SKEXP0070")]
+ public static IKernelBuilder AddGoogleAIChatClient(
+ this IKernelBuilder builder,
+ string modelId,
+ string apiKey,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddGoogleAIChatClient(
+ modelId,
+ apiKey,
+ serviceId,
+ openTelemetrySourceName,
+ openTelemetryConfig);
+
+ return builder;
+ }
+
+ ///
+ /// Add Google AI to the .
+ ///
+ /// The kernel builder.
+ /// The model for chat completion.
+ /// The to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// The optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated kernel builder.
+ [Experimental("SKEXP0070")]
+ public static IKernelBuilder AddGoogleAIChatClient(
+ this IKernelBuilder builder,
+ string modelId,
+ Google.GenAI.Client? googleClient = null,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddGoogleAIChatClient(
+ modelId,
+ googleClient,
+ serviceId,
+ openTelemetrySourceName,
+ openTelemetryConfig);
+
+ return builder;
+ }
+#endif
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
index a45001278e9a..53af05b250a7 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
@@ -47,4 +49,99 @@ public static IServiceCollection AddGoogleAIEmbeddingGenerator(
loggerFactory: serviceProvider.GetService(),
dimensions: dimensions));
}
+
+#if NET
+ ///
+ /// Add Google AI to the specified service collection.
+ ///
+ /// The service collection to add the Google AI Chat Client to.
+ /// The model for chat completion.
+ /// The API key for authentication with the Google AI API.
+ /// Optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated service collection.
+ [Experimental("SKEXP0070")]
+ public static IServiceCollection AddGoogleAIChatClient(
+ this IServiceCollection services,
+ string modelId,
+ string apiKey,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(services);
+ Verify.NotNullOrWhiteSpace(modelId);
+ Verify.NotNullOrWhiteSpace(apiKey);
+
+ IChatClient Factory(IServiceProvider serviceProvider, object? _)
+ {
+ var loggerFactory = serviceProvider.GetService();
+
+ var googleClient = new Google.GenAI.Client(apiKey: apiKey);
+
+ var builder = new GoogleGenAIChatClient(googleClient, modelId)
+ .AsBuilder()
+ .UseKernelFunctionInvocation(loggerFactory)
+ .UseOpenTelemetry(loggerFactory, openTelemetrySourceName, openTelemetryConfig);
+
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder.Build();
+ }
+
+ services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return services;
+ }
+
+ ///
+ /// Add Google AI to the specified service collection.
+ ///
+ /// The service collection to add the Google AI Chat Client to.
+ /// The model for chat completion.
+ /// The to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// Optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated service collection.
+ [Experimental("SKEXP0070")]
+ public static IServiceCollection AddGoogleAIChatClient(
+ this IServiceCollection services,
+ string modelId,
+ Google.GenAI.Client? googleClient = null,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(services);
+ Verify.NotNullOrWhiteSpace(modelId);
+
+ IChatClient Factory(IServiceProvider serviceProvider, object? _)
+ {
+ var loggerFactory = serviceProvider.GetService();
+
+ var client = googleClient ?? serviceProvider.GetRequiredService();
+
+ var builder = new GoogleGenAIChatClient(client, modelId)
+ .AsBuilder()
+ .UseKernelFunctionInvocation(loggerFactory)
+ .UseOpenTelemetry(loggerFactory, openTelemetrySourceName, openTelemetryConfig);
+
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder.Build();
+ }
+
+ services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return services;
+ }
+#endif
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Services/GoogleGenAIChatClient.cs b/dotnet/src/Connectors/Connectors.Google/Services/GoogleGenAIChatClient.cs
new file mode 100644
index 000000000000..1a6fbceb408b
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google/Services/GoogleGenAIChatClient.cs
@@ -0,0 +1,508 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if NET
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.GenAI;
+using Google.GenAI.Types;
+using Microsoft.Extensions.AI;
+using AITextContent = Microsoft.Extensions.AI.TextContent;
+using AIDataContent = Microsoft.Extensions.AI.DataContent;
+using AIUriContent = Microsoft.Extensions.AI.UriContent;
+using AIFunctionCallContent = Microsoft.Extensions.AI.FunctionCallContent;
+using AIFunctionResultContent = Microsoft.Extensions.AI.FunctionResultContent;
+
+namespace Microsoft.SemanticKernel.Connectors.Google;
+
+///
+/// Provides an implementation based on Google.GenAI .
+///
+internal sealed class GoogleGenAIChatClient : IChatClient
+{
+ /// The wrapped instance (optional).
+ private readonly Client? _client;
+
+ /// The wrapped instance.
+ private readonly Models _models;
+
+ /// The default model that should be used when no override is specified.
+ private readonly string? _defaultModelId;
+
+ /// Lazily-initialized metadata describing the implementation.
+ private ChatClientMetadata? _metadata;
+
+ /// Initializes a new instance.
+ /// The to wrap.
+ /// The default model ID to use for chat requests if not specified.
+ public GoogleGenAIChatClient(Client client, string? defaultModelId)
+ {
+ Verify.NotNull(client);
+
+ this._client = client;
+ this._models = client.Models;
+ this._defaultModelId = defaultModelId;
+ }
+
+ /// Initializes a new instance.
+ /// The client to wrap.
+ /// The default model ID to use for chat requests if not specified.
+ public GoogleGenAIChatClient(Models models, string? defaultModelId)
+ {
+ Verify.NotNull(models);
+
+ this._models = models;
+ this._defaultModelId = defaultModelId;
+ }
+
+ ///
+ public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ // Create the request.
+ (string? modelId, List contents, GenerateContentConfig config) = this.CreateRequest(messages, options);
+
+ // Send it.
+ GenerateContentResponse generateResult = await this._models.GenerateContentAsync(modelId!, contents, config).ConfigureAwait(false);
+
+ // Create the response.
+ ChatResponse chatResponse = new(new ChatMessage(ChatRole.Assistant, new List()))
+ {
+ CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
+ ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
+ RawRepresentation = generateResult,
+ ResponseId = generateResult.ResponseId,
+ };
+
+ // Populate the response messages.
+ chatResponse.FinishReason = PopulateResponseContents(generateResult, chatResponse.Messages[0].Contents);
+
+ // Populate usage information if there is any.
+ if (generateResult.UsageMetadata is { } usageMetadata)
+ {
+ chatResponse.Usage = ExtractUsageDetails(usageMetadata);
+ }
+
+ // Return the response.
+ return chatResponse;
+ }
+
+ ///
+ public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ // Create the request.
+ (string? modelId, List contents, GenerateContentConfig config) = this.CreateRequest(messages, options);
+
+ // Send it, and process the results.
+ await foreach (GenerateContentResponse generateResult in this._models.GenerateContentStreamAsync(modelId!, contents, config).WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ // Create a response update for each result in the stream.
+ ChatResponseUpdate responseUpdate = new(ChatRole.Assistant, new List())
+ {
+ CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
+ ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
+ RawRepresentation = generateResult,
+ ResponseId = generateResult.ResponseId,
+ };
+
+ // Populate the response update contents.
+ responseUpdate.FinishReason = PopulateResponseContents(generateResult, responseUpdate.Contents);
+
+ // Populate usage information if there is any.
+ if (generateResult.UsageMetadata is { } usageMetadata)
+ {
+ responseUpdate.Contents.Add(new UsageContent(ExtractUsageDetails(usageMetadata)));
+ }
+
+ // Yield the update.
+ yield return responseUpdate;
+ }
+ }
+
+ ///
+ public object? GetService(System.Type serviceType, object? serviceKey = null)
+ {
+ Verify.NotNull(serviceType);
+
+ if (serviceKey is null)
+ {
+ // If there's a request for metadata, lazily-initialize it and return it.
+ if (serviceType == typeof(ChatClientMetadata))
+ {
+ return this._metadata ??= new("google.genai", new Uri("https://generativelanguage.googleapis.com/"), defaultModelId: this._defaultModelId);
+ }
+
+ // Allow a consumer to access the underlying client if they need it.
+ if (serviceType.IsInstanceOfType(this._models))
+ {
+ return this._models;
+ }
+
+ if (this._client is not null && serviceType.IsInstanceOfType(this._client))
+ {
+ return this._client;
+ }
+
+ if (serviceType.IsInstanceOfType(this))
+ {
+ return this;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ void IDisposable.Dispose() { /* nop */ }
+
+ /// Creates the message parameters for from and .
+ private (string? ModelId, List Contents, GenerateContentConfig Config) CreateRequest(IEnumerable messages, ChatOptions? options)
+ {
+ // Create the GenerateContentConfig object. If the options contains a RawRepresentationFactory, try to use it to
+ // create the request instance, allowing the caller to populate it with GenAI-specific options. Otherwise, create
+ // a new instance directly.
+ string? model = this._defaultModelId;
+ List contents = [];
+ GenerateContentConfig config = options?.RawRepresentationFactory?.Invoke(this) as GenerateContentConfig ?? new();
+
+ if (options is not null)
+ {
+ if (options.FrequencyPenalty is { } frequencyPenalty)
+ {
+ config.FrequencyPenalty ??= frequencyPenalty;
+ }
+
+ if (options.Instructions is { } instructions)
+ {
+ ((config.SystemInstruction ??= new()).Parts ??= []).Add(new() { Text = instructions });
+ }
+
+ if (options.MaxOutputTokens is { } maxOutputTokens)
+ {
+ config.MaxOutputTokens ??= maxOutputTokens;
+ }
+
+ if (!string.IsNullOrWhiteSpace(options.ModelId))
+ {
+ model = options.ModelId;
+ }
+
+ if (options.PresencePenalty is { } presencePenalty)
+ {
+ config.PresencePenalty ??= presencePenalty;
+ }
+
+ if (options.Seed is { } seed)
+ {
+ config.Seed ??= (int)seed;
+ }
+
+ if (options.StopSequences is { } stopSequences)
+ {
+ (config.StopSequences ??= []).AddRange(stopSequences);
+ }
+
+ if (options.Temperature is { } temperature)
+ {
+ config.Temperature ??= temperature;
+ }
+
+ if (options.TopP is { } topP)
+ {
+ config.TopP ??= topP;
+ }
+
+ if (options.TopK is { } topK)
+ {
+ config.TopK ??= topK;
+ }
+
+ // Populate tools. Each kind of tool is added on its own, except for function declarations,
+ // which are grouped into a single FunctionDeclaration.
+ List? functionDeclarations = null;
+ if (options.Tools is { } tools)
+ {
+ foreach (var tool in tools)
+ {
+ switch (tool)
+ {
+ case AIFunction af:
+ functionDeclarations ??= [];
+ functionDeclarations.Add(new()
+ {
+ Name = af.Name,
+ Description = af.Description ?? "",
+ });
+ break;
+ }
+ }
+ }
+
+ if (functionDeclarations is { Count: > 0 })
+ {
+ Tool functionTools = new();
+ (functionTools.FunctionDeclarations ??= []).AddRange(functionDeclarations);
+ (config.Tools ??= []).Add(functionTools);
+ }
+
+ // Transfer over the tool mode if there are any tools.
+ if (options.ToolMode is { } toolMode && config.Tools?.Count > 0)
+ {
+ switch (toolMode)
+ {
+ case NoneChatToolMode:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.NONE } };
+ break;
+
+ case AutoChatToolMode:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.AUTO } };
+ break;
+
+ case RequiredChatToolMode required:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.ANY } };
+ if (required.RequiredFunctionName is not null)
+ {
+ ((config.ToolConfig.FunctionCallingConfig ??= new()).AllowedFunctionNames ??= []).Add(required.RequiredFunctionName);
+ }
+ break;
+ }
+ }
+
+ // Set the response format if specified.
+ if (options.ResponseFormat is ChatResponseFormatJson responseFormat)
+ {
+ config.ResponseMimeType = "application/json";
+ if (responseFormat.Schema is { } schema)
+ {
+ config.ResponseJsonSchema = schema;
+ }
+ }
+ }
+
+ // Transfer messages to request, handling system messages specially
+ Dictionary? callIdToFunctionNames = null;
+ foreach (var message in messages)
+ {
+ if (message.Role == ChatRole.System)
+ {
+ string instruction = message.Text;
+ if (!string.IsNullOrWhiteSpace(instruction))
+ {
+ ((config.SystemInstruction ??= new()).Parts ??= []).Add(new() { Text = instruction });
+ }
+
+ continue;
+ }
+
+ Content content = new() { Role = message.Role == ChatRole.Assistant ? "model" : "user" };
+ content.Parts ??= [];
+ AddPartsForAIContents(ref callIdToFunctionNames, message.Contents, content.Parts);
+
+ contents.Add(content);
+ }
+
+ // Make sure the request contains at least one content part (the request would always fail if empty).
+ if (!contents.SelectMany(c => c.Parts ?? Enumerable.Empty()).Any())
+ {
+ contents.Add(new() { Role = "user", Parts = [new() { Text = "" }] });
+ }
+
+ return (model, contents, config);
+ }
+
+ /// Creates s for and adds them to .
+ private static void AddPartsForAIContents(ref Dictionary? callIdToFunctionNames, IList contents, List parts)
+ {
+ for (int i = 0; i < contents.Count; i++)
+ {
+ var content = contents[i];
+
+ Part? part = null;
+ switch (content)
+ {
+ case AITextContent textContent:
+ part = new() { Text = textContent.Text };
+ break;
+
+ case AIDataContent dataContent:
+ part = new()
+ {
+ InlineData = new()
+ {
+ MimeType = dataContent.MediaType,
+ Data = dataContent.Data.ToArray(),
+ }
+ };
+ break;
+
+ case AIUriContent uriContent:
+ part = new()
+ {
+ FileData = new()
+ {
+ FileUri = uriContent.Uri.AbsoluteUri,
+ MimeType = uriContent.MediaType,
+ }
+ };
+ break;
+
+ case AIFunctionCallContent functionCallContent:
+ (callIdToFunctionNames ??= [])[functionCallContent.CallId] = functionCallContent.Name;
+ callIdToFunctionNames[""] = functionCallContent.Name; // track last function name in case calls don't have IDs
+
+ part = new()
+ {
+ FunctionCall = new()
+ {
+ Id = functionCallContent.CallId,
+ Name = functionCallContent.Name,
+ Args = functionCallContent.Arguments is null ? null : functionCallContent.Arguments as Dictionary ?? new(functionCallContent.Arguments!),
+ }
+ };
+ break;
+
+ case AIFunctionResultContent functionResultContent:
+ part = new()
+ {
+ FunctionResponse = new()
+ {
+ Id = functionResultContent.CallId,
+ Name = callIdToFunctionNames?.TryGetValue(functionResultContent.CallId, out string? functionName) is true || callIdToFunctionNames?.TryGetValue("", out functionName) is true ?
+ functionName :
+ null,
+ Response = functionResultContent.Result is null ? null : new() { ["result"] = functionResultContent.Result },
+ }
+ };
+ break;
+ }
+
+ if (part is not null)
+ {
+ parts.Add(part);
+ }
+ }
+ }
+
+ /// Creates s for and adds them to .
+ private static void AddAIContentsForParts(List parts, IList contents)
+ {
+ foreach (var part in parts)
+ {
+ AIContent? content = null;
+
+ if (!string.IsNullOrEmpty(part.Text))
+ {
+ content = new AITextContent(part.Text);
+ }
+ else if (part.InlineData is { } inlineData)
+ {
+ content = new AIDataContent(inlineData.Data, inlineData.MimeType ?? "application/octet-stream");
+ }
+ else if (part.FileData is { FileUri: not null } fileData)
+ {
+ content = new AIUriContent(new Uri(fileData.FileUri), fileData.MimeType ?? "application/octet-stream");
+ }
+ else if (part.FunctionCall is { Name: not null } functionCall)
+ {
+ content = new AIFunctionCallContent(functionCall.Id ?? "", functionCall.Name, functionCall.Args!);
+ }
+ else if (part.FunctionResponse is { } functionResponse)
+ {
+ content = new AIFunctionResultContent(
+ functionResponse.Id ?? "",
+ functionResponse.Response?.TryGetValue("output", out var output) is true ? output :
+ functionResponse.Response?.TryGetValue("error", out var error) is true ? error :
+ null);
+ }
+
+ if (content is not null)
+ {
+ content.RawRepresentation = part;
+ contents.Add(content);
+ }
+ }
+ }
+
+ private static ChatFinishReason? PopulateResponseContents(GenerateContentResponse generateResult, IList responseContents)
+ {
+ ChatFinishReason? finishReason = null;
+
+ // Populate the response messages. There should only be at most one candidate, but if there are more, ignore all but the first.
+ if (generateResult.Candidates is { Count: > 0 } &&
+ generateResult.Candidates[0] is { Content: { } candidateContent } candidate)
+ {
+ // Grab the finish reason if one exists.
+ finishReason = ConvertFinishReason(candidate.FinishReason);
+
+ // Add all of the response content parts as AIContents.
+ if (candidateContent.Parts is { } parts)
+ {
+ AddAIContentsForParts(parts, responseContents);
+ }
+ }
+
+ // Populate error information if there is any.
+ if (generateResult.PromptFeedback is { } promptFeedback)
+ {
+ responseContents.Add(new ErrorContent(promptFeedback.BlockReasonMessage));
+ }
+
+ return finishReason;
+ }
+
+ /// Creates an M.E.AI from a Google .
+ private static ChatFinishReason? ConvertFinishReason(FinishReason? finishReason)
+ {
+ return finishReason switch
+ {
+ null => null,
+
+ FinishReason.MAX_TOKENS =>
+ ChatFinishReason.Length,
+
+ FinishReason.MALFORMED_FUNCTION_CALL or
+ FinishReason.UNEXPECTED_TOOL_CALL =>
+ ChatFinishReason.ToolCalls,
+
+ FinishReason.FINISH_REASON_UNSPECIFIED or
+ FinishReason.STOP =>
+ ChatFinishReason.Stop,
+
+ _ => ChatFinishReason.ContentFilter,
+ };
+ }
+
+ /// Creates a populated from the supplied .
+ private static UsageDetails ExtractUsageDetails(GenerateContentResponseUsageMetadata usageMetadata)
+ {
+ UsageDetails details = new()
+ {
+ InputTokenCount = usageMetadata.PromptTokenCount,
+ OutputTokenCount = usageMetadata.CandidatesTokenCount,
+ TotalTokenCount = usageMetadata.TotalTokenCount,
+ };
+
+ AddIfPresent(nameof(usageMetadata.CachedContentTokenCount), usageMetadata.CachedContentTokenCount);
+ AddIfPresent(nameof(usageMetadata.ThoughtsTokenCount), usageMetadata.ThoughtsTokenCount);
+ AddIfPresent(nameof(usageMetadata.ToolUsePromptTokenCount), usageMetadata.ToolUsePromptTokenCount);
+
+ return details;
+
+ void AddIfPresent(string key, int? value)
+ {
+ if (value is int i)
+ {
+ (details.AdditionalCounts ??= [])[key] = i;
+ }
+ }
+ }
+}
+
+#endif
From 1586f0e73daf42605bc96fbcc2876ec55a27ea23 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 15:49:00 +0000
Subject: [PATCH 3/7] Add vertexAI parameter to extension methods and update
Google.GenAI to 0.6.0
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
---
dotnet/Directory.Packages.props | 2 +-
.../src/Connectors/Connectors.Google/Connectors.Google.csproj | 2 +-
.../Extensions/GoogleAIKernelBuilderExtensions.cs | 3 +++
...GoogleAIServiceCollectionExtensions.DependencyInjection.cs | 4 +++-
4 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 5e7208d61702..2bf66842a8d8 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -35,7 +35,7 @@
-
+
diff --git a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
index 760f6322fcc6..9b7534700ee2 100644
--- a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
+++ b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
index 3443f5b549db..75ed1d304658 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
@@ -127,6 +127,7 @@ public static IKernelBuilder AddGoogleAIEmbeddingGenerator(
/// The kernel builder.
/// The model for chat completion.
/// The API key for authentication with the Google AI API.
+ /// Whether to use Vertex AI.
/// The optional service ID.
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
@@ -136,6 +137,7 @@ public static IKernelBuilder AddGoogleAIChatClient(
this IKernelBuilder builder,
string modelId,
string apiKey,
+ bool vertexAI = false,
string? serviceId = null,
string? openTelemetrySourceName = null,
Action? openTelemetryConfig = null)
@@ -145,6 +147,7 @@ public static IKernelBuilder AddGoogleAIChatClient(
builder.Services.AddGoogleAIChatClient(
modelId,
apiKey,
+ vertexAI,
serviceId,
openTelemetrySourceName,
openTelemetryConfig);
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
index 53af05b250a7..20bfd2118aa7 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
@@ -57,6 +57,7 @@ public static IServiceCollection AddGoogleAIEmbeddingGenerator(
/// The service collection to add the Google AI Chat Client to.
/// The model for chat completion.
/// The API key for authentication with the Google AI API.
+ /// Whether to use Vertex AI.
/// Optional service ID.
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
@@ -66,6 +67,7 @@ public static IServiceCollection AddGoogleAIChatClient(
this IServiceCollection services,
string modelId,
string apiKey,
+ bool vertexAI = false,
string? serviceId = null,
string? openTelemetrySourceName = null,
Action? openTelemetryConfig = null)
@@ -78,7 +80,7 @@ IChatClient Factory(IServiceProvider serviceProvider, object? _)
{
var loggerFactory = serviceProvider.GetService();
- var googleClient = new Google.GenAI.Client(apiKey: apiKey);
+ var googleClient = new Google.GenAI.Client(apiKey: apiKey, vertexAI: vertexAI);
var builder = new GoogleGenAIChatClient(googleClient, modelId)
.AsBuilder()
From 238fcde50f18998b741d1f47ddb91c7d28ede7a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 16:48:55 +0000
Subject: [PATCH 4/7] Remove [Experimental] attributes from alpha pre-release
APIs
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
---
.../Connectors.Google.UnitTests.csproj | 2 +-
.../Extensions/GoogleAIServiceCollectionExtensionsTests.cs | 7 -------
.../Extensions/GoogleAIKernelBuilderExtensions.cs | 3 ---
...gleAIServiceCollectionExtensions.DependencyInjection.cs | 3 ---
4 files changed, 1 insertion(+), 14 deletions(-)
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj b/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
index 1b546a8f4db5..535cb3e6389a 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Connectors.Google.UnitTests.csproj
@@ -8,7 +8,7 @@
enable
disable
false
- $(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050,SKEXP0070
+ $(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
index 6caded591225..6a0e6001e03d 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
@@ -117,7 +116,6 @@ public void GoogleAIEmbeddingGeneratorShouldBeRegisteredInServiceCollection()
#if NET
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
{
// Arrange
@@ -133,7 +131,6 @@ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
}
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
{
// Arrange
@@ -149,7 +146,6 @@ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
}
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithClient()
{
// Arrange
@@ -166,7 +162,6 @@ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithClient()
}
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithClient()
{
// Arrange
@@ -183,7 +178,6 @@ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithClient()
}
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldBeRegisteredWithServiceId()
{
// Arrange
@@ -200,7 +194,6 @@ public void GoogleAIChatClientShouldBeRegisteredWithServiceId()
}
[Fact]
- [Experimental("SKEXP0070")]
public void GoogleAIChatClientShouldResolveFromServiceProviderWhenClientNotProvided()
{
// Arrange
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
index 75ed1d304658..e16e642f262c 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -132,7 +131,6 @@ public static IKernelBuilder AddGoogleAIEmbeddingGenerator(
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated kernel builder.
- [Experimental("SKEXP0070")]
public static IKernelBuilder AddGoogleAIChatClient(
this IKernelBuilder builder,
string modelId,
@@ -165,7 +163,6 @@ public static IKernelBuilder AddGoogleAIChatClient(
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated kernel builder.
- [Experimental("SKEXP0070")]
public static IKernelBuilder AddGoogleAIChatClient(
this IKernelBuilder builder,
string modelId,
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
index 20bfd2118aa7..c5cac0575f60 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
@@ -62,7 +61,6 @@ public static IServiceCollection AddGoogleAIEmbeddingGenerator(
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated service collection.
- [Experimental("SKEXP0070")]
public static IServiceCollection AddGoogleAIChatClient(
this IServiceCollection services,
string modelId,
@@ -110,7 +108,6 @@ IChatClient Factory(IServiceProvider serviceProvider, object? _)
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated service collection.
- [Experimental("SKEXP0070")]
public static IServiceCollection AddGoogleAIChatClient(
this IServiceCollection services,
string modelId,
From 4f6c91cee045484bf2b9ede80f31863e221610a4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 17:22:31 +0000
Subject: [PATCH 5/7] Add unit tests for GoogleGeminiChatClient and integration
tests for ChatClient
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
---
.../Services/GoogleGeminiChatClientTests.cs | 108 +++++++
.../Google/Gemini/GeminiChatClientTests.cs | 183 ++++++++++++
.../GeminiFunctionCallingChatClientTests.cs | 273 ++++++++++++++++++
.../Connectors/Google/TestsBase.cs | 32 ++
.../IntegrationTests/IntegrationTests.csproj | 1 +
5 files changed, 597 insertions(+)
create mode 100644 dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
create mode 100644 dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
create mode 100644 dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
new file mode 100644
index 000000000000..8d2023ca75dc
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if NET
+
+using System;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.Google;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Google.UnitTests.Services;
+
+public sealed class GoogleGeminiChatClientTests
+{
+ [Fact]
+ public void ChatClientShouldBeCreatedWithApiKey()
+ {
+ // Arrange
+ string modelId = "gemini-1.5-pro";
+ string apiKey = "test-api-key";
+
+ // Act
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddGoogleAIChatClient(modelId, apiKey);
+ var kernel = kernelBuilder.Build();
+
+ // Assert
+ var chatClient = kernel.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void ChatClientShouldBeCreatedWithGoogleClient()
+ {
+ // Arrange
+ string modelId = "gemini-1.5-pro";
+ var googleClient = new global::Google.GenAI.Client(apiKey: "test-api-key");
+
+ // Act
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddGoogleAIChatClient(modelId, googleClient);
+ var kernel = kernelBuilder.Build();
+
+ // Assert
+ var chatClient = kernel.GetRequiredService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void ChatClientShouldBeCreatedWithServiceId()
+ {
+ // Arrange
+ string modelId = "gemini-1.5-pro";
+ string apiKey = "test-api-key";
+ string serviceId = "test-service";
+
+ // Act
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddGoogleAIChatClient(modelId, apiKey, serviceId: serviceId);
+ var kernel = kernelBuilder.Build();
+
+ // Assert
+ var chatClient = kernel.GetRequiredService(serviceId);
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void ChatClientThrowsForNullModelId()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient(null!, "apiKey"));
+ }
+
+ [Fact]
+ public void ChatClientThrowsForEmptyModelId()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("", "apiKey"));
+ }
+
+ [Fact]
+ public void ChatClientThrowsForNullApiKey()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("modelId", (string)null!));
+ }
+
+ [Fact]
+ public void ChatClientThrowsForEmptyApiKey()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("modelId", ""));
+ }
+}
+
+#endif
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
new file mode 100644
index 000000000000..03cdeb1b7952
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
@@ -0,0 +1,183 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using xRetry;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
+
+public sealed class GeminiChatClientTests(ITestOutputHelper output) : TestsBase(output)
+{
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("Large Language Model", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Brandon", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and write a long story about my name.")
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ Assert.True(responses.Count > 1);
+ var message = string.Concat(responses.Select(c => c.Text));
+ Assert.False(string.IsNullOrWhiteSpace(message));
+ this.Output.WriteLine(message);
+ }
+
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.System, "You are helpful assistant. Your name is Roger."),
+ new ChatMessage(ChatRole.System, "You know ACDD equals 1520"),
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("1520", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Roger", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.System, "You are helpful assistant. Your name is Roger."),
+ new ChatMessage(ChatRole.System, "You know ACDD equals 1520"),
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ Assert.True(responses.Count > 1);
+ var message = string.Concat(responses.Select(c => c.Text));
+ this.Output.WriteLine(message);
+ Assert.Contains("1520", message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Roger", message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Usage);
+ this.Output.WriteLine($"Input tokens: {response.Usage.InputTokenCount}");
+ this.Output.WriteLine($"Output tokens: {response.Usage.OutputTokenCount}");
+ this.Output.WriteLine($"Total tokens: {response.Usage.TotalTokenCount}");
+ }
+
+ [RetryTheory]
+ [InlineData(false, Skip = "This test is for manual verification.")]
+ [InlineData(true, Skip = "This test is for manual verification.")]
+ public async Task ChatClientWithChatOptionsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Generate a random number between 1 and 100.")
+ };
+
+ var chatOptions = new ChatOptions
+ {
+ Temperature = 0.0f,
+ MaxOutputTokens = 100
+ };
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
new file mode 100644
index 000000000000..154429b1dc57
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
@@ -0,0 +1,273 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.Google;
+using xRetry;
+using Xunit;
+using Xunit.Abstractions;
+using AIFunctionCallContent = Microsoft.Extensions.AI.FunctionCallContent;
+
+namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
+
+public sealed class GeminiFunctionCallingChatClientTests(ITestOutputHelper output) : TestsBase(output)
+{
+ private const string SkipMessage = "This test is for manual verification.";
+
+ [RetryTheory(Skip = SkipMessage)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType(nameof(CustomerPlugin));
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Select(f => f.AsAIFunction())
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+
+ var functionCallContent = response.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+ Assert.NotNull(functionCallContent);
+ Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory(Skip = SkipMessage)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType(nameof(CustomerPlugin));
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Select(f => f.AsAIFunction())
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools
+ };
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory, chatOptions).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+
+ var functionCallContent = responses
+ .SelectMany(r => r.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+ Assert.NotNull(functionCallContent);
+ Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory(Skip = SkipMessage)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ChatClientWithAutoInvokeFunctionsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Select(f => f.AsAIFunction())
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("John Kowalski", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Anna Nowak", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory(Skip = SkipMessage)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Select(f => f.AsAIFunction())
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var responses = await autoInvokingClient.GetStreamingResponseAsync(chatHistory, chatOptions).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ var content = string.Concat(responses.Select(c => c.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("John Kowalski", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Anna Nowak", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryTheory(Skip = SkipMessage)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ChatClientWithMultipleFunctionCallsAsync(bool isVertexAI)
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetChatClient(isVertexAI);
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers first and next return age of Anna customer?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Select(f => f.AsAIFunction())
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("28", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public sealed class CustomerPlugin
+ {
+ [KernelFunction(nameof(GetCustomers))]
+ [Description("Get list of customers.")]
+ [return: Description("List of customers.")]
+ public string[] GetCustomers()
+ {
+ return
+ [
+ "John Kowalski",
+ "Anna Nowak",
+ "Steve Smith",
+ ];
+ }
+
+ [KernelFunction(nameof(GetCustomerAge))]
+ [Description("Get age of customer.")]
+ [return: Description("Age of customer.")]
+ public int GetCustomerAge([Description("Name of customer")] string customerName)
+ {
+ return customerName switch
+ {
+ "John Kowalski" => 35,
+ "Anna Nowak" => 28,
+ "Steve Smith" => 42,
+ _ => throw new ArgumentException("Customer not found."),
+ };
+ }
+ }
+
+ public sealed class MathPlugin
+ {
+ [KernelFunction(nameof(Sum))]
+ [Description("Sum numbers.")]
+ public int Sum([Description("Numbers to sum")] int[] numbers)
+ {
+ return numbers.Sum();
+ }
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
index 723785497ccd..53a0880e345b 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
@@ -3,6 +3,8 @@
using System;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.Google;
using Microsoft.SemanticKernel.Embeddings;
@@ -65,6 +67,36 @@ protected TestsBase(ITestOutputHelper output)
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null)
};
+ protected IChatClient GetChatClient(bool isVertexAI, string? overrideModelId = null)
+ {
+ var modelId = isVertexAI
+ ? overrideModelId ?? this.VertexAI.Gemini.ModelId
+ : overrideModelId ?? this.GoogleAI.Gemini.ModelId;
+
+ var apiKey = isVertexAI ? this.VertexAI.BearerKey : this.GoogleAI.ApiKey;
+
+ var kernel = Kernel.CreateBuilder()
+ .AddGoogleAIChatClient(modelId, apiKey, vertexAI: isVertexAI)
+ .Build();
+
+ return kernel.GetRequiredService();
+ }
+
+ protected IChatClient GetChatClientWithVision(bool isVertexAI)
+ {
+ var modelId = isVertexAI
+ ? this.VertexAI.Gemini.VisionModelId
+ : this.GoogleAI.Gemini.VisionModelId;
+
+ var apiKey = isVertexAI ? this.VertexAI.BearerKey : this.GoogleAI.ApiKey;
+
+ var kernel = Kernel.CreateBuilder()
+ .AddGoogleAIChatClient(modelId, apiKey, vertexAI: isVertexAI)
+ .Build();
+
+ return kernel.GetRequiredService();
+ }
+
[Obsolete("Temporary test utility for Obsolete ITextEmbeddingGenerationService")]
protected ITextEmbeddingGenerationService GetEmbeddingService(ServiceType serviceType) => serviceType switch
{
diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index d0e45a75f94f..ec65cb12f288 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -41,6 +41,7 @@
+
From f9f08fbf1d1ffd9b99ac275fba5b5709885da98c Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Wed, 3 Dec 2025 18:01:57 +0000
Subject: [PATCH 6/7] Improvements
---
...oogleAIServiceCollectionExtensionsTests.cs | 7 ++--
.../Services/GoogleGeminiChatClientTests.cs | 6 +--
.../Google/Gemini/GeminiChatClientTests.cs | 39 ++++++++++---------
.../GeminiFunctionCallingChatClientTests.cs | 14 ++-----
4 files changed, 31 insertions(+), 35 deletions(-)
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
index 6a0e6001e03d..3fc6265202cd 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using Google.GenAI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
@@ -150,7 +151,7 @@ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithClient()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
- var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+ using var googleClient = new Client(apiKey: "apiKey");
// Act
kernelBuilder.AddGoogleAIChatClient("modelId", googleClient);
@@ -166,7 +167,7 @@ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithClient()
{
// Arrange
var services = new ServiceCollection();
- var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+ using var googleClient = new Client(apiKey: "apiKey");
// Act
services.AddGoogleAIChatClient("modelId", googleClient);
@@ -198,7 +199,7 @@ public void GoogleAIChatClientShouldResolveFromServiceProviderWhenClientNotProvi
{
// Arrange
var services = new ServiceCollection();
- var googleClient = new global::Google.GenAI.Client(apiKey: "apiKey");
+ using var googleClient = new Client(apiKey: "apiKey");
services.AddSingleton(googleClient);
// Act
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
index 8d2023ca75dc..e904c8d5d4b5 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
#if NET
using System;
+using Google.GenAI;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Connectors.Google;
using Xunit;
namespace SemanticKernel.Connectors.Google.UnitTests.Services;
@@ -34,7 +34,7 @@ public void ChatClientShouldBeCreatedWithGoogleClient()
{
// Arrange
string modelId = "gemini-1.5-pro";
- var googleClient = new global::Google.GenAI.Client(apiKey: "test-api-key");
+ using var googleClient = new Client(apiKey: "test-api-key");
// Act
var kernelBuilder = Kernel.CreateBuilder();
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
index 03cdeb1b7952..ca3d8c752005 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
@@ -12,9 +12,10 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
public sealed class GeminiChatClientTests(ITestOutputHelper output) : TestsBase(output)
{
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ private const string SkipReason = "This test is for manual verification.";
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
{
// Arrange
@@ -40,9 +41,9 @@ public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
Assert.Contains("Brandon", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
{
// Arrange
@@ -66,9 +67,9 @@ public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
this.Output.WriteLine(message);
}
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
{
// Arrange
@@ -96,9 +97,9 @@ public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
Assert.Contains("Roger", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
{
// Arrange
@@ -125,9 +126,9 @@ public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
Assert.Contains("Roger", message, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
{
// Arrange
@@ -151,9 +152,9 @@ public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
this.Output.WriteLine($"Total tokens: {response.Usage.TotalTokenCount}");
}
- [RetryTheory]
- [InlineData(false, Skip = "This test is for manual verification.")]
- [InlineData(true, Skip = "This test is for manual verification.")]
+ [RetryTheory(Skip = SkipReason)]
+ [InlineData(false)]
+ [InlineData(true)]
public async Task ChatClientWithChatOptionsAsync(bool isVertexAI)
{
// Arrange
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
index 154429b1dc57..4b8fbecd5fdf 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ComponentModel;
@@ -6,7 +6,6 @@
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Connectors.Google;
using xRetry;
using Xunit;
using Xunit.Abstractions;
@@ -36,7 +35,6 @@ public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync(bool isVert
var tools = kernel.Plugins
.SelectMany(p => p)
- .Select(f => f.AsAIFunction())
.Cast()
.ToList();
@@ -80,7 +78,6 @@ public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync(bo
var tools = kernel.Plugins
.SelectMany(p => p)
- .Select(f => f.AsAIFunction())
.Cast()
.ToList();
@@ -122,7 +119,6 @@ public async Task ChatClientWithAutoInvokeFunctionsAsync(bool isVertexAI)
var tools = kernel.Plugins
.SelectMany(p => p)
- .Select(f => f.AsAIFunction())
.Cast()
.ToList();
@@ -133,7 +129,7 @@ public async Task ChatClientWithAutoInvokeFunctionsAsync(bool isVertexAI)
};
// Use FunctionInvokingChatClient for auto-invoke
- var autoInvokingClient = new FunctionInvokingChatClient(sut);
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
// Act
var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
@@ -165,7 +161,6 @@ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync(bool isVertexA
var tools = kernel.Plugins
.SelectMany(p => p)
- .Select(f => f.AsAIFunction())
.Cast()
.ToList();
@@ -176,7 +171,7 @@ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync(bool isVertexA
};
// Use FunctionInvokingChatClient for auto-invoke
- var autoInvokingClient = new FunctionInvokingChatClient(sut);
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
// Act
var responses = await autoInvokingClient.GetStreamingResponseAsync(chatHistory, chatOptions).ToListAsync();
@@ -208,7 +203,6 @@ public async Task ChatClientWithMultipleFunctionCallsAsync(bool isVertexAI)
var tools = kernel.Plugins
.SelectMany(p => p)
- .Select(f => f.AsAIFunction())
.Cast()
.ToList();
@@ -219,7 +213,7 @@ public async Task ChatClientWithMultipleFunctionCallsAsync(bool isVertexAI)
};
// Use FunctionInvokingChatClient for auto-invoke
- var autoInvokingClient = new FunctionInvokingChatClient(sut);
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
// Act
var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
From c8be25152b3f5782a88d7ffe85b04deff109fcaa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 18:31:27 +0000
Subject: [PATCH 7/7] Split ChatClient extensions into AddGoogleGenAIChatClient
and AddGoogleVertexAIChatClient
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
---
...oogleAIServiceCollectionExtensionsTests.cs | 58 +++-
.../Services/GoogleGeminiChatClientTests.cs | 77 +++++-
.../Connectors.Google.csproj | 1 +
.../GoogleAIKernelBuilderExtensions.cs | 47 +++-
...ollectionExtensions.DependencyInjection.cs | 61 ++++-
.../Google/Gemini/GeminiChatClientTests.cs | 51 ++--
.../GeminiFunctionCallingChatClientTests.cs | 42 ++-
.../Gemini/GeminiVertexAIChatClientTests.cs | 173 ++++++++++++
...iVertexAIFunctionCallingChatClientTests.cs | 257 ++++++++++++++++++
.../Connectors/Google/TestsBase.cs | 40 ++-
10 files changed, 706 insertions(+), 101 deletions(-)
create mode 100644 dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIChatClientTests.cs
create mode 100644 dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIFunctionCallingChatClientTests.cs
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
index 3fc6265202cd..fc05f3aaefaf 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/GoogleAIServiceCollectionExtensionsTests.cs
@@ -117,13 +117,13 @@ public void GoogleAIEmbeddingGeneratorShouldBeRegisteredInServiceCollection()
#if NET
[Fact]
- public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
+ public void GoogleGenAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
// Act
- kernelBuilder.AddGoogleAIChatClient("modelId", "apiKey");
+ kernelBuilder.AddGoogleGenAIChatClient("modelId", "apiKey");
var kernel = kernelBuilder.Build();
// Assert
@@ -132,13 +132,13 @@ public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithApiKey()
}
[Fact]
- public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
+ public void GoogleGenAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
{
// Arrange
var services = new ServiceCollection();
// Act
- services.AddGoogleAIChatClient("modelId", "apiKey");
+ services.AddGoogleGenAIChatClient("modelId", "apiKey");
var serviceProvider = services.BuildServiceProvider();
// Assert
@@ -146,6 +146,36 @@ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithApiKey()
Assert.NotNull(chatClient);
}
+ [Fact]
+ public void GoogleVertexAIChatClientShouldBeRegisteredInKernelServices()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act
+ kernelBuilder.AddGoogleVertexAIChatClient("modelId", project: "test-project", location: "us-central1");
+
+ // Assert - just verify no exception during registration
+ // Resolution requires real credentials, so skip that in unit tests
+ var kernel = kernelBuilder.Build();
+ Assert.NotNull(kernel.Services);
+ }
+
+ [Fact]
+ public void GoogleVertexAIChatClientShouldBeRegisteredInServiceCollection()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddGoogleVertexAIChatClient("modelId", project: "test-project", location: "us-central1");
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert - just verify no exception during registration
+ // Resolution requires real credentials, so skip that in unit tests
+ Assert.NotNull(serviceProvider);
+ }
+
[Fact]
public void GoogleAIChatClientShouldBeRegisteredInKernelServicesWithClient()
{
@@ -179,14 +209,14 @@ public void GoogleAIChatClientShouldBeRegisteredInServiceCollectionWithClient()
}
[Fact]
- public void GoogleAIChatClientShouldBeRegisteredWithServiceId()
+ public void GoogleGenAIChatClientShouldBeRegisteredWithServiceId()
{
// Arrange
var services = new ServiceCollection();
const string serviceId = "test-service-id";
// Act
- services.AddGoogleAIChatClient("modelId", "apiKey", serviceId: serviceId);
+ services.AddGoogleGenAIChatClient("modelId", "apiKey", serviceId: serviceId);
var serviceProvider = services.BuildServiceProvider();
// Assert
@@ -194,6 +224,22 @@ public void GoogleAIChatClientShouldBeRegisteredWithServiceId()
Assert.NotNull(chatClient);
}
+ [Fact]
+ public void GoogleVertexAIChatClientShouldBeRegisteredWithServiceId()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ const string serviceId = "test-service-id";
+
+ // Act
+ services.AddGoogleVertexAIChatClient("modelId", project: "test-project", location: "us-central1", serviceId: serviceId);
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert - just verify no exception during registration
+ // Resolution requires real credentials, so skip that in unit tests
+ Assert.NotNull(serviceProvider);
+ }
+
[Fact]
public void GoogleAIChatClientShouldResolveFromServiceProviderWhenClientNotProvided()
{
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
index e904c8d5d4b5..91bf5435efdc 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleGeminiChatClientTests.cs
@@ -13,7 +13,7 @@ namespace SemanticKernel.Connectors.Google.UnitTests.Services;
public sealed class GoogleGeminiChatClientTests
{
[Fact]
- public void ChatClientShouldBeCreatedWithApiKey()
+ public void GenAIChatClientShouldBeCreatedWithApiKey()
{
// Arrange
string modelId = "gemini-1.5-pro";
@@ -21,7 +21,7 @@ public void ChatClientShouldBeCreatedWithApiKey()
// Act
var kernelBuilder = Kernel.CreateBuilder();
- kernelBuilder.AddGoogleAIChatClient(modelId, apiKey);
+ kernelBuilder.AddGoogleGenAIChatClient(modelId, apiKey);
var kernel = kernelBuilder.Build();
// Assert
@@ -29,6 +29,22 @@ public void ChatClientShouldBeCreatedWithApiKey()
Assert.NotNull(chatClient);
}
+ [Fact]
+ public void VertexAIChatClientShouldBeCreated()
+ {
+ // Arrange
+ string modelId = "gemini-1.5-pro";
+
+ // Act
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddGoogleVertexAIChatClient(modelId, project: "test-project", location: "us-central1");
+ var kernel = kernelBuilder.Build();
+
+ // Assert - just verify no exception during registration
+ // Resolution requires real credentials, so skip that in unit tests
+ Assert.NotNull(kernel.Services);
+ }
+
[Fact]
public void ChatClientShouldBeCreatedWithGoogleClient()
{
@@ -47,7 +63,7 @@ public void ChatClientShouldBeCreatedWithGoogleClient()
}
[Fact]
- public void ChatClientShouldBeCreatedWithServiceId()
+ public void GenAIChatClientShouldBeCreatedWithServiceId()
{
// Arrange
string modelId = "gemini-1.5-pro";
@@ -56,7 +72,7 @@ public void ChatClientShouldBeCreatedWithServiceId()
// Act
var kernelBuilder = Kernel.CreateBuilder();
- kernelBuilder.AddGoogleAIChatClient(modelId, apiKey, serviceId: serviceId);
+ kernelBuilder.AddGoogleGenAIChatClient(modelId, apiKey, serviceId: serviceId);
var kernel = kernelBuilder.Build();
// Assert
@@ -65,43 +81,80 @@ public void ChatClientShouldBeCreatedWithServiceId()
}
[Fact]
- public void ChatClientThrowsForNullModelId()
+ public void VertexAIChatClientShouldBeCreatedWithServiceId()
+ {
+ // Arrange
+ string modelId = "gemini-1.5-pro";
+ string serviceId = "test-service";
+
+ // Act
+ var kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.AddGoogleVertexAIChatClient(modelId, project: "test-project", location: "us-central1", serviceId: serviceId);
+ var kernel = kernelBuilder.Build();
+
+ // Assert - just verify no exception during registration
+ // Resolution requires real credentials, so skip that in unit tests
+ Assert.NotNull(kernel.Services);
+ }
+
+ [Fact]
+ public void GenAIChatClientThrowsForNullModelId()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleGenAIChatClient(null!, "apiKey"));
+ }
+
+ [Fact]
+ public void GenAIChatClientThrowsForEmptyModelId()
+ {
+ // Arrange
+ var kernelBuilder = Kernel.CreateBuilder();
+
+ // Act & Assert
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleGenAIChatClient("", "apiKey"));
+ }
+
+ [Fact]
+ public void GenAIChatClientThrowsForNullApiKey()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
// Act & Assert
- Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient(null!, "apiKey"));
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleGenAIChatClient("modelId", null!));
}
[Fact]
- public void ChatClientThrowsForEmptyModelId()
+ public void GenAIChatClientThrowsForEmptyApiKey()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
// Act & Assert
- Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("", "apiKey"));
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleGenAIChatClient("modelId", ""));
}
[Fact]
- public void ChatClientThrowsForNullApiKey()
+ public void VertexAIChatClientThrowsForNullModelId()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
// Act & Assert
- Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("modelId", (string)null!));
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleVertexAIChatClient(null!, project: "test-project", location: "us-central1"));
}
[Fact]
- public void ChatClientThrowsForEmptyApiKey()
+ public void VertexAIChatClientThrowsForEmptyModelId()
{
// Arrange
var kernelBuilder = Kernel.CreateBuilder();
// Act & Assert
- Assert.ThrowsAny(() => kernelBuilder.AddGoogleAIChatClient("modelId", ""));
+ Assert.ThrowsAny(() => kernelBuilder.AddGoogleVertexAIChatClient("", project: "test-project", location: "us-central1"));
}
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
index 9b7534700ee2..7e104ef8b230 100644
--- a/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
+++ b/dotnet/src/Connectors/Connectors.Google/Connectors.Google.csproj
@@ -26,6 +26,7 @@
+
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
index e16e642f262c..72518e91aaf8 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs
@@ -121,31 +121,64 @@ public static IKernelBuilder AddGoogleAIEmbeddingGenerator(
#if NET
///
- /// Add Google AI to the .
+ /// Add Google GenAI to the .
///
/// The kernel builder.
/// The model for chat completion.
- /// The API key for authentication with the Google AI API.
- /// Whether to use Vertex AI.
+ /// The API key for authentication with the Google GenAI API.
/// The optional service ID.
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated kernel builder.
- public static IKernelBuilder AddGoogleAIChatClient(
+ public static IKernelBuilder AddGoogleGenAIChatClient(
this IKernelBuilder builder,
string modelId,
string apiKey,
- bool vertexAI = false,
string? serviceId = null,
string? openTelemetrySourceName = null,
Action? openTelemetryConfig = null)
{
Verify.NotNull(builder);
- builder.Services.AddGoogleAIChatClient(
+ builder.Services.AddGoogleGenAIChatClient(
modelId,
apiKey,
- vertexAI,
+ serviceId,
+ openTelemetrySourceName,
+ openTelemetryConfig);
+
+ return builder;
+ }
+
+ ///
+ /// Add Google Vertex AI to the .
+ ///
+ /// The kernel builder.
+ /// The model for chat completion.
+ /// The Google Cloud project ID. If null, will attempt to use the GOOGLE_CLOUD_PROJECT environment variable.
+ /// The Google Cloud location (e.g., "us-central1"). If null, will attempt to use the GOOGLE_CLOUD_LOCATION environment variable.
+ /// The optional for authentication. If null, the client will use its internal discovery implementation to get credentials from the environment.
+ /// The optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated kernel builder.
+ public static IKernelBuilder AddGoogleVertexAIChatClient(
+ this IKernelBuilder builder,
+ string modelId,
+ string? project = null,
+ string? location = null,
+ Google.Apis.Auth.OAuth2.ICredential? credential = null,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddGoogleVertexAIChatClient(
+ modelId,
+ project,
+ location,
+ credential,
serviceId,
openTelemetrySourceName,
openTelemetryConfig);
diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
index c5cac0575f60..bb094ef08cde 100644
--- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.DependencyInjection.cs
@@ -51,21 +51,19 @@ public static IServiceCollection AddGoogleAIEmbeddingGenerator(
#if NET
///
- /// Add Google AI to the specified service collection.
+ /// Add Google GenAI to the specified service collection.
///
- /// The service collection to add the Google AI Chat Client to.
+ /// The service collection to add the Google GenAI Chat Client to.
/// The model for chat completion.
- /// The API key for authentication with the Google AI API.
- /// Whether to use Vertex AI.
+ /// The API key for authentication with the Google GenAI API.
/// Optional service ID.
/// An optional name for the OpenTelemetry source.
/// An optional callback that can be used to configure the instance.
/// The updated service collection.
- public static IServiceCollection AddGoogleAIChatClient(
+ public static IServiceCollection AddGoogleGenAIChatClient(
this IServiceCollection services,
string modelId,
string apiKey,
- bool vertexAI = false,
string? serviceId = null,
string? openTelemetrySourceName = null,
Action? openTelemetryConfig = null)
@@ -78,7 +76,56 @@ IChatClient Factory(IServiceProvider serviceProvider, object? _)
{
var loggerFactory = serviceProvider.GetService();
- var googleClient = new Google.GenAI.Client(apiKey: apiKey, vertexAI: vertexAI);
+ var googleClient = new Google.GenAI.Client(apiKey: apiKey);
+
+ var builder = new GoogleGenAIChatClient(googleClient, modelId)
+ .AsBuilder()
+ .UseKernelFunctionInvocation(loggerFactory)
+ .UseOpenTelemetry(loggerFactory, openTelemetrySourceName, openTelemetryConfig);
+
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder.Build();
+ }
+
+ services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return services;
+ }
+
+ ///
+ /// Add Google Vertex AI to the specified service collection.
+ ///
+ /// The service collection to add the Google Vertex AI Chat Client to.
+ /// The model for chat completion.
+ /// The Google Cloud project ID. If null, will attempt to use the GOOGLE_CLOUD_PROJECT environment variable.
+ /// The Google Cloud location (e.g., "us-central1"). If null, will attempt to use the GOOGLE_CLOUD_LOCATION environment variable.
+ /// The optional for authentication. If null, the client will use its internal discovery implementation to get credentials from the environment.
+ /// Optional service ID.
+ /// An optional name for the OpenTelemetry source.
+ /// An optional callback that can be used to configure the instance.
+ /// The updated service collection.
+ public static IServiceCollection AddGoogleVertexAIChatClient(
+ this IServiceCollection services,
+ string modelId,
+ string? project = null,
+ string? location = null,
+ Google.Apis.Auth.OAuth2.ICredential? credential = null,
+ string? serviceId = null,
+ string? openTelemetrySourceName = null,
+ Action? openTelemetryConfig = null)
+ {
+ Verify.NotNull(services);
+ Verify.NotNullOrWhiteSpace(modelId);
+
+ IChatClient Factory(IServiceProvider serviceProvider, object? _)
+ {
+ var loggerFactory = serviceProvider.GetService();
+
+ var googleClient = new Google.GenAI.Client(vertexAI: true, credential: credential, project: project, location: location);
var builder = new GoogleGenAIChatClient(googleClient, modelId)
.AsBuilder()
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
index ca3d8c752005..cf649b24a09f 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatClientTests.cs
@@ -10,13 +10,12 @@
namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
-public sealed class GeminiChatClientTests(ITestOutputHelper output) : TestsBase(output)
+public sealed class GeminiGenAIChatClientTests(ITestOutputHelper output) : TestsBase(output)
{
private const string SkipReason = "This test is for manual verification.";
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientGenerationReturnsValidResponseAsync()
{
// Arrange
var chatHistory = new[]
@@ -26,7 +25,7 @@ public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var response = await sut.GetResponseAsync(chatHistory);
@@ -41,10 +40,8 @@ public async Task ChatClientGenerationReturnsValidResponseAsync(bool isVertexAI)
Assert.Contains("Brandon", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientStreamingReturnsValidResponseAsync()
{
// Arrange
var chatHistory = new[]
@@ -54,7 +51,7 @@ public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
new ChatMessage(ChatRole.User, "Call me by my name and write a long story about my name.")
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
@@ -67,10 +64,8 @@ public async Task ChatClientStreamingReturnsValidResponseAsync(bool isVertexAI)
this.Output.WriteLine(message);
}
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientWithSystemMessagesAsync()
{
// Arrange
var chatHistory = new[]
@@ -82,7 +77,7 @@ public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var response = await sut.GetResponseAsync(chatHistory);
@@ -97,10 +92,8 @@ public async Task ChatClientWithSystemMessagesAsync(bool isVertexAI)
Assert.Contains("Roger", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientStreamingWithSystemMessagesAsync()
{
// Arrange
var chatHistory = new[]
@@ -112,7 +105,7 @@ public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
@@ -126,10 +119,8 @@ public async Task ChatClientStreamingWithSystemMessagesAsync(bool isVertexAI)
Assert.Contains("Roger", message, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientReturnsUsageDetailsAsync()
{
// Arrange
var chatHistory = new[]
@@ -139,7 +130,7 @@ public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var response = await sut.GetResponseAsync(chatHistory);
@@ -152,10 +143,8 @@ public async Task ChatClientReturnsUsageDetailsAsync(bool isVertexAI)
this.Output.WriteLine($"Total tokens: {response.Usage.TotalTokenCount}");
}
- [RetryTheory(Skip = SkipReason)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientWithChatOptionsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientWithChatOptionsAsync()
{
// Arrange
var chatHistory = new[]
@@ -169,7 +158,7 @@ public async Task ChatClientWithChatOptionsAsync(bool isVertexAI)
MaxOutputTokens = 100
};
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
// Act
var response = await sut.GetResponseAsync(chatHistory, chatOptions);
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
index 4b8fbecd5fdf..9173365a60b9 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingChatClientTests.cs
@@ -13,20 +13,18 @@
namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
-public sealed class GeminiFunctionCallingChatClientTests(ITestOutputHelper output) : TestsBase(output)
+public sealed class GeminiGenAIFunctionCallingChatClientTests(ITestOutputHelper output) : TestsBase(output)
{
private const string SkipMessage = "This test is for manual verification.";
- [RetryTheory(Skip = SkipMessage)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync()
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromType(nameof(CustomerPlugin));
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
var chatHistory = new[]
{
@@ -60,16 +58,14 @@ public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync(bool isVert
Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipMessage)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync()
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromType(nameof(CustomerPlugin));
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
var chatHistory = new[]
{
@@ -101,16 +97,14 @@ public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync(bo
Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipMessage)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientWithAutoInvokeFunctionsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithAutoInvokeFunctionsAsync()
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromType("CustomerPlugin");
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
var chatHistory = new[]
{
@@ -143,16 +137,14 @@ public async Task ChatClientWithAutoInvokeFunctionsAsync(bool isVertexAI)
Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipMessage)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync()
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromType("CustomerPlugin");
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
var chatHistory = new[]
{
@@ -185,16 +177,14 @@ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync(bool isVertexA
Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
}
- [RetryTheory(Skip = SkipMessage)]
- [InlineData(false)]
- [InlineData(true)]
- public async Task ChatClientWithMultipleFunctionCallsAsync(bool isVertexAI)
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithMultipleFunctionCallsAsync()
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromType("CustomerPlugin");
- var sut = this.GetChatClient(isVertexAI);
+ var sut = this.GetGenAIChatClient();
var chatHistory = new[]
{
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIChatClientTests.cs
new file mode 100644
index 000000000000..9ccca355133c
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIChatClientTests.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using xRetry;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
+
+public sealed class GeminiVertexAIChatClientTests(ITestOutputHelper output) : TestsBase(output)
+{
+ private const string SkipReason = "This test is for manual verification.";
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientGenerationReturnsValidResponseAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("Large Language Model", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Brandon", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientStreamingReturnsValidResponseAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and write a long story about my name.")
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ Assert.True(responses.Count > 1);
+ var message = string.Concat(responses.Select(c => c.Text));
+ Assert.False(string.IsNullOrWhiteSpace(message));
+ this.Output.WriteLine(message);
+ }
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientWithSystemMessagesAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.System, "You are helpful assistant. Your name is Roger."),
+ new ChatMessage(ChatRole.System, "You know ACDD equals 1520"),
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("1520", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Roger", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientStreamingWithSystemMessagesAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.System, "You are helpful assistant. Your name is Roger."),
+ new ChatMessage(ChatRole.System, "You know ACDD equals 1520"),
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Tell me your name and the value of ACDD.")
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ Assert.True(responses.Count > 1);
+ var message = string.Concat(responses.Select(c => c.Text));
+ this.Output.WriteLine(message);
+ Assert.Contains("1520", message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Roger", message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientReturnsUsageDetailsAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, I'm Brandon, how are you?"),
+ new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks for asking."),
+ new ChatMessage(ChatRole.User, "Call me by my name and expand this abbreviation: LLM")
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Usage);
+ this.Output.WriteLine($"Input tokens: {response.Usage.InputTokenCount}");
+ this.Output.WriteLine($"Output tokens: {response.Usage.OutputTokenCount}");
+ this.Output.WriteLine($"Total tokens: {response.Usage.TotalTokenCount}");
+ }
+
+ [RetryFact(Skip = SkipReason)]
+ public async Task ChatClientWithChatOptionsAsync()
+ {
+ // Arrange
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Generate a random number between 1 and 100.")
+ };
+
+ var chatOptions = new ChatOptions
+ {
+ Temperature = 0.0f,
+ MaxOutputTokens = 100
+ };
+
+ var sut = this.GetVertexAIChatClient();
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIFunctionCallingChatClientTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIFunctionCallingChatClientTests.cs
new file mode 100644
index 000000000000..964260a69f6f
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiVertexAIFunctionCallingChatClientTests.cs
@@ -0,0 +1,257 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using xRetry;
+using Xunit;
+using Xunit.Abstractions;
+using AIFunctionCallContent = Microsoft.Extensions.AI.FunctionCallContent;
+
+namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini;
+
+public sealed class GeminiVertexAIFunctionCallingChatClientTests(ITestOutputHelper output) : TestsBase(output)
+{
+ private const string SkipMessage = "This test is for manual verification.";
+
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithFunctionCallingReturnsToolCallsAsync()
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType(nameof(CustomerPlugin));
+
+ var sut = this.GetVertexAIChatClient();
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Messages);
+ Assert.NotEmpty(response.Messages);
+
+ var functionCallContent = response.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+ Assert.NotNull(functionCallContent);
+ Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientStreamingWithFunctionCallingReturnsToolCallsAsync()
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType(nameof(CustomerPlugin));
+
+ var sut = this.GetVertexAIChatClient();
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools
+ };
+
+ // Act
+ var responses = await sut.GetStreamingResponseAsync(chatHistory, chatOptions).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+
+ var functionCallContent = responses
+ .SelectMany(r => r.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+ Assert.NotNull(functionCallContent);
+ Assert.Contains("GetCustomers", functionCallContent.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithAutoInvokeFunctionsAsync()
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetVertexAIChatClient();
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("John Kowalski", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Anna Nowak", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientStreamingWithAutoInvokeFunctionsAsync()
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetVertexAIChatClient();
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var responses = await autoInvokingClient.GetStreamingResponseAsync(chatHistory, chatOptions).ToListAsync();
+
+ // Assert
+ Assert.NotEmpty(responses);
+ var content = string.Concat(responses.Select(c => c.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("John Kowalski", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Anna Nowak", content, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Steve Smith", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RetryFact(Skip = SkipMessage)]
+ public async Task ChatClientWithMultipleFunctionCallsAsync()
+ {
+ // Arrange
+ var kernel = new Kernel();
+ kernel.ImportPluginFromType("CustomerPlugin");
+
+ var sut = this.GetVertexAIChatClient();
+
+ var chatHistory = new[]
+ {
+ new ChatMessage(ChatRole.User, "Hello, could you show me list of customers first and next return age of Anna customer?")
+ };
+
+ var tools = kernel.Plugins
+ .SelectMany(p => p)
+ .Cast()
+ .ToList();
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = tools,
+ ToolMode = ChatToolMode.Auto
+ };
+
+ // Use FunctionInvokingChatClient for auto-invoke
+ using var autoInvokingClient = new FunctionInvokingChatClient(sut);
+
+ // Act
+ var response = await autoInvokingClient.GetResponseAsync(chatHistory, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ var content = string.Join("", response.Messages.Select(m => m.Text));
+ this.Output.WriteLine(content);
+ Assert.Contains("28", content, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public sealed class CustomerPlugin
+ {
+ [KernelFunction(nameof(GetCustomers))]
+ [Description("Get list of customers.")]
+ [return: Description("List of customers.")]
+ public string[] GetCustomers()
+ {
+ return
+ [
+ "John Kowalski",
+ "Anna Nowak",
+ "Steve Smith",
+ ];
+ }
+
+ [KernelFunction(nameof(GetCustomerAge))]
+ [Description("Get age of customer.")]
+ [return: Description("Age of customer.")]
+ public int GetCustomerAge([Description("Name of customer")] string customerName)
+ {
+ return customerName switch
+ {
+ "John Kowalski" => 35,
+ "Anna Nowak" => 28,
+ "Steve Smith" => 42,
+ _ => throw new ArgumentException("Customer not found."),
+ };
+ }
+ }
+
+ public sealed class MathPlugin
+ {
+ [KernelFunction(nameof(Sum))]
+ [Description("Sum numbers.")]
+ public int Sum([Description("Numbers to sum")] int[] numbers)
+ {
+ return numbers.Sum();
+ }
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
index 53a0880e345b..7e6bb8a45f54 100644
--- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs
@@ -67,31 +67,47 @@ protected TestsBase(ITestOutputHelper output)
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null)
};
- protected IChatClient GetChatClient(bool isVertexAI, string? overrideModelId = null)
+ protected IChatClient GetGenAIChatClient(string? overrideModelId = null)
{
- var modelId = isVertexAI
- ? overrideModelId ?? this.VertexAI.Gemini.ModelId
- : overrideModelId ?? this.GoogleAI.Gemini.ModelId;
+ var modelId = overrideModelId ?? this.GoogleAI.Gemini.ModelId;
+ var apiKey = this.GoogleAI.ApiKey;
- var apiKey = isVertexAI ? this.VertexAI.BearerKey : this.GoogleAI.ApiKey;
+ var kernel = Kernel.CreateBuilder()
+ .AddGoogleGenAIChatClient(modelId, apiKey)
+ .Build();
+
+ return kernel.GetRequiredService();
+ }
+
+ protected IChatClient GetVertexAIChatClient(string? overrideModelId = null)
+ {
+ var modelId = overrideModelId ?? this.VertexAI.Gemini.ModelId;
var kernel = Kernel.CreateBuilder()
- .AddGoogleAIChatClient(modelId, apiKey, vertexAI: isVertexAI)
+ .AddGoogleVertexAIChatClient(modelId, project: this.VertexAI.ProjectId, location: this.VertexAI.Location)
.Build();
return kernel.GetRequiredService();
}
- protected IChatClient GetChatClientWithVision(bool isVertexAI)
+ protected IChatClient GetGenAIChatClientWithVision()
{
- var modelId = isVertexAI
- ? this.VertexAI.Gemini.VisionModelId
- : this.GoogleAI.Gemini.VisionModelId;
+ var modelId = this.GoogleAI.Gemini.VisionModelId;
+ var apiKey = this.GoogleAI.ApiKey;
- var apiKey = isVertexAI ? this.VertexAI.BearerKey : this.GoogleAI.ApiKey;
+ var kernel = Kernel.CreateBuilder()
+ .AddGoogleGenAIChatClient(modelId, apiKey)
+ .Build();
+
+ return kernel.GetRequiredService();
+ }
+
+ protected IChatClient GetVertexAIChatClientWithVision()
+ {
+ var modelId = this.VertexAI.Gemini.VisionModelId;
var kernel = Kernel.CreateBuilder()
- .AddGoogleAIChatClient(modelId, apiKey, vertexAI: isVertexAI)
+ .AddGoogleVertexAIChatClient(modelId, project: this.VertexAI.ProjectId, location: this.VertexAI.Location)
.Build();
return kernel.GetRequiredService();