From c33e69fd39f664527a4beda6f708d6b44d3570dd Mon Sep 17 00:00:00 2001 From: Ananth Mudumba Date: Tue, 5 May 2026 14:49:58 -0700 Subject: [PATCH] EmbeddingGenerator: Adds ICosmosEmbeddingGenerator client-wide configuration (preview) Adds public preview surface (gated on #if PREVIEW): - ICosmosEmbeddingGenerator interface - CosmosEmbeddingResult (Vectors, TotalTokens, Latency) - CosmosClientOptions.EmbeddingGenerator - CosmosClientBuilder.WithEmbeddingGenerator - CosmosClient.EmbeddingGenerator Surface-only. Pipeline wiring lands in a follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Microsoft.Azure.Cosmos/src/CosmosClient.cs | 9 ++ .../src/CosmosClientOptions.cs | 12 ++ .../src/CosmosEmbeddingResult.cs | 69 +++++++++++ .../src/Fluent/CosmosClientBuilder.cs | 18 +++ .../src/ICosmosEmbeddingGenerator.cs | 113 ++++++++++++++++++ .../Contracts/DotNetPreviewSDKAPI.net6.json | 100 ++++++++++++++++ .../CosmosClientOptionsUnitTests.cs | 107 +++++++++++++++++ changelog.md | 2 + 8 files changed, 430 insertions(+) create mode 100644 Microsoft.Azure.Cosmos/src/CosmosEmbeddingResult.cs create mode 100644 Microsoft.Azure.Cosmos/src/ICosmosEmbeddingGenerator.cs diff --git a/Microsoft.Azure.Cosmos/src/CosmosClient.cs b/Microsoft.Azure.Cosmos/src/CosmosClient.cs index 3e19e603ca..3cb7353f8c 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClient.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClient.cs @@ -607,6 +607,15 @@ internal CosmosClient( /// This property is read-only. Modifying any options after the client has been created has no effect on the existing client instance. public virtual CosmosClientOptions ClientOptions => this.ClientContext.ClientOptions; +#if PREVIEW + /// + /// Gets the client-wide , or null if none was set. + /// Set via or + /// . + /// + public virtual ICosmosEmbeddingGenerator EmbeddingGenerator => this.ClientContext.ClientOptions.EmbeddingGenerator; +#endif + /// /// The response factory used to create CosmosClient response types. /// diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 9e3b8dfc00..90920e82e1 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -405,6 +405,18 @@ public ConnectionMode ConnectionMode #endif ReadConsistencyStrategy? ReadConsistencyStrategy { get; set; } + /// + /// Gets or sets the client-wide default used to generate + /// query-time vector embeddings for hybrid and vector-search queries. + /// + [JsonIgnore] +#if PREVIEW + public +#else + internal +#endif + ICosmosEmbeddingGenerator EmbeddingGenerator { get; set; } + /// /// Sets the priority level for requests created using cosmos client. /// diff --git a/Microsoft.Azure.Cosmos/src/CosmosEmbeddingResult.cs b/Microsoft.Azure.Cosmos/src/CosmosEmbeddingResult.cs new file mode 100644 index 0000000000..8d61ba2bbe --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/CosmosEmbeddingResult.cs @@ -0,0 +1,69 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Collections.Generic; + + /// + /// The result of a call to . + /// Carries the generated float32 vectors plus optional diagnostic fields (token usage, + /// latency) the SDK surfaces through CosmosDiagnostics. + /// +#if PREVIEW + public +#else + internal +#endif + sealed class CosmosEmbeddingResult + { + /// + /// Initializes a new instance of . + /// + /// + /// The generated float32 embedding vectors, one per input string supplied to the + /// originating call, + /// in the same order as the inputs. + /// + /// + /// Optional total token count consumed by the embedding service to produce these vectors. + /// Pass null when the underlying service does not report token usage. + /// + /// + /// Optional duration the implementation observed for the embedding service call (for + /// example, the wall-clock time around the underlying HTTP request). Surfaced through + /// CosmosDiagnostics for query-time observability. Pass null when the + /// implementation does not measure latency. + /// + public CosmosEmbeddingResult( + IReadOnlyList> vectors, + int? totalTokens = null, + TimeSpan? latency = null) + { + this.Vectors = vectors ?? throw new ArgumentNullException(nameof(vectors)); + this.TotalTokens = totalTokens; + this.Latency = latency; + } + + /// + /// Gets the generated float32 embedding vectors, one per input string, in the same + /// order as the inputs supplied to . + /// + public IReadOnlyList> Vectors { get; } + + /// + /// Gets the total number of tokens the embedding service consumed to generate + /// , or null when the underlying service does not report it. + /// + public int? TotalTokens { get; } + + /// + /// Gets the duration the implementation observed for the underlying embedding service + /// call, or null when the implementation does not measure it. Surfaced through + /// CosmosDiagnostics for query-time observability. + /// + public TimeSpan? Latency { get; } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index e33de132e0..4775f3e690 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -869,5 +869,23 @@ CosmosClientBuilder WithReadConsistencyStrategy(Cosmos.ReadConsistencyStrategy r this.clientOptions.ReadConsistencyStrategy = readConsistencyStrategy; return this; } + + /// + /// Sets the client-wide default used to generate + /// query-time vector embeddings for hybrid and vector-search queries. + /// + /// The embedding generator to use as the client-wide default. + /// The current . + /// Thrown if is null. +#if PREVIEW + public +#else + internal +#endif + CosmosClientBuilder WithEmbeddingGenerator(ICosmosEmbeddingGenerator embeddingGenerator) + { + this.clientOptions.EmbeddingGenerator = embeddingGenerator ?? throw new ArgumentNullException(nameof(embeddingGenerator)); + return this; + } } } diff --git a/Microsoft.Azure.Cosmos/src/ICosmosEmbeddingGenerator.cs b/Microsoft.Azure.Cosmos/src/ICosmosEmbeddingGenerator.cs new file mode 100644 index 0000000000..a1164dae0c --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/ICosmosEmbeddingGenerator.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Defines a contract for generating float32 vector embeddings from input text strings + /// supplied by the Azure Cosmos DB query pipeline. + /// The SDK invokes this when a query plan contains GenerateEmbeddings(...) literals + /// (for example VectorDistance(GenerateEmbeddings("big brown cat"), c.embedding)). + /// Set a client-wide default via CosmosClientOptions.EmbeddingGenerator or + /// CosmosClientBuilder.WithEmbeddingGenerator. Implementations MUST be thread-safe and are + /// responsible for any caching, retries, and authentication required to call the underlying + /// embedding service. + /// + /// + /// Preview surface. The SDK call site that invokes this method is delivered + /// in a follow-up release. Setting an instance via + /// or + /// has no runtime effect + /// today; the surface is shipped in this preview so customers can author and test + /// implementations against the contract ahead of the resolver landing. + /// Lifecycle and disposal. The customer owns the generator instance. The SDK + /// keeps a reference for the lifetime of the configured (or the + /// reference it was bound to) but never disposes it. If the + /// implementation holds disposable resources (for example an HttpClient or an + /// EmbeddingClient), the customer is responsible for disposing them when their + /// application tears down. + /// + /// Error semantics. Implementations are responsible for handling transient + /// failures from the underlying embedding service (network errors, rate limiting, etc.) + /// via their own retry policy. The SDK does not retry calls to this method. Any exception + /// thrown by the implementation is wrapped into a and + /// surfaced to the originating SDK caller. + /// + /// Cancellation. Implementations should honor the supplied + /// cooperatively wherever feasible (typically by forwarding + /// it to the underlying HTTP call). Best-effort cancellation is acceptable; ignoring the + /// token entirely is discouraged because it defeats caller-side timeouts. + /// + /// Idempotency and concurrency. The SDK may invoke this method multiple times + /// for the same inputs (for example during internal query retry) and may invoke it + /// concurrently from multiple threads. Implementations must be safe to call repeatedly + /// and from parallel callers, and must not assume per-call state. Note that each call + /// typically incurs cost at the underlying embedding service; implementations may cache + /// responses internally if they want to avoid duplicate billing for identical inputs. + /// +#if PREVIEW + public +#else + internal +#endif + interface ICosmosEmbeddingGenerator + { + /// + /// Generates an embedding vector for each of the supplied input strings. + /// + /// + /// The input strings to embed, in the order the implementation MUST preserve in the + /// returned (one vector per input, same + /// index). Typed as so implementations can size their + /// outbound batch without re-enumeration and so the 1:1 ordered contract is encoded + /// in the signature. + /// + /// + /// The embedding service endpoint to call (for example the Azure OpenAI account endpoint). + /// Sourced from the container's EmbeddingSource.Endpoint when configured. + /// + /// + /// The model deployment name to invoke at . Sourced from the + /// container's EmbeddingSource.DeploymentName when configured. + /// + /// + /// The vector dimensionality the produced embeddings must match. For models that support + /// dimensionality reduction (for example text-embedding-3-small / + /// text-embedding-3-large), implementations MUST forward this value to the + /// underlying service so the returned vectors have the expected length; otherwise the + /// service returns its default size, which may not match the container's + /// . + /// + /// + /// A propagated from the originating SDK call + /// (for example FeedIterator.ReadNextAsync). Implementations should honor cancellation. + /// + /// + /// A task that resolves to a whose + /// contains one float32 vector per input, + /// each of length , in the same order as + /// . + /// + /// Query-time vectors are sent to the Azure Cosmos DB gateway as float32 regardless of + /// the container's stored . Implementations targeting + /// containers configured for , + /// , or storage + /// should still produce float32 vectors here; the Azure Cosmos DB service applies the + /// configured quantization at write time. This contract + /// covers all four storage configurations supported by + /// the container's . + /// + /// + Task GenerateEmbeddingsAsync( + IReadOnlyList texts, + string endpoint, + string deploymentName, + int dimensions, + CancellationToken cancellationToken = default); + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json index 3ed1d7753e..dc82836891 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json @@ -42,6 +42,22 @@ }, "NestedTypes": {} }, + "Microsoft.Azure.Cosmos.CosmosClient;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": {}, + "Members": { + "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator EmbeddingGenerator": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator EmbeddingGenerator;CanRead:True;CanWrite:False;Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + } + }, + "NestedTypes": {} + }, "Microsoft.Azure.Cosmos.CosmosClientOptions;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { @@ -55,6 +71,20 @@ "Attributes": [], "MethodInfo": "Boolean get_EnableRemoteRegionPreferredForSessionRetry();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator EmbeddingGenerator[Newtonsoft.Json.JsonIgnoreAttribute()]": { + "Type": "Property", + "Attributes": [ + "JsonIgnoreAttribute" + ], + "MethodInfo": "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator EmbeddingGenerator;CanRead:True;CanWrite:True;Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_EmbeddingGenerator(Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator get_EmbeddingGenerator();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.Nullable`1[Microsoft.Azure.Cosmos.ReadConsistencyStrategy] get_ReadConsistencyStrategy()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ @@ -91,6 +121,13 @@ "Attributes": [], "MethodInfo": "System.TimeSpan InferenceRequestTimeout;CanRead:True;CanWrite:True;System.TimeSpan get_InferenceRequestTimeout();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_InferenceRequestTimeout(System.TimeSpan);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "Void set_EmbeddingGenerator(Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator)[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Void set_EmbeddingGenerator(Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Void set_EnableRemoteRegionPreferredForSessionRetry(Boolean)": { "Type": "Method", "Attributes": [], @@ -908,6 +945,53 @@ }, "NestedTypes": {} }, + "Microsoft.Azure.Cosmos.CosmosEmbeddingResult;System.Object;IsAbstract:False;IsSealed:True;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": {}, + "Members": { + "System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]] get_Vectors()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]] get_Vectors();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]] Vectors": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]] Vectors;CanRead:True;CanWrite:False;System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]] get_Vectors();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Nullable`1[System.Int32] get_TotalTokens()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "System.Nullable`1[System.Int32] get_TotalTokens();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Nullable`1[System.Int32] TotalTokens": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "System.Nullable`1[System.Int32] TotalTokens;CanRead:True;CanWrite:False;System.Nullable`1[System.Int32] get_TotalTokens();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Nullable`1[System.TimeSpan] get_Latency()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "System.Nullable`1[System.TimeSpan] get_Latency();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "System.Nullable`1[System.TimeSpan] Latency": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "System.Nullable`1[System.TimeSpan] Latency;CanRead:True;CanWrite:False;System.Nullable`1[System.TimeSpan] get_Latency();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "Void .ctor(System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]], System.Nullable`1[System.Int32], System.Nullable`1[System.TimeSpan])": { + "Type": "Constructor", + "Attributes": [], + "MethodInfo": "Void .ctor(System.Collections.Generic.IReadOnlyList`1[System.ReadOnlyMemory`1[System.Single]], System.Nullable`1[System.Int32], System.Nullable`1[System.TimeSpan])" + } + }, + "NestedTypes": {} + }, "Microsoft.Azure.Cosmos.Embedding;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { @@ -1102,6 +1186,11 @@ "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { + "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithEmbeddingGenerator(Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithEmbeddingGenerator(Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithEnableRemoteRegionPreferredForSessionRetry(Boolean)": { "Type": "Method", "Attributes": [], @@ -1151,6 +1240,17 @@ }, "NestedTypes": {} }, + "Microsoft.Azure.Cosmos.ICosmosEmbeddingGenerator;;IsAbstract:True;IsSealed:False;IsInterface:True;IsEnum:False;IsClass:False;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { + "Subclasses": {}, + "Members": { + "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.CosmosEmbeddingResult] GenerateEmbeddingsAsync(System.Collections.Generic.IReadOnlyList`1[System.String], System.String, System.String, Int32, System.Threading.CancellationToken)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.CosmosEmbeddingResult] GenerateEmbeddingsAsync(System.Collections.Generic.IReadOnlyList`1[System.String], System.String, System.String, Int32, System.Threading.CancellationToken);IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + } + }, + "NestedTypes": {} + }, "Microsoft.Azure.Cosmos.ItemRequestOptions;Microsoft.Azure.Cosmos.RequestOptions;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs index 1fe9127de4..9b3797ba11 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -278,6 +278,100 @@ public void VerifyReadConsistencyStrategyBuilderProperties() } } + [TestMethod] + public void VerifyEmbeddingGeneratorBuilderProperties() + { + string endpoint = AccountEndpoint; + string key = MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey; + + // Verify default is null + CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( + accountEndpoint: endpoint, + authKeyOrResourceToken: key); + + CosmosClient cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); + CosmosClientOptions clientOptions = cosmosClient.ClientOptions; + + Assert.IsNull(clientOptions.EmbeddingGenerator); + + // Verify WithEmbeddingGenerator sets the property + ICosmosEmbeddingGenerator generator = new MockEmbeddingGenerator(); + cosmosClientBuilder = new CosmosClientBuilder( + accountEndpoint: endpoint, + authKeyOrResourceToken: key); + + cosmosClientBuilder.WithEmbeddingGenerator(generator); + + cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); + clientOptions = cosmosClient.ClientOptions; + + Assert.AreSame(generator, clientOptions.EmbeddingGenerator, + "EmbeddingGenerator instance did not round-trip through the builder"); + + // Verify null throws ArgumentNullException + Assert.ThrowsException( + () => new CosmosClientBuilder(accountEndpoint: endpoint, authKeyOrResourceToken: key) + .WithEmbeddingGenerator(null), + "WithEmbeddingGenerator should throw ArgumentNullException for null input"); + } + +#if PREVIEW + [TestMethod] + public void CosmosClient_EmbeddingGenerator_ReturnsConfiguredInstance() + { + string endpoint = AccountEndpoint; + string key = MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey; + + // Default: CosmosClient.EmbeddingGenerator is null when nothing was configured. + CosmosClient defaultClient = new CosmosClientBuilder(endpoint, key) + .Build(new MockDocumentClient()); + Assert.IsNull(defaultClient.EmbeddingGenerator, + "CosmosClient.EmbeddingGenerator must be null when no generator was configured"); + + // Configured via builder: CosmosClient.EmbeddingGenerator returns the same instance. + ICosmosEmbeddingGenerator builderGenerator = new MockEmbeddingGenerator(); + CosmosClient builderClient = new CosmosClientBuilder(endpoint, key) + .WithEmbeddingGenerator(builderGenerator) + .Build(new MockDocumentClient()); + Assert.AreSame(builderGenerator, builderClient.EmbeddingGenerator, + "CosmosClient.EmbeddingGenerator must return the instance set via CosmosClientBuilder.WithEmbeddingGenerator"); + + // Configured via CosmosClientOptions directly: same accessor surfaces it. + ICosmosEmbeddingGenerator optionsGenerator = new MockEmbeddingGenerator(); + CosmosClient optionsClient = new CosmosClientBuilder(endpoint, key) + .WithCustomSerializer(new CosmosJsonDotNetSerializer()) // ensures non-default options path + .Build(new MockDocumentClient()); + optionsClient.ClientOptions.EmbeddingGenerator = optionsGenerator; + Assert.AreSame(optionsGenerator, optionsClient.EmbeddingGenerator, + "CosmosClient.EmbeddingGenerator must return the instance set on CosmosClientOptions.EmbeddingGenerator"); + } + + [TestMethod] + public void CosmosClientOptions_Clone_PreservesEmbeddingGenerator() + { + ICosmosEmbeddingGenerator generator = new MockEmbeddingGenerator(); + + CosmosClientOptions options = new CosmosClientOptions + { + EmbeddingGenerator = generator, + }; + + CosmosClientOptions clone = options.Clone(); + + Assert.AreSame(generator, clone.EmbeddingGenerator, + "CosmosClientOptions.Clone() must preserve the EmbeddingGenerator reference on the clone"); + + // The clone must be a distinct instance so subsequent mutations are isolated. + Assert.AreNotSame(options, clone, "Clone() must return a distinct instance"); + + // Mutating the clone must not affect the source. + ICosmosEmbeddingGenerator otherGenerator = new MockEmbeddingGenerator(); + clone.EmbeddingGenerator = otherGenerator; + Assert.AreSame(generator, options.EmbeddingGenerator, + "Mutating EmbeddingGenerator on a clone must not affect the original options instance"); + } +#endif + /// /// Test to validate that when the partition level failover is enabled with the preferred regions list is missing, then the client /// initialization should succeed. This should hold true for both environment variable and CosmosClientOptions. @@ -1335,5 +1429,18 @@ public int Compare(object x, object y) return 1; } } + + private sealed class MockEmbeddingGenerator : ICosmosEmbeddingGenerator + { + public System.Threading.Tasks.Task GenerateEmbeddingsAsync( + System.Collections.Generic.IReadOnlyList texts, + string endpoint, + string deploymentName, + int dimensions, + System.Threading.CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } } } \ No newline at end of file diff --git a/changelog.md b/changelog.md index 9761217a21..a5b7a303db 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Features Added +- [5838](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/5838) EmbeddingGenerator: Adds ICosmosEmbeddingGenerator client-wide configuration (preview) + #### Breaking Changes #### Bugs Fixed