diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index b340fed5f7..58a28f76c4 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -25,6 +25,9 @@
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 70ea1603c0..816ffdb678 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -52,6 +52,7 @@
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj
new file mode 100644
index 0000000000..cf6774e8d8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+ $(NoWarn);IDE0059
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs
new file mode 100644
index 0000000000..2a1d47a456
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs
@@ -0,0 +1,558 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Runtime.CompilerServices;
+using Google.Apis.Util;
+using Google.GenAI;
+using Google.GenAI.Types;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides an implementation based on .
+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.
+ public GoogleGenAIChatClient(Client client, string? defaultModelId)
+ {
+ this._client = client;
+ this._models = client.Models;
+ this._defaultModelId = defaultModelId;
+ }
+
+ /// Initializes a new instance.
+ public GoogleGenAIChatClient(Models client, string? defaultModelId)
+ {
+ this._models = client;
+ this._defaultModelId = defaultModelId;
+ }
+
+ ///
+ public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Utilities.ThrowIfNull(messages, nameof(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)
+ {
+ Utilities.ThrowIfNull(messages, nameof(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)
+ {
+ Utilities.ThrowIfNull(serviceType, nameof(serviceType));
+
+ if (serviceKey is null)
+ {
+ // If there's a request for metadata, lazily-initialize it and return it. We don't need to worry about race conditions,
+ // as there's no requirement that the same instance be returned each time, and creation is idempotent.
+ if (serviceType == typeof(ChatClientMetadata))
+ {
+ return this._metadata ??= new("gcp.gen_ai", new("https://generativelanguage.googleapis.com/"), defaultModelId: this._defaultModelId);
+ }
+
+ // Allow a consumer to "break glass" and 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 = new();
+ 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 ??= new()).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 ??= new()).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 AIFunctionDeclaration af:
+ functionDeclarations ??= new();
+ functionDeclarations.Add(new()
+ {
+ Name = af.Name,
+ Description = af.Description ?? "",
+ ParametersJsonSchema = af.JsonSchema,
+ });
+ break;
+
+ case HostedCodeInterpreterTool:
+ (config.Tools ??= new()).Add(new() { CodeExecution = new() });
+ break;
+
+ case HostedFileSearchTool:
+ (config.Tools ??= new()).Add(new() { Retrieval = new() });
+ break;
+
+ case HostedWebSearchTool:
+ (config.Tools ??= new()).Add(new() { GoogleSearch = new() });
+ break;
+ }
+ }
+ }
+
+ if (functionDeclarations is { Count: > 0 })
+ {
+ Tool functionTools = new();
+ (functionTools.FunctionDeclarations ??= new()).AddRange(functionDeclarations);
+ (config.Tools ??= new()).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 ??= new()).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 ??= new()).Add(new() { Text = instruction });
+ }
+
+ continue;
+ }
+
+ Content content = new() { Role = message.Role == ChatRole.Assistant ? "model" : "user" };
+ content.Parts ??= new();
+ 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() { { 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];
+
+ byte[]? thoughtSignature = null;
+ if (content is not TextReasoningContent { ProtectedData: not null } &&
+ i + 1 < contents.Count &&
+ contents[i + 1] is TextReasoningContent nextReasoning &&
+ string.IsNullOrWhiteSpace(nextReasoning.Text) &&
+ nextReasoning.ProtectedData is { } protectedData)
+ {
+ i++;
+ thoughtSignature = Convert.FromBase64String(protectedData);
+ }
+
+ Part? part = null;
+ switch (content)
+ {
+ case TextContent textContent:
+ part = new() { Text = textContent.Text };
+ break;
+
+ case TextReasoningContent reasoningContent:
+ part = new()
+ {
+ Thought = true,
+ Text = !string.IsNullOrWhiteSpace(reasoningContent.Text) ? reasoningContent.Text : null,
+ ThoughtSignature = reasoningContent.ProtectedData is not null ? Convert.FromBase64String(reasoningContent.ProtectedData) : null,
+ };
+ break;
+
+ case DataContent dataContent:
+ part = new()
+ {
+ InlineData = new()
+ {
+ MimeType = dataContent.MediaType,
+ Data = dataContent.Data.ToArray(),
+ DisplayName = dataContent.Name,
+ }
+ };
+ break;
+
+ case UriContent uriContent:
+ part = new()
+ {
+ FileData = new()
+ {
+ FileUri = uriContent.Uri.AbsoluteUri,
+ MimeType = uriContent.MediaType,
+ }
+ };
+ break;
+
+ case FunctionCallContent functionCallContent:
+ (callIdToFunctionNames ??= new())[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 FunctionResultContent 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)
+ {
+ part.ThoughtSignature ??= thoughtSignature;
+ 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 = part.Thought is true ?
+ new TextReasoningContent(part.Text) :
+ new TextContent(part.Text);
+ }
+ else if (part.InlineData is { } inlineData)
+ {
+ content = new DataContent(inlineData.Data, inlineData.MimeType ?? "application/octet-stream")
+ {
+ Name = inlineData.DisplayName,
+ };
+ }
+ else if (part.FileData is { FileUri: not null } fileData)
+ {
+ content = new UriContent(new Uri(fileData.FileUri), fileData.MimeType ?? "application/octet-stream");
+ }
+ else if (part.FunctionCall is { Name: not null } functionCall)
+ {
+ content = new FunctionCallContent(functionCall.Id ?? "", functionCall.Name, functionCall.Args!);
+ }
+ else if (part.FunctionResponse is { } functionResponse)
+ {
+ content = new FunctionResultContent(
+ 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);
+
+ if (part.ThoughtSignature is { } thoughtSignature)
+ {
+ contents.Add(new TextReasoningContent(null)
+ {
+ ProtectedData = Convert.ToBase64String(thoughtSignature),
+ });
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+ // Add any citation metadata.
+ if (candidate.CitationMetadata is { Citations: { Count: > 0 } citations } &&
+ responseContents.OfType().FirstOrDefault() is TextContent textContent)
+ {
+ foreach (var citation in citations)
+ {
+ textContent.Annotations = new List()
+ {
+ new CitationAnnotation()
+ {
+ Title = citation.Title,
+ Url = Uri.TryCreate(citation.Uri, UriKind.Absolute, out Uri? uri) ? uri : null,
+ AnnotatedRegions = new List()
+ {
+ new TextSpanAnnotatedRegion()
+ {
+ StartIndex = citation.StartIndex,
+ EndIndex = citation.EndIndex,
+ }
+ },
+ }
+ };
+ }
+ }
+ }
+
+ // 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 ??= new())[key] = i;
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs
new file mode 100644
index 0000000000..b1044fa373
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Google.Apis.Util;
+using Google.GenAI;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides implementations of Microsoft.Extensions.AI abstractions based on .
+public static class GoogleGenAIExtensions
+{
+ ///
+ /// Creates an wrapper around the specified .
+ ///
+ /// The to wrap.
+ /// The default model ID to use for chat requests if not specified in .
+ /// An that wraps the specified client.
+ /// is .
+ public static IChatClient AsIChatClient(this Client client, string? defaultModelId = null)
+ {
+ Utilities.ThrowIfNull(client, nameof(client));
+ return new GoogleGenAIChatClient(client, defaultModelId);
+ }
+
+ ///
+ /// Creates an wrapper around the specified .
+ ///
+ /// The client to wrap.
+ /// The default model ID to use for chat requests if not specified in .
+ /// An that wraps the specified client.
+ /// is .
+ public static IChatClient AsIChatClient(this Models models, string? defaultModelId = null)
+ {
+ Utilities.ThrowIfNull(models, nameof(models));
+ return new GoogleGenAIChatClient(models, defaultModelId);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs
new file mode 100644
index 0000000000..db633dc47d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create and use an AI agent with Google Gemini
+
+using Google.GenAI;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Mscc.GenerativeAI.Microsoft;
+
+const string JokerInstructions = "You are good at telling jokes.";
+const string JokerName = "JokerAgent";
+
+string apiKey = Environment.GetEnvironmentVariable("GOOGLE_GENAI_API_KEY") ?? throw new InvalidOperationException("Please set the GOOGLE_GENAI_API_KEY environment variable.");
+string model = Environment.GetEnvironmentVariable("GOOGLE_GENAI_MODEL") ?? "gemini-2.5-fast";
+
+// Using a Google GenAI IChatClient implementation
+// Until the PR https://github.com/googleapis/dotnet-genai/pull/81 is not merged this option
+// requires usage of also both GeminiChatClient.cs and GoogleGenAIExtensions.cs polyfills to work.
+
+ChatClientAgent agentGenAI = new(
+ new Client(vertexAI: false, apiKey: apiKey).AsIChatClient(model),
+ name: JokerName,
+ instructions: JokerInstructions);
+
+AgentRunResponse response = await agentGenAI.RunAsync("Tell me a joke about a pirate.");
+Console.WriteLine($"Google GenAI client based agent response:\n{response}");
+
+// Using a community driven Mscc.GenerativeAI.Microsoft package
+
+ChatClientAgent agentCommunity = new(
+ new GeminiChatClient(apiKey, model),
+ name: JokerName,
+ instructions: JokerInstructions);
+
+response = await agentCommunity.RunAsync("Tell me a joke about a pirate.");
+Console.WriteLine($"Community client based agent response:\n{response}");
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md
new file mode 100644
index 0000000000..bc3a3592e6
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md
@@ -0,0 +1,37 @@
+# Creating an AIAgent with Google Gemini
+
+This sample demonstrates how to create an AIAgent using Google Gemini models as the underlying inference service.
+
+The sample showcases two different `IChatClient` implementations:
+
+1. **Google GenAI** - Using the official [Google.GenAI](https://www.nuget.org/packages/Google.GenAI) package
+2. **Mscc.GenerativeAI.Microsoft** - Using the community-driven [Mscc.GenerativeAI.Microsoft](https://www.nuget.org/packages/Mscc.GenerativeAI.Microsoft) package
+
+## Prerequisites
+
+Before you begin, ensure you have the following prerequisites:
+
+- .NET 10.0 SDK or later
+- Google AI Studio API key (get one at [Google AI Studio](https://aistudio.google.com/apikey))
+
+Set the following environment variables:
+
+```powershell
+$env:GOOGLE_GENAI_API_KEY="your-google-api-key" # Replace with your Google AI Studio API key
+$env:GOOGLE_GENAI_MODEL="gemini-2.5-fast" # Optional, defaults to gemini-2.5-fast
+```
+
+## Package Options
+
+### Google GenAI (Official)
+
+The official Google GenAI package provides direct access to Google's Generative AI models. This sample uses an extension method to convert the Google client to an `IChatClient`.
+
+> [!NOTE]
+> Until PR [googleapis/dotnet-genai#81](https://github.com/googleapis/dotnet-genai/pull/81) is merged, this option requires the additional `GeminiChatClient.cs` and `GoogleGenAIExtensions.cs` files included in this sample.
+>
+> We appreciate any community push by liking and commenting in the above PR to get it merged and release as part of official Google GenAI package.
+
+### Mscc.GenerativeAI.Microsoft (Community)
+
+The community-driven Mscc.GenerativeAI.Microsoft package provides a ready-to-use `IChatClient` implementation for Google Gemini models through the `GeminiChatClient` class.