diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/VertexAI/VertexAIEmbeddingEndpointTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/VertexAI/VertexAIEmbeddingEndpointTests.cs
new file mode 100644
index 000000000000..c3a9702810f1
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/VertexAI/VertexAIEmbeddingEndpointTests.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Connectors.Google;
+using Microsoft.SemanticKernel.Connectors.Google.Core;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Google.UnitTests.Core.VertexAI;
+
+public sealed class VertexAIEmbeddingEndpointTests : IDisposable
+{
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly HttpClient _httpClient;
+
+ public VertexAIEmbeddingEndpointTests()
+ {
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(
+ """
+ {
+ "embedding": {
+ "values": [0.1, 0.2, 0.3]
+ }
+ }
+ """);
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+ }
+
+ [Theory]
+ [InlineData("gemini-embedding-2", true)]
+ [InlineData("gemini-embedding-2-preview", true)]
+ [InlineData("gemini-embedding-2-0", true)]
+ [InlineData("GEMINI-EMBEDDING-2", true)]
+ [InlineData("gemini-embedding-001", false)]
+ [InlineData("textembedding-gecko", false)]
+ [InlineData("textembedding-gecko@003", false)]
+ [InlineData("text-embedding-004", false)]
+ [InlineData("custom-model", false)]
+ public void UsesEmbedContentEndpoint_ReturnsExpectedValue(string modelId, bool expected)
+ {
+ Assert.Equal(expected, VertexAIEmbeddingClient.UsesEmbedContentEndpoint(modelId));
+ }
+
+ [Fact]
+ public async Task Constructor_UsesEmbedContentSuffix_ForGeminiEmbedding2Async()
+ {
+ // Arrange
+ var client = this.CreateClient("gemini-embedding-2");
+
+ // Act
+ await client.GenerateEmbeddingsAsync(["hello"]);
+
+ // Assert
+ Assert.NotNull(this._messageHandlerStub.RequestUri);
+ Assert.Contains(":embedContent", this._messageHandlerStub.RequestUri.ToString(), StringComparison.Ordinal);
+ Assert.DoesNotContain(":predict", this._messageHandlerStub.RequestUri.ToString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task Constructor_UsesPredictSuffix_ForLegacyModelsAsync()
+ {
+ // Arrange – predict path expects the legacy response shape
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(
+ """
+ {
+ "predictions": [
+ {
+ "embeddings": {
+ "values": [0.1, 0.2, 0.3]
+ }
+ }
+ ]
+ }
+ """);
+ var client = this.CreateClient("gemini-embedding-001");
+
+ // Act
+ await client.GenerateEmbeddingsAsync(["hello"]);
+
+ // Assert
+ Assert.NotNull(this._messageHandlerStub.RequestUri);
+ Assert.Contains(":predict", this._messageHandlerStub.RequestUri.ToString(), StringComparison.Ordinal);
+ Assert.DoesNotContain(":embedContent", this._messageHandlerStub.RequestUri.ToString(), StringComparison.Ordinal);
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+
+ private VertexAIEmbeddingClient CreateClient(string modelId)
+ {
+ return new VertexAIEmbeddingClient(
+ httpClient: this._httpClient,
+ modelId: modelId,
+ bearerTokenProvider: () => ValueTask.FromResult("fake-key"),
+ apiVersion: VertexAIVersion.V1,
+ location: "us-central1",
+ projectId: "fake-project-id");
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentRequest.cs b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentRequest.cs
new file mode 100644
index 000000000000..ceae25b8e104
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentRequest.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Connectors.Google.Core;
+
+///
+/// Request body for Vertex AI :embedContent (Gemini Embedding 2 models).
+///
+internal sealed class VertexAIEmbedContentRequest
+{
+ [JsonPropertyName("content")]
+ public GeminiContent Content { get; set; } = null!;
+
+ [JsonPropertyName("outputDimensionality")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? OutputDimensionality { get; set; }
+
+ [JsonPropertyName("taskType")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? TaskType { get; set; }
+
+ [JsonPropertyName("title")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Title { get; set; }
+
+ public static VertexAIEmbedContentRequest FromText(string text, int? dimensions = null) => new()
+ {
+ Content = new GeminiContent
+ {
+ Parts =
+ [
+ new GeminiPart { Text = text }
+ ]
+ },
+ OutputDimensionality = dimensions
+ };
+}
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentResponse.cs b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentResponse.cs
new file mode 100644
index 000000000000..24180be971cd
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbedContentResponse.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Connectors.Google.Core;
+
+///
+/// Response body for Vertex AI :embedContent (Gemini Embedding 2 models).
+///
+internal sealed class VertexAIEmbedContentResponse
+{
+ [JsonPropertyName("embedding")]
+ [JsonRequired]
+ public ResponseEmbedding Embedding { get; set; } = null!;
+
+ internal sealed class ResponseEmbedding
+ {
+ [JsonPropertyName("values")]
+ [JsonRequired]
+ public ReadOnlyMemory Values { get; set; }
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs
index cb59e0087481..4d21aabde548 100644
--- a/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -19,6 +19,7 @@ internal sealed class VertexAIEmbeddingClient : ClientBase
private readonly string _embeddingModelId;
private readonly Uri _embeddingEndpoint;
private readonly int? _dimensions;
+ private readonly bool _usesEmbedContent;
///
/// Represents a client for interacting with the embeddings models by Vertex AI.
@@ -54,10 +55,19 @@ public VertexAIEmbeddingClient(
string baseUri = GetVertexAIBaseUri(location);
this._embeddingModelId = modelId;
- this._embeddingEndpoint = new Uri($"{baseUri}/{versionSubLink}/projects/{projectId}/locations/{location}/publishers/google/models/{this._embeddingModelId}:predict");
+ this._usesEmbedContent = UsesEmbedContentEndpoint(modelId);
+ string endpointSuffix = this._usesEmbedContent ? "embedContent" : "predict";
+ this._embeddingEndpoint = new Uri($"{baseUri}/{versionSubLink}/projects/{projectId}/locations/{location}/publishers/google/models/{this._embeddingModelId}:{endpointSuffix}");
this._dimensions = dimensions;
}
+ ///
+ /// Newer multimodal Gemini Embedding 2 models use the :embedContent endpoint.
+ /// Legacy text embedding models continue to use :predict.
+ ///
+ internal static bool UsesEmbedContentEndpoint(string modelId)
+ => modelId.StartsWith("gemini-embedding-2", StringComparison.OrdinalIgnoreCase);
+
///
/// Generates embeddings for the given data asynchronously.
///
@@ -72,8 +82,13 @@ public async Task>> GenerateEmbeddingsAsync(
{
Verify.NotNullOrEmpty(data);
- var geminiRequest = this.GetEmbeddingRequest(data, options);
- using var httpRequestMessage = await this.CreateHttpRequestAsync(geminiRequest, this._embeddingEndpoint).ConfigureAwait(false);
+ if (this._usesEmbedContent)
+ {
+ return await this.GenerateEmbedContentEmbeddingsAsync(data, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ var predictRequest = this.GetEmbeddingRequest(data, options);
+ using var httpRequestMessage = await this.CreateHttpRequestAsync(predictRequest, this._embeddingEndpoint).ConfigureAwait(false);
string body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken)
.ConfigureAwait(false);
@@ -81,6 +96,32 @@ public async Task>> GenerateEmbeddingsAsync(
return DeserializeAndProcessEmbeddingsResponse(body);
}
+ ///
+ /// The Vertex AI :embedContent API accepts a single content object per request.
+ /// Issue one call per input string and preserve order.
+ ///
+ private async Task>> GenerateEmbedContentEmbeddingsAsync(
+ IList data,
+ EmbeddingGenerationOptions? options,
+ CancellationToken cancellationToken)
+ {
+ var results = new List>(data.Count);
+ int? dimensions = options?.Dimensions ?? this._dimensions;
+
+ foreach (string text in data)
+ {
+ var request = VertexAIEmbedContentRequest.FromText(text, dimensions);
+ using var httpRequestMessage = await this.CreateHttpRequestAsync(request, this._embeddingEndpoint).ConfigureAwait(false);
+
+ string body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken)
+ .ConfigureAwait(false);
+
+ results.Add(DeserializeAndProcessEmbedContentResponse(body));
+ }
+
+ return results;
+ }
+
private VertexAIEmbeddingRequest GetEmbeddingRequest(IEnumerable data, EmbeddingGenerationOptions? options = null)
=> VertexAIEmbeddingRequest.FromData(data, options?.Dimensions ?? this._dimensions);
@@ -89,4 +130,7 @@ private static List> DeserializeAndProcessEmbeddingsRespon
private static List> ProcessEmbeddingsResponse(VertexAIEmbeddingResponse embeddingsResponse)
=> embeddingsResponse.Predictions.Select(prediction => prediction.Embeddings.Values).ToList();
+
+ private static ReadOnlyMemory DeserializeAndProcessEmbedContentResponse(string body)
+ => DeserializeResponse(body).Embedding.Values;
}