diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs
index 895bf8873df..990c92d3ad9 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs
@@ -17,7 +17,7 @@ namespace Microsoft.Extensions.AI;
///
/// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options
/// with a new instance, the callback may simply return that new instance, for example _ => new ChatOptions() { MaxTokens = 1000 }. To provide
-/// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example
+/// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example
/// options => options ?? new ChatOptions() { MaxTokens = 1000 }. Any changes to the caller-provided options instance will persist on the
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
/// and mutating the clone, for example:
@@ -31,6 +31,9 @@ namespace Microsoft.Extensions.AI;
///
///
///
+/// The callback may return , in which case a options will be passed to the next client in the pipeline.
+///
+///
/// The provided implementation of is thread-safe for concurrent use so long as the employed configuration
/// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the
/// configuration callback, as multiple calls to it may end up running in parallel with the same options instance.
@@ -39,7 +42,7 @@ namespace Microsoft.Extensions.AI;
public sealed class ConfigureOptionsChatClient : DelegatingChatClient
{
/// The callback delegate used to configure options.
- private readonly Func _configureOptions;
+ private readonly Func _configureOptions;
/// Initializes a new instance of the class with the specified callback.
/// The inner client.
@@ -47,7 +50,7 @@ public sealed class ConfigureOptionsChatClient : DelegatingChatClient
/// The delegate to invoke to configure the instance. It is passed the caller-supplied
/// instance and should return the configured instance to use.
///
- public ConfigureOptionsChatClient(IChatClient innerClient, Func configureOptions)
+ public ConfigureOptionsChatClient(IChatClient innerClient, Func configureOptions)
: base(innerClient)
{
_configureOptions = Throw.IfNull(configureOptions);
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs
index 12b903c0dac..2d98fbd9003 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs
@@ -21,9 +21,10 @@ public static class ConfigureOptionsChatClientBuilderExtensions
///
/// The .
///
+ ///
/// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options
/// with a new instance, the callback may simply return that new instance, for example _ => new ChatOptions() { MaxTokens = 1000 }. To provide
- /// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example
+ /// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example
/// options => options ?? new ChatOptions() { MaxTokens = 1000 }. Any changes to the caller-provided options instance will persist on the
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
/// and mutating the clone, for example:
@@ -35,9 +36,13 @@ public static class ConfigureOptionsChatClientBuilderExtensions
/// return newOptions;
/// }
///
+ ///
+ ///
+ /// The callback may return , in which case a options will be passed to the next client in the pipeline.
+ ///
///
public static ChatClientBuilder UseChatOptions(
- this ChatClientBuilder builder, Func configureOptions)
+ this ChatClientBuilder builder, Func configureOptions)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configureOptions);
diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs
new file mode 100644
index 00000000000..9068ac41caa
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+#pragma warning disable SA1629 // Documentation text should end with a period
+
+namespace Microsoft.Extensions.AI;
+
+/// A delegating embedding generator that updates or replaces the used by the remainder of the pipeline.
+/// Specifies the type of the input passed to the generator.
+/// Specifies the type of the embedding instance produced by the generator.
+///
+///
+/// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options
+/// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide
+/// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example
+/// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the
+/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
+/// and mutating the clone, for example:
+///
+/// options =>
+/// {
+/// var newOptions = options?.Clone() ?? new();
+/// newOptions.Dimensions = 100;
+/// return newOptions;
+/// }
+///
+///
+///
+/// The callback may return , in which case a options will be passed to the next generator in the pipeline.
+///
+///
+/// The provided implementation of is thread-safe for concurrent use so long as the employed configuration
+/// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the
+/// configuration callback, as multiple calls to it may end up running in parallel with the same options instance.
+///
+///
+public sealed class ConfigureOptionsEmbeddingGenerator : DelegatingEmbeddingGenerator
+ where TEmbedding : Embedding
+{
+ /// The callback delegate used to configure options.
+ private readonly Func _configureOptions;
+
+ ///
+ /// Initializes a new instance of the class with the
+ /// specified callback.
+ ///
+ /// The inner generator.
+ ///
+ /// The delegate to invoke to configure the instance. It is passed the caller-supplied
+ /// instance and should return the configured instance to use.
+ ///
+ public ConfigureOptionsEmbeddingGenerator(
+ IEmbeddingGenerator innerGenerator,
+ Func configureOptions)
+ : base(innerGenerator)
+ {
+ _configureOptions = Throw.IfNull(configureOptions);
+ }
+
+ ///
+ public override async Task> GenerateAsync(
+ IEnumerable values,
+ EmbeddingGenerationOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ return await base.GenerateAsync(values, _configureOptions(options), cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs
new file mode 100644
index 00000000000..011f4c058e9
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Shared.Diagnostics;
+
+#pragma warning disable SA1629 // Documentation text should end with a period
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides extensions for configuring instances.
+public static class ConfigureOptionsEmbeddingGeneratorBuilderExtensions
+{
+ ///
+ /// Adds a callback that updates or replaces . This can be used to set default options.
+ ///
+ /// Specifies the type of the input passed to the generator.
+ /// Specifies the type of the embedding instance produced by the generator.
+ /// The .
+ ///
+ /// The delegate to invoke to configure the instance. It is passed the caller-supplied
+ /// instance and should return the configured instance to use.
+ ///
+ /// The .
+ ///
+ ///
+ /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options
+ /// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide
+ /// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example
+ /// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the
+ /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
+ /// and mutating the clone, for example:
+ ///
+ /// options =>
+ /// {
+ /// var newOptions = options?.Clone() ?? new();
+ /// newOptions.Dimensions = 100;
+ /// return newOptions;
+ /// }
+ ///
+ ///
+ ///
+ /// The callback may return , in which case a options will be passed to the next generator in the pipeline.
+ ///
+ ///
+ public static EmbeddingGeneratorBuilder UseEmbeddingGenerationOptions(
+ this EmbeddingGeneratorBuilder builder,
+ Func configureOptions)
+ where TEmbedding : Embedding
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configureOptions);
+
+ return builder.Use(innerGenerator => new ConfigureOptionsEmbeddingGenerator(innerGenerator, configureOptions));
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs
index a27761c99ec..a911340813f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs
@@ -26,11 +26,13 @@ public void UseChatOptions_InvalidArgs_Throws()
Assert.Throws("configureOptions", () => builder.UseChatOptions(null!));
}
- [Fact]
- public async Task ConfigureOptions_ReturnedInstancePassedToNextClient()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullReturned)
{
ChatOptions providedOptions = new();
- ChatOptions returnedOptions = new();
+ ChatOptions? returnedOptions = nullReturned ? null : new();
ChatCompletion expectedCompletion = new(Array.Empty());
var expectedUpdates = Enumerable.Range(0, 3).Select(i => new StreamingChatCompletionUpdate()).ToArray();
using CancellationTokenSource cts = new();
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs
new file mode 100644
index 00000000000..b8a4b82cb59
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class ConfigureOptionsEmbeddingGeneratorTests
+{
+ [Fact]
+ public void ConfigureOptionsEmbeddingGenerator_InvalidArgs_Throws()
+ {
+ Assert.Throws("innerGenerator", () => new ConfigureOptionsEmbeddingGenerator>(null!, _ => new EmbeddingGenerationOptions()));
+ Assert.Throws("configureOptions", () => new ConfigureOptionsEmbeddingGenerator>(new TestEmbeddingGenerator(), null!));
+ }
+
+ [Fact]
+ public void UseEmbeddingGenerationOptions_InvalidArgs_Throws()
+ {
+ var builder = new EmbeddingGeneratorBuilder>();
+ Assert.Throws("configureOptions", () => builder.UseEmbeddingGenerationOptions(null!));
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullReturned)
+ {
+ EmbeddingGenerationOptions providedOptions = new();
+ EmbeddingGenerationOptions? returnedOptions = nullReturned ? null : new();
+ GeneratedEmbeddings> expectedEmbeddings = [];
+ using CancellationTokenSource cts = new();
+
+ using IEmbeddingGenerator> innerGenerator = new TestEmbeddingGenerator
+ {
+ GenerateAsyncCallback = (inputs, options, cancellationToken) =>
+ {
+ Assert.Same(returnedOptions, options);
+ Assert.Equal(cts.Token, cancellationToken);
+ return Task.FromResult(expectedEmbeddings);
+ }
+ };
+
+ using var generator = new EmbeddingGeneratorBuilder>()
+ .UseEmbeddingGenerationOptions(options =>
+ {
+ Assert.Same(providedOptions, options);
+ return returnedOptions;
+ })
+ .Use(innerGenerator);
+
+ var embeddings = await generator.GenerateAsync([], providedOptions, cts.Token);
+ Assert.Same(expectedEmbeddings, embeddings);
+ }
+}