diff --git a/eng/packages/General.props b/eng/packages/General.props
index b7066789238..5be4031ad4d 100644
--- a/eng/packages/General.props
+++ b/eng/packages/General.props
@@ -16,7 +16,7 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
index a50aea8dea3..8c1febc6bc8 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
@@ -2,12 +2,13 @@
## NOT YET RELEASED
+- Updated to depend on OpenAI 2.5.0.
- Added M.E.AI to OpenAI conversions for response format types.
- Added `ResponseTool` to `AITool` conversions.
## 9.9.0-preview.1.25458.4
-- Updated to depend on OpenAI 2.4.0
+- Updated to depend on OpenAI 2.4.0.
- Updated tool mappings to recognize any `AIFunctionDeclaration`.
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
- Fixed handling of annotated but empty content in the `AsIChatClient` for `AssistantClient`.
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs
index dbe9bf10237..9c3da231065 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.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.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
@@ -11,12 +14,25 @@
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
+#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
namespace Microsoft.Extensions.AI;
/// An for an OpenAI .
internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator>
{
+ // This delegate instance is used to call the internal overload of GenerateEmbeddingsAsync that accepts
+ // a RequestOptions. This should be replaced once a better way to pass RequestOptions is available.
+ private static readonly Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>?
+ _generateEmbeddingsAsync =
+ (Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>?)
+ typeof(EmbeddingClient)
+ .GetMethod(
+ nameof(EmbeddingClient.GenerateEmbeddingsAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(IEnumerable), typeof(OpenAI.Embeddings.EmbeddingGenerationOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(
+ typeof(Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>));
+
/// Metadata about the embedding generator.
private readonly EmbeddingGeneratorMetadata _metadata;
@@ -49,7 +65,10 @@ public async Task>> GenerateAsync(IEnumerab
{
OpenAI.Embeddings.EmbeddingGenerationOptions? openAIOptions = ToOpenAIOptions(options);
- var embeddings = (await _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken).ConfigureAwait(false)).Value;
+ var t = _generateEmbeddingsAsync is not null ?
+ _generateEmbeddingsAsync(_embeddingClient, values, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) :
+ _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken);
+ var embeddings = (await t.ConfigureAwait(false)).Value;
return new(embeddings.Select(e =>
new Embedding(e.ToFloats())
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index d094f5f8581..ec662a68891 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -65,12 +65,7 @@ public OpenAIResponsesChatClient(OpenAIResponseClient responseClient)
_responseClient = responseClient;
- // https://github.com/openai/openai-dotnet/issues/662
- // Update to avoid reflection once OpenAIResponseClient.Model is exposed publicly.
- string? model = typeof(OpenAIResponseClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- ?.GetValue(responseClient) as string;
-
- _metadata = new("openai", responseClient.Endpoint, model);
+ _metadata = new("openai", responseClient.Endpoint, responseClient.Model);
}
///
@@ -469,27 +464,19 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
break;
case HostedCodeInterpreterTool codeTool:
- string json;
- if (codeTool.Inputs is { Count: > 0 } inputs)
- {
- string jsonArray = JsonSerializer.Serialize(
- inputs.OfType().Select(c => c.FileId),
- OpenAIJsonContext.Default.IEnumerableString);
- json = $$"""{"type":"code_interpreter","container":{"type":"auto",files:{{jsonArray}}} }""";
- }
- else
- {
- json = """{"type":"code_interpreter","container":{"type":"auto"}}""";
- }
-
- result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json)));
+ result.Tools.Add(
+ ResponseTool.CreateCodeInterpreterTool(
+ new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ?
+ CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) :
+ new())));
break;
case HostedMcpServerTool mcpTool:
McpTool responsesMcpTool = ResponseTool.CreateMcpTool(
mcpTool.ServerName,
mcpTool.Url,
- mcpTool.Headers);
+ serverDescription: mcpTool.ServerDescription,
+ headers: mcpTool.Headers);
if (mcpTool.AllowedTools is not null)
{
@@ -673,8 +660,8 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable(() => client.GetResponseAsync("hello"));
+
+ Assert.StartsWith("User-Agent header: OpenAI", e.Message);
+ Assert.Contains("MEAI", e.Message);
+ }
+
private static IChatClient CreateChatClient(HttpClient httpClient, string modelId) =>
new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
.GetChatClient(modelId)
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
index 43112fa88e3..0db88d499e1 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
@@ -125,10 +125,7 @@ public async Task GetEmbeddingsAsync_ExpectedRequestResponse()
using VerbatimHttpHandler handler = new(Input, Output);
using HttpClient httpClient = new(handler);
- using IEmbeddingGenerator> generator = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions
- {
- Transport = new HttpClientPipelineTransport(httpClient),
- }).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
+ using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small");
var response = await generator.GenerateAsync([
"hello, world!",
@@ -188,10 +185,7 @@ public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInR
using VerbatimHttpHandler handler = new(Input, Output);
using HttpClient httpClient = new(handler);
- using IEmbeddingGenerator> generator = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions
- {
- Transport = new HttpClientPipelineTransport(httpClient),
- }).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
+ using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small");
var response = await generator.GenerateAsync([
"hello, world!",
@@ -221,4 +215,24 @@ public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInR
Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0));
}
}
+
+ [Fact]
+ public async Task RequestHeaders_UserAgent_ContainsMEAI()
+ {
+ using var handler = new ThrowUserAgentExceptionHandler();
+ using HttpClient httpClient = new(handler);
+ using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small");
+
+ InvalidOperationException e = await Assert.ThrowsAsync(() => generator.GenerateAsync("hello"));
+
+ Assert.StartsWith("User-Agent header: OpenAI", e.Message);
+ Assert.Contains("MEAI", e.Message);
+ }
+
+ private static IEmbeddingGenerator> CreateEmbeddingGenerator(HttpClient httpClient, string modelId) =>
+ new OpenAIClient(
+ new ApiKeyCredential("apikey"),
+ new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
+ .GetEmbeddingClient(modelId)
+ .AsIEmbeddingGenerator();
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 866e43e172d..79e63162923 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -1034,7 +1034,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
AITool mcpTool = rawTool ?
- ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
+ ResponseTool.CreateMcpTool("deepwiki", serverUri: new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
{
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
@@ -1506,6 +1506,19 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
Assert.Equal(1569, response.Usage.TotalTokenCount);
}
+ [Fact]
+ public async Task RequestHeaders_UserAgent_ContainsMEAI()
+ {
+ using var handler = new ThrowUserAgentExceptionHandler();
+ using HttpClient httpClient = new(handler);
+ using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+
+ InvalidOperationException e = await Assert.ThrowsAsync(() => client.GetResponseAsync("hello"));
+
+ Assert.StartsWith("User-Agent header: OpenAI", e.Message);
+ Assert.Contains("MEAI", e.Message);
+ }
+
private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) =>
new OpenAIClient(
new ApiKeyCredential("apikey"),
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs
new file mode 100644
index 00000000000..6b4b0fa0f1c
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs
@@ -0,0 +1,15 @@
+// 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.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI;
+
+internal sealed class ThrowUserAgentExceptionHandler : HttpMessageHandler
+{
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
+ throw new InvalidOperationException($"User-Agent header: {request.Headers.UserAgent}");
+}