Skip to content

Commit fd7059a

Browse files
abbottdev-krollCopilotCopilotabbottdevwestey-m
authored
.Net: feat: Add support for dimensions in Vertex AI embedding services (#13612)
### Motivation and Context **Why is this change required?** PR #10489 added support for configuring embedding [dimensions] (outputDimensionality) for the Google AI connector, but the equivalent Vertex AI connector was not updated. This means specifying [Dimensions] in [EmbeddingGenerationOptions] or via the constructor has no effect when using Vertex AI — the API always returns the model's default dimensionality. **What problem does it solve?** When using [VertexAIEmbeddingGenerator] or [VertexAITextEmbeddingGenerationService]with a [dimensions] value (e.g. 128), the output embedding length is the model default (e.g. 3072) instead of the requested size. **What scenario does it contribute to?** Users who need to control embedding dimensionality for storage optimization, performance, or compatibility with downstream systems when using the Vertex AI endpoint. Fixes: #12988 ### Description This PR adds outputDimensionality support to the Vertex AI embedding connector, mirroring the existing Google AI implementation from PR #10489. The Google connector has two parallel embedding paths — Google AI (uses API key, calls generativelanguage.googleapis.com) and Vertex AI (uses bearer token, calls [{location}-aiplatform.googleapis.com]). PR #10489 only wired up dimensions for the Google AI path. This PR applies the same pattern to every layer of the Vertex AI path. The key structural difference between the two APIs is where outputDimensionality goes in the request JSON: Google AI puts it per-content-item: `{ "requests": [{ "content": {...}, "outputDimensionality": 128 }] }` Vertex AI puts it in the shared parameters block: `{ "instances": [...], "parameters": { "autoTruncate": false, "outputDimensionality": 128 } }` The implementation follows this difference. In [VertexAIEmbeddingRequest], outputDimensionality is added to the existing [RequestParameters] class (alongside autoTruncate), rather than on each instance item. Dimensions flow through the same chain as Google AI: 1. Extension methods accept int? dimensions = null and pass it to the generator/service constructor 2. [VertexAIEmbeddingGenerator] passes it to [VertexAITextEmbeddingGenerationService] 3. The service passes it to [VertexAIEmbeddingClient], which stores it as a default 4. At request time, the client resolves the final value as [options?.Dimensions ?? this._dimensions] — runtime [EmbeddingGenerationOptions] take priority over the constructor default 5. [VertexAIEmbeddingRequest.FromData()] sets it on the parameters block, and [JsonIgnore(WhenWritingNull)] ensures it's omitted when not specified All new parameters default to null, preserving full backward compatibility. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: abbottdev <3226335+abbottdev@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
1 parent 14ea2fc commit fd7059a

9 files changed

+477
-57
lines changed
Lines changed: 164 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,190 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net.Http;
6+
using System.Text;
37
using System.Threading.Tasks;
48
using Microsoft.Extensions.AI;
59
using Microsoft.SemanticKernel.Connectors.Google;
610
using Xunit;
711

812
namespace SemanticKernel.Connectors.Google.UnitTests.Services;
913

10-
public sealed class VertexAIEmbeddingGeneratorTests
14+
public sealed class VertexAIEmbeddingGeneratorTests : IDisposable
1115
{
16+
private const string Model = "fake-model";
17+
private const string BearerKey = "fake-key";
18+
private const int Dimensions = 512;
19+
private readonly HttpMessageHandlerStub _messageHandlerStub;
20+
private readonly HttpClient _httpClient;
21+
22+
public VertexAIEmbeddingGeneratorTests()
23+
{
24+
this._messageHandlerStub = new HttpMessageHandlerStub
25+
{
26+
ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
27+
{
28+
Content = new StringContent(
29+
"""
30+
{
31+
"predictions": [
32+
{
33+
"embeddings": {
34+
"values": [0.1, 0.2, 0.3, 0.4, 0.5]
35+
}
36+
}
37+
]
38+
}
39+
""",
40+
Encoding.UTF8,
41+
"application/json"
42+
)
43+
}
44+
};
45+
46+
this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false);
47+
}
48+
1249
[Fact]
1350
public void AttributesShouldContainModelIdBearerAsString()
1451
{
1552
// Arrange & Act
16-
string model = "fake-model";
17-
using var service = new VertexAIEmbeddingGenerator(model, "key", "location", "project");
53+
using var service = new VertexAIEmbeddingGenerator(Model, BearerKey, "location", "project");
1854

1955
// Assert
20-
Assert.Equal(model, service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelId);
56+
Assert.Equal(Model, service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelId);
2157
}
2258

2359
[Fact]
2460
public void AttributesShouldContainModelIdBearerAsFunc()
2561
{
2662
// Arrange & Act
27-
string model = "fake-model";
28-
using var service = new VertexAIEmbeddingGenerator(model, () => ValueTask.FromResult("key"), "location", "project");
63+
using var service = new VertexAIEmbeddingGenerator(Model, () => ValueTask.FromResult(BearerKey), "location", "project");
64+
65+
// Assert
66+
Assert.Equal(Model, service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelId);
67+
}
68+
69+
[Fact]
70+
public void AttributesShouldNotContainDimensionsWhenNotProvided()
71+
{
72+
// Arrange & Act
73+
using var service = new VertexAIEmbeddingGenerator(Model, BearerKey, "location", "project");
74+
75+
// Assert
76+
Assert.Null(service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelDimensions);
77+
}
78+
79+
[Fact]
80+
public void AttributesShouldContainDimensionsWhenProvided()
81+
{
82+
// Arrange & Act
83+
using var service = new VertexAIEmbeddingGenerator(Model, BearerKey, "location", "project", dimensions: Dimensions);
84+
85+
// Assert
86+
Assert.Equal(Dimensions, service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelDimensions);
87+
}
88+
89+
[Fact]
90+
public void GetDimensionsReturnsCorrectValue()
91+
{
92+
// Arrange
93+
using var service = new VertexAIEmbeddingGenerator(Model, BearerKey, "location", "project", dimensions: Dimensions);
94+
95+
// Act
96+
var result = service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelDimensions;
97+
98+
// Assert
99+
Assert.Equal(Dimensions, result);
100+
}
101+
102+
[Fact]
103+
public void GetDimensionsReturnsNullWhenNotProvided()
104+
{
105+
// Arrange
106+
using var service = new VertexAIEmbeddingGenerator(Model, BearerKey, "location", "project");
107+
108+
// Act
109+
var result = service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelDimensions;
110+
111+
// Assert
112+
Assert.Null(result);
113+
}
114+
115+
[Fact]
116+
public async Task ShouldNotIncludeDimensionsInRequestWhenNotProvidedAsync()
117+
{
118+
// Arrange
119+
using var service = new VertexAIEmbeddingGenerator(
120+
modelId: Model,
121+
bearerKey: BearerKey,
122+
location: "location",
123+
projectId: "project",
124+
dimensions: null,
125+
httpClient: this._httpClient);
126+
var dataToEmbed = new List<string> { "Text to embed" };
127+
128+
// Act
129+
await service.GenerateAsync(dataToEmbed);
130+
131+
// Assert
132+
Assert.NotNull(this._messageHandlerStub.RequestContent);
133+
var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent);
134+
Assert.DoesNotContain("outputDimensionality", requestBody);
135+
}
136+
137+
[Theory]
138+
[InlineData(Dimensions)]
139+
[InlineData(Dimensions * 2)]
140+
public async Task ShouldIncludeDimensionsInRequestWhenProvidedAsync(int? dimensions)
141+
{
142+
// Arrange
143+
using var service = new VertexAIEmbeddingGenerator(
144+
modelId: Model,
145+
bearerKey: BearerKey,
146+
location: "location",
147+
projectId: "project",
148+
dimensions: dimensions,
149+
httpClient: this._httpClient);
150+
var dataToEmbed = new List<string> { "Text to embed" };
151+
152+
// Act
153+
await service.GenerateAsync(dataToEmbed);
154+
155+
// Assert
156+
Assert.NotNull(this._messageHandlerStub.RequestContent);
157+
var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent);
158+
Assert.Contains($"\"outputDimensionality\":{dimensions}", requestBody);
159+
}
160+
161+
[Fact]
162+
public async Task OptionsDimensionsShouldOverrideConstructorDefaultAsync()
163+
{
164+
// Arrange
165+
const int OptionsDimensions = Dimensions * 2;
166+
using var service = new VertexAIEmbeddingGenerator(
167+
modelId: Model,
168+
bearerKey: BearerKey,
169+
location: "location",
170+
projectId: "project",
171+
dimensions: Dimensions,
172+
httpClient: this._httpClient);
173+
var dataToEmbed = new List<string> { "Text to embed" };
174+
var options = new EmbeddingGenerationOptions { Dimensions = OptionsDimensions };
175+
176+
// Act
177+
await service.GenerateAsync(dataToEmbed, options);
29178

30179
// Assert
31-
Assert.Equal(model, service.GetService<EmbeddingGeneratorMetadata>()!.DefaultModelId);
180+
Assert.NotNull(this._messageHandlerStub.RequestContent);
181+
var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent);
182+
Assert.Contains($"\"outputDimensionality\":{OptionsDimensions}", requestBody);
183+
}
184+
185+
public void Dispose()
186+
{
187+
this._messageHandlerStub.Dispose();
188+
this._httpClient.Dispose();
32189
}
33190
}

0 commit comments

Comments
 (0)