diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index 67037001071..fb0ee48a236 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -2,8 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
@@ -25,6 +28,27 @@ namespace Microsoft.Extensions.AI;
/// Represents an for an OpenAI or .
internal sealed class OpenAIChatClient : IChatClient
{
+ // These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept
+ // a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
+ private static readonly Func, ChatCompletionOptions, RequestOptions, Task>>?
+ _completeChatAsync =
+ (Func, ChatCompletionOptions, RequestOptions, Task>>?)
+ typeof(ChatClient)
+ .GetMethod(
+ nameof(ChatClient.CompleteChatAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(IEnumerable), typeof(ChatCompletionOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(
+ typeof(Func, ChatCompletionOptions, RequestOptions, Task>>));
+ private static readonly Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>?
+ _completeChatStreamingAsync =
+ (Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>?)
+ typeof(ChatClient)
+ .GetMethod(
+ nameof(ChatClient.CompleteChatStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(IEnumerable), typeof(ChatCompletionOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(
+ typeof(Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>));
+
/// Metadata about the client.
private readonly ChatClientMetadata _metadata;
@@ -64,7 +88,10 @@ public async Task GetResponseAsync(
var openAIOptions = ToOpenAIOptions(options);
// Make the call to OpenAI.
- var response = await _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken).ConfigureAwait(false);
+ var task = _completeChatAsync is not null ?
+ _completeChatAsync(_chatClient, openAIChatMessages, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) :
+ _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken);
+ var response = await task.ConfigureAwait(false);
return FromOpenAIChatCompletion(response.Value, openAIOptions);
}
@@ -79,7 +106,9 @@ public IAsyncEnumerable GetStreamingResponseAsync(
var openAIOptions = ToOpenAIOptions(options);
// Make the call to OpenAI.
- var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken);
+ var chatCompletionUpdates = _completeChatStreamingAsync is not null ?
+ _completeChatStreamingAsync(_chatClient, openAIChatMessages, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) :
+ _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken);
return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken);
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index e2fbfdf3f84..fc9f84ed228 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -32,6 +33,23 @@ internal sealed class OpenAIResponsesChatClient : IChatClient
private static readonly Type? _internalResponseReasoningSummaryTextDeltaEventType = Type.GetType("OpenAI.Responses.InternalResponseReasoningSummaryTextDeltaEvent, OpenAI");
private static readonly PropertyInfo? _summaryTextDeltaProperty = _internalResponseReasoningSummaryTextDeltaEventType?.GetProperty("Delta");
+ // These delegate instances are used to call the internal overloads of CreateResponseAsync and CreateResponseStreamingAsync that accept
+ // a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
+ private static readonly Func, ResponseCreationOptions, RequestOptions, Task>>?
+ _createResponseAsync =
+ (Func, ResponseCreationOptions, RequestOptions, Task>>?)
+ typeof(OpenAIResponseClient).GetMethod(
+ nameof(OpenAIResponseClient.CreateResponseAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, Task>>));
+ private static readonly Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>?
+ _createResponseStreamingAsync =
+ (Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>?)
+ typeof(OpenAIResponseClient).GetMethod(
+ nameof(OpenAIResponseClient.CreateResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>));
+
/// Metadata about the client.
private readonly ChatClientMetadata _metadata;
@@ -79,7 +97,10 @@ public async Task GetResponseAsync(
var openAIOptions = ToOpenAIResponseCreationOptions(options);
// Make the call to the OpenAIResponseClient.
- var openAIResponse = (await _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken).ConfigureAwait(false)).Value;
+ var task = _createResponseAsync is not null ?
+ _createResponseAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) :
+ _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken);
+ var openAIResponse = (await task.ConfigureAwait(false)).Value;
// Convert the response to a ChatResponse.
return FromOpenAIResponse(openAIResponse, openAIOptions);
@@ -208,7 +229,9 @@ public IAsyncEnumerable GetStreamingResponseAsync(
var openAIResponseItems = ToOpenAIResponseItems(messages, options);
var openAIOptions = ToOpenAIResponseCreationOptions(options);
- var streamingUpdates = _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken);
+ var streamingUpdates = _createResponseStreamingAsync is not null ?
+ _createResponseStreamingAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) :
+ _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken);
return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken);
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs
new file mode 100644
index 00000000000..dbe38e42a2c
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+#pragma warning disable CA1307 // Specify StringComparison
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides utility methods for creating .
+internal static class RequestOptionsExtensions
+{
+ /// Creates a configured for use with OpenAI.
+ public static RequestOptions ToRequestOptions(this CancellationToken cancellationToken, bool streaming)
+ {
+ RequestOptions requestOptions = new()
+ {
+ CancellationToken = cancellationToken,
+ BufferResponse = !streaming
+ };
+
+ requestOptions.AddPolicy(MeaiUserAgentPolicy.Instance, PipelinePosition.PerCall);
+
+ return requestOptions;
+ }
+
+ /// Provides a pipeline policy that adds a "MEAI/x.y.z" user-agent header.
+ private sealed class MeaiUserAgentPolicy : PipelinePolicy
+ {
+ public static MeaiUserAgentPolicy Instance { get; } = new MeaiUserAgentPolicy();
+
+ private static readonly string _userAgentValue = CreateUserAgentValue();
+
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ AddUserAgentHeader(message);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ AddUserAgentHeader(message);
+ return ProcessNextAsync(message, pipeline, currentIndex);
+ }
+
+ private static void AddUserAgentHeader(PipelineMessage message) =>
+ message.Request.Headers.Add("User-Agent", _userAgentValue);
+
+ private static string CreateUserAgentValue()
+ {
+ const string Name = "MEAI";
+
+ if (typeof(MeaiUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version)
+ {
+ int pos = version.IndexOf('+');
+ if (pos >= 0)
+ {
+ version = version.Substring(0, pos);
+ }
+
+ if (version.Length > 0)
+ {
+ return $"{Name}/{version}";
+ }
+ }
+
+ return Name;
+ }
+ }
+}