.NET: Support Vertex AI embedContent for Gemini embedding models - #14269
.NET: Support Vertex AI embedContent for Gemini embedding models#14269Patrick Ribbsaeter (patrickswedish) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the .NET Google Vertex AI embeddings connector to select the correct REST method suffix for different embedding model IDs (e.g., newer Gemini embedding models), and adds a new test file intended to validate that routing.
Changes:
- Build the embeddings endpoint URI with a model-dependent suffix (
:embedContentvs:predict). - Add model-id based suffix selection logic in
VertexAIEmbeddingClient. - Add a new
VertexAIEmbeddingClientTestsfile (currently placeholder tests, and currently placed under the production project).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs | Adds endpoint-suffix selection logic and uses it when building the Vertex AI embeddings endpoint URI. |
| dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClientTests.cs | Introduces tests for endpoint suffix selection, but currently as placeholders and currently located in the shipping library project. |
Suppressed comments (1)
dotnet/src/Connectors/Connectors.Google/Core/VertexAI/VertexAIEmbeddingClient.cs:94
- GetEmbeddingEndpointSuffix allocates a new HashSet on every call. Since the method is only doing prefix checks (and the HashSet is redundant with the StartsWith logic), this can be simplified to avoid the per-call allocation.
private static string GetEmbeddingEndpointSuffix(string modelId)
{
var embedContentModels = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"gemini-embedding-2",
"gemini-embedding-2-preview",
"gemini-embedding-2-0",
"textembedding-gecko",
"textembedding-gecko@latest",
"textembedding-gecko@001",
"textembedding-gecko@002",
"textembedding-gecko@003"
};
if (embedContentModels.Contains(modelId))
{
return "embedContent";
}
if (modelId.StartsWith("gemini-embedding-2", StringComparison.OrdinalIgnoreCase) ||
modelId.StartsWith("textembedding-gecko", StringComparison.OrdinalIgnoreCase))
{
return "embedContent";
}
return "predict";
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.SemanticKernel.Connectors.Google.Core; | ||
| using Moq; | ||
| using Xunit; |
| // or the method would be made internal for testing | ||
|
|
||
| // For now, this demonstrates the expected behavior | ||
| // The actual implementation in VertexAIEmbeddingClient.cs contains the logic | ||
| Assert.True(true); // Placeholder - method is private |
| namespace Microsoft.SemanticKernel.Connectors.Google.Core | ||
| { |
| /// </summary> | ||
| /// <param name="httpClient">HttpClient instance used to send HTTP requests</param> | ||
| /// <param name="modelId">Embeddings generation model id</param> | ||
| /// <param name="bearerTokenProvider">Bearer key provider used for authentication</param> |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 76%
✓ Correctness
The PR adds endpoint routing logic to select between 'embedContent' and 'predict' Vertex AI endpoints based on model ID. The main correctness concern is redundant logic in GetEmbeddingEndpointSuffix (the HashSet check is fully subsumed by the StartsWith checks that follow). The tests are all placeholders that assert nothing meaningful. The request/response format difference between :predict and :embedContent endpoints is not addressed - if the two endpoints expect different request/response schemas, routing alone won't fix the issue.
✓ Security Reliability
The change routes embedding requests to different Vertex AI endpoints based on model ID. The core logic is sound from a security perspective - modelId is validated before use and the endpoint suffix is always one of two hardcoded strings. The test file appears to be placed in the production source directory rather than a test project, and all tests are placeholders that assert nothing. No security or reliability issues found in the production code changes.
✓ Test Coverage
The new test file contains only placeholder assertions (
Assert.True(true)) that verify nothing. All three test methods acknowledge in comments that they cannot actually test the behavior, making them dead code that provides zero coverage for the newGetEmbeddingEndpointSuffixrouting logic. Additionally, the test file is placed in the production source directory rather than a test project.
✓ Failure Modes
The PR switches certain Vertex AI embedding models from the :predict endpoint to :embedContent, but the request/response serialization (VertexAIEmbeddingRequest/VertexAIEmbeddingResponse) appears unchanged. If the :embedContent endpoint uses a different request/response schema, this will cause runtime deserialization failures or silent empty results. The tests are all placeholders with Assert.True(true) and provide no actual coverage. Additionally, the test file is placed in the production source directory rather than a test project.
✗ Design Approach
The endpoint selection change is not paired with the request/response contract changes that the repo already uses for content-embedding APIs, so the new
:embedContentroute is likely wired to the wrong payload/parser. The added tests also do not execute any real behavior, so they do not protect this new routing logic.
Flagged Issues
-
VertexAIEmbeddingClient.csnow routes gemini-embedding-2/textembedding-gecko models to:embedContent, butGenerateEmbeddingsAsyncstill serializesVertexAIEmbeddingRequestand parsesVertexAIEmbeddingResponse, which are hard-coded to the legacyinstances/parameters→predictionscontract. The existing embed-content pattern in this repo (GoogleAIEmbeddingClient.cs,GoogleAIEmbeddingRequest.cs,GoogleAIEmbeddingResponse.cs) uses dedicated wire types. Without matching payload/parser changes, routed models will hit runtime deserialization failures or return empty results.
Suggestions
- Replace the placeholder assertions in the test file with real checks that exercise the model-to-endpoint routing (e.g., expose the suffix logic as internal with
[InternalsVisibleTo], or useHttpMessageHandlerStubto assert on the constructed request URI).
Automated review by patrickswedish's agents
| string endpointSuffix = GetEmbeddingEndpointSuffix(modelId); | ||
| this._embeddingEndpoint = new Uri($"{baseUri}/{versionSubLink}/projects/{projectId}/locations/{location}/publishers/google/models/{this._embeddingModelId}:{endpointSuffix}"); | ||
| this._dimensions = dimensions; | ||
| } |
There was a problem hiding this comment.
Switching only the RPC suffix here leaves the rest of the client on the legacy predict wire contract. GenerateEmbeddingsAsync still uses VertexAIEmbeddingRequest/VertexAIEmbeddingResponse shaped as instances/parameters → predictions. The :embedContent endpoint expects a different schema (see GoogleAIEmbeddingRequest/GoogleAIEmbeddingResponse for the correct pattern). As written, redirected models will send the wrong payload and fail to parse the response.
|
Flagged issue
Source: automated DevFlow PR review |
|
Hi team — just checking in on this one. The fix resolves the VertexAI endpoint issue and all local tests pass. Happy to rebase on latest |
Motivation and ContextFixes a silent endpoint routing bug that breaks all embedding calls for Google's Gemini Embedding 2 family (e.g.
The DescriptionRoot cause: // Before (buggy — always :predict)
this._embeddingEndpoint = new Uri(
$"{baseUri}/{versionSubLink}/projects/{projectId}/locations/{location}/publishers/google/models/{this._embeddingModelId}:predict"
);Fix:
// After
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}"
);
Contribution Checklist
|
Select :embedContent endpoint and wire contract for Gemini Embedding 2 models, while preserving :predict for legacy models.
3b4666e to
713d4f8
Compare
Motivation and Context
Newer multimodal Gemini embedding models on Google Cloud Vertex AI (such as
gemini-embedding-2-0andgemini-embedding-2-preview) require the Vertex AI:embedContentendpoint and wire contract rather than the legacy:predictendpoint.Previously,
VertexAIEmbeddingClientunconditionally constructed the:predictendpoint and serialized requests usingVertexAIEmbeddingRequest(instances/parameters->predictions), causing deserialization failures when calling Gemini embedding models.Description
gemini-embedding-2to the Vertex AI:embedContentendpoint while preserving the legacy:predictendpoint for other embedding models (textembedding-gecko,text-embedding-004, custom models).VertexAIEmbedContentRequestandVertexAIEmbedContentResponsematching the Vertex AI:embedContentwire schema (contentwithparts, optionaloutputDimensionality,taskType, andtitle).EmbeddingGenerationOptions(Dimensions,task_type, andtitleviaAdditionalProperties) in both:embedContentand:predictpaths.:embedContent, send sequential requests per input string, preserve input order, and enforce cancellation checks.Connectors.Google.UnitTestscovering endpoint routing, payload serialization, response deserialization, dimensions, task type/title options, sequential multi-input handling, and cancellation.Contribution Checklist